SpringSecurity OAuth2 获取Token端点TokenEndpoint、Token授权TokenGranter接口 详解

2023-05-16

1、前言

  在《授权服务器是如何实现授权的呢?》中,我们可以了解到服务端实现授权的流程,同时知道,当授权端点AuthorizationEndpoint生成授权码时,就会重定向到客户端的请求地址,这个时候,客户端就会拿着授权码再来授权服务器换取对应的Token,这篇内容,我们就详细分析如何使用授权码code换取Token的。在前面文章中,我们可以了解到客户端是通过“/oauth/token”来换取token的,该接口对应TokenEndpoint类的postAccessToken()方法,我们这篇文章就围绕获取token的TokenEndpoint类进行。

2、TokenEndpoint 获取Token的端点

  TokenEndpoint 是OAuth2规范中描述的令牌请求的端点,主要实现客户端获取token的能力,提供了"/oauth/token"接口,暴露给客户端用来获取Token。

  和授权端点AuthorizationEndpoint 类似,令牌请求端点TokenEndpoint 也继承自AbstractEndpoint抽象类,在《SpringSecurity OAuth2授权端点AuthorizationEndpoint、授权码AuthorizationCodeServices 详解》中,已经分析了AbstractEndpoint抽象类的实现,主要是初始化了TokenGranter、ClientDetailsService、OAuth2RequestFactory和WebResponseExceptionTranslator等对象,这里不再贴出代码进行分析了。
在这里插入图片描述

  和授权端点AuthorizationEndpoint相比,令牌请求端点TokenEndpoint更加简单一些,因为这里只提供了一个Post类型的"/oauth/token"token请求接口(GET类型的token请求接口忽略,实际是调用POST方式实现的,默认不开启),而授权端点AuthorizationEndpoint涉及到授权接口、授权同意接口等,在授权过程中还涉及到了用户交互操作。

“/oauth/token” 令牌请求

  令牌请求端点TokenEndpoint,提供了"/oauth/token"接口,暴露给客户端用来获取Token。默认只支持POST方法,可以通过allowedRequestMethods配置运行GET方法。

  该方法的调用发生在授权请求之后,跳转到业务界面之前,即需要访问授权的业务页面时,使用获取的授权码code,来换取对应的token,不过该步骤对前端的浏览器是不可见的,发生授权服务器和业务客户端之间的请求。

  "/oauth/token"接口对应的postAccessToken()方法,实现的逻辑如下:

  1. 首先,判断请求中的principal参数是不是Authentication类型,该方法只处理Authentication类型的参数,不是该类型参数的直接抛出InsufficientAuthenticationException异常。
  2. 然后,通过getClientId()方法,从principal参数中获取client的信息(clientId)。首先判断,是否已经被授权,如果没有被授权,就直接抛出InsufficientAuthenticationException异常,如果已经授权就返回对应的clientId,其中OAuth2Authentication类型的参数时,获取clientId是通过OAuth2Request对象获取。
protected String getClientId(Principal principal) {
	Authentication client = (Authentication) principal;
	if (!client.isAuthenticated()) {
		throw new InsufficientAuthenticationException("The client is not authenticated.");
	}
	String clientId = client.getName();
	if (client instanceof OAuth2Authentication) {
		// Might be a client and user combined authentication
		clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId();
	}
	return clientId;
}
  1. 获取客户端信息(ClientDetails)。根据上一步获取到的clientId,通过ClientDetailsService获取对应ClientDetails信息。默认提供了两种ClientDetailsService对象的实现,也可以自定义进行实现。后续详细分析ClientDetailsService实现类。
  2. 创建TokenRequest对象。根据传递的参数parameters和获取到的客户端详细信息authenticatedClient,通过OAuth2RequestFactory,创建TokenRequest对象。其中,OAuth2RequestFactory默认使用DefaultOAuth2RequestFactory对象。
  3. 验证clientId。clientId不能为空,且请求中的client信息,要与存储在授权服务器端的客户端信息保持一致。
  4. 校验scope。通过OAuth2RequestValidator对象进行校验scope,默认实现DefaultOAuth2RequestValidator,通过对比请求中的scope和客户端authenticatedClient对象进行比较,进而实现判断。
  5. 判断grant type。不能为空,且在该模式下,不支持implicit(简单)认证模式。
  6. 设置scope。判断是否是刷新token或授权码验证,并根据结果设置对应的scope。
  7. 生成Token。到这里,就是真正产生Token的地方了。通过TokenGranter,来生成对应的Token对象。关于TokenGranter实现方式,后续将会详细介绍。
    10.返回对象。把生成的token,通过调用getResponse()方法进行返回。

 &esmp;关于"/oauth/token"接口对应的postAccessToken()方法的完整实现如下:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

	if (!(principal instanceof Authentication)) {
		throw new InsufficientAuthenticationException(
				"There is no client authentication. Try adding an appropriate authentication filter.");
	}

	String clientId = getClientId(principal);
	ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

	TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

	if (clientId != null && !clientId.equals("")) {
		// Only validate the client details if a client authenticated during this
		// request.
		if (!clientId.equals(tokenRequest.getClientId())) {
			// double check to make sure that the client ID in the token request is the same as that in the
			// authenticated client
			throw new InvalidClientException("Given client ID does not match authenticated client");
		}
	}
	if (authenticatedClient != null) {
		oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
	}
	if (!StringUtils.hasText(tokenRequest.getGrantType())) {
		throw new InvalidRequestException("Missing grant type");
	}
	if (tokenRequest.getGrantType().equals("implicit")) {
		throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
	}
	if (isAuthCodeRequest(parameters)) {
		// The scope was requested or determined during the authorization step
		if (!tokenRequest.getScope().isEmpty()) {
			logger.debug("Clearing scope of incoming token request");
			tokenRequest.setScope(Collections.<String> emptySet());
		}
	}
	if (isRefreshTokenRequest(parameters)) {
		// A refresh token has its own default scopes, so we should ignore any added by the factory here.
		tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
	}
	OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
	if (token == null) {
		throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
	}
	return getResponse(token);
}

3、ClientDetailsService 客户端详细信息查询

  在前面的postAccessToken()方法中,用到了ClientDetailsService的loadClientByClientId()方法获取对应ClientDetails信息,这里我们着重看一下ClientDetailsService 实现方式。

  ClientDetailsService 客户端信息管理,提供了根据clientId查询客户端详细信息的方法,框架提供了InMemoryClientDetailsService和JdbcClientDetailsService两个实现类。
在这里插入图片描述

ClientDetailsService接口

  ClientDetailsService接口定义了一个查询客户端详细信息的接口,如下所示:

public interface ClientDetailsService {
  /**
   * 查询客户端详细信息
   */
  ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;

}
InMemoryClientDetailsService实现类

  InMemoryClientDetailsService实现了ClientDetailsService接口,该实现类实现了客户端信息的内存存储,即存储在了定义的clientDetailsStore属性(Map<String, ClientDetails>类型)中,key对应clientId,value对应客户端详细信息ClientDetails。

  除了实现了接口中定义的loadClientByClientId()方法,还提供了一个设置客户端信息的方法(即为clientDetailsStore 属性赋值)setClientDetailsStore()。具体实现如下:

public class InMemoryClientDetailsService implements ClientDetailsService {

  private Map<String, ClientDetails> clientDetailsStore = new HashMap<String, ClientDetails>();

  public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
    ClientDetails details = clientDetailsStore.get(clientId);
    if (details == null) {
      throw new NoSuchClientException("No client with requested id: " + clientId);
    }
    return details;
  }

  public void setClientDetailsStore(Map<String, ? extends ClientDetails> clientDetailsStore) {
    this.clientDetailsStore = new HashMap<String, ClientDetails>(clientDetailsStore);
  }
}

  JdbcClientDetailsService实现类和InMemoryClientDetailsService接口类似,不过JdbcClientDetailsService不仅实现了ClientDetailsService 接口,还实现了ClientRegistrationService接口,即提供了客户端信息的注册能力。

  ClientRegistrationService接口定义,如下:

public interface ClientRegistrationService {
	//增加客户端信息
	void addClientDetails(ClientDetails clientDetails) throws ClientAlreadyExistsException;
	//修改客户端信息
	void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException;
	//更新客户端秘钥
	void updateClientSecret(String clientId, String secret) throws NoSuchClientException;
	//删除客户端信息
	void removeClientDetails(String clientId) throws NoSuchClientException;
	//查询客户端信息
	List<ClientDetails> listClientDetails();
}

4、TokenGranter 生成授权token

4.1、TokenGranter 层级结构

在这里插入图片描述
  其中,AuthorizationCodeTokenGranter 授权码模式、ClientCredentialsTokenGranter 客户端模式、ImplicitTokenGranter implicit 模式、RefreshTokenGranter 刷新 token 模式、ResourceOwnerPasswordTokenGranter 密码模式。组合代理类 CompositeTokenGranter。

  TokenGranter 接口有两个子类,其中AbstractTokenGranter 抽象类是 TokenGranter 接口的通用实现,其他真正实现TokenGranter 功能的类,都继承自AbstractTokenGranter 抽象类,而CompositeTokenGranter子类主要是为了组合使用其他TokenGranter实现类。

4.2、TokenGranter 接口
public interface TokenGranter {
	//生成Token
	OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}
4.3、AbstractTokenGranter 抽象类

  在AbstractTokenGranter抽象类中,定义了AuthorizationServerTokenServices、ClientDetailsService、OAuth2RequestFactory和grantType四个字段,并在提供了一个带四个参数的构造函数。

  grant()方法的实现逻辑:首先,验证grantType是否匹配,然后通过clientDetailsService对象获取客户端ClientDetails信息,验证客户端是否支持当前请求的grantType类型,最后再通过getAccessToken()方法获取OAuth2AccessToken对象。具体实现如下:

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

	if (!this.grantType.equals(grantType)) {
		return null;
	}
	String clientId = tokenRequest.getClientId();
	ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
	validateGrantType(grantType, client);

	if (logger.isDebugEnabled()) {
		logger.debug("Getting access token for: " + clientId);
	}
	return getAccessToken(client, tokenRequest);
}

  在getAccessToken()方法中,首先通过调用getOAuth2Authentication()方法,获取OAuth2Authentication对象,然后又通过tokenServices的createAccessToken()方法创建Token对象,具体实现如下:

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
	return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}

  在getOAuth2Authentication()方法中,首先通过OAuth2RequestFactory对象创建storedOAuth2Request对象,然后根据该对象创建OAuth2Authentication实例并返回,具体实现如下:

protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
	OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
	return new OAuth2Authentication(storedOAuth2Request, null);
}

  在抽象类AbstractTokenGranter的子类中,就是通过重写grant()方法、getAccessToken()方法或getOAuth2Authentication()方法实现对应功能的。我们分别进行分析:

4.4、RefreshTokenGranter 刷新Token的实现

  RefreshTokenGranter子类,是通过重写getAccessToken()方法实现刷新Token功能的,在抽象类中定义的getAccessToken()方法是通过AuthorizationServerTokenServices的createAccessToken()方法创建对象的,而这里通过调用AuthorizationServerTokenServices的refreshAccessToken()方法实现刷新token的功能,关于AuthorizationServerTokenServices后续后专门详细分析,这里暂不展开,具体实现如下:

//RefreshTokenGranter.java
@Override
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
	String refreshToken = tokenRequest.getRequestParameters().get("refresh_token");
	return getTokenServices().refreshAccessToken(refreshToken, tokenRequest);
}
4.5、ClientCredentialsTokenGranter 客户端授权模式

  ClientCredentialsTokenGranter子类,是通过重写grant()方法实现客户端授权功能的,首先通过调用父类的grant()方法获取对应OAuth2AccessToken 对象,然后再根据allowRefresh参数设置refreshToken为空即可,说明一般客户端的认证不允许刷新token。

//ClientCredentialsTokenGranter.java
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
	OAuth2AccessToken token = super.grant(grantType, tokenRequest);
	if (token != null) {
		DefaultOAuth2AccessToken norefresh = new DefaultOAuth2AccessToken(token);
		// The spec says that client credentials should not be allowed to get a refresh token
		if (!allowRefresh) {
			norefresh.setRefreshToken(null);
		}
		token = norefresh;
	}
	return token;
}
4.5、ResourceOwnerPasswordTokenGranter

  ResourceOwnerPasswordTokenGranter子类,是通过重写getOAuth2Authentication()方法实现资源授权功能的。

  在ResourceOwnerPasswordTokenGranter子类中,又增加了一个AuthenticationManager字段的定义,主要用来实现用户名密码的验证,并生成对应的Authentication对象,具体实现如下:

//ResourceOwnerPasswordTokenGranter.java
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

	Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
	String username = parameters.get("username");
	String password = parameters.get("password");
	// Protect from downstream leaks of password
	parameters.remove("password");

	Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
	((AbstractAuthenticationToken) userAuth).setDetails(parameters);
	try {
		userAuth = authenticationManager.authenticate(userAuth);
	}
	catch (AccountStatusException ase) {
		//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
		throw new InvalidGrantException(ase.getMessage());
	}
	catch (BadCredentialsException e) {
		// If the username/password are wrong the spec says we should send 400/invalid grant
		throw new InvalidGrantException(e.getMessage());
	}
	if (userAuth == null || !userAuth.isAuthenticated()) {
		throw new InvalidGrantException("Could not authenticate user: " + username);
	}
	
	OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		
	return new OAuth2Authentication(storedOAuth2Request, userAuth);
}

  在ResourceOwnerPasswordTokenGranter重写的getOAuth2Authentication()方法中,首先获取用户名密码构建UsernamePasswordAuthenticationToken对象,然后通过AuthenticationManager进行校验,返回校验过的Authentication对象,否则就会抛出对应的异常,认证成功后,再通过OAuth2RequestFactory创建OAuth2Request对象,最后new一个OAuth2Authentication实例对象并返回,并在父类中根据该对象创建OAuth2AccessToken对象。即该过程中,首先完成了用户名密码的校验,然后才生成对应的token。

4.6、AuthorizationCodeTokenGranter

  AuthorizationCodeTokenGranter子类,和ResourceOwnerPasswordTokenGranter类一样,都是通过重写getOAuth2Authentication()方法实现对应功能的。但是在AuthorizationCodeTokenGranter类中,引入了AuthorizationCodeServices属性,通过调用consumeAuthorizationCode()方法,获取授权码对应的用户认证信息OAuth2Authentication,然后再根据认证信息获取存储的OAuth2Request对象,再获取其中的redirectUri和ClientId参数与调用传递参数对比校验,再创建新的OAuth2Request对象,并结合获取的Authentication对象,new一个OAuth2Authentication实例对象进行返回,并在父类中根据该对象创建OAuth2AccessToken对象。具体实现如下:

//AuthorizationCodeTokenGranter.java
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

	Map<String, String> parameters = tokenRequest.getRequestParameters();
	String authorizationCode = parameters.get("code");
	String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);
	if (authorizationCode == null) {
		throw new InvalidRequestException("An authorization code must be supplied.");
	}
	OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
	if (storedAuth == null) {
		throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
	}
	OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
	String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
			OAuth2Utils.REDIRECT_URI);
	if ((redirectUri != null || redirectUriApprovalParameter != null)
			&& !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
		throw new RedirectMismatchException("Redirect URI mismatch.");
	}
	String pendingClientId = pendingOAuth2Request.getClientId();
	String clientId = tokenRequest.getClientId();
	if (clientId != null && !clientId.equals(pendingClientId)) {
		throw new InvalidClientException("Client ID mismatch");
	}
	Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
			.getRequestParameters());
	combinedParameters.putAll(parameters);
	OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
	Authentication userAuth = storedAuth.getUserAuthentication();
	return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);
}
4.7、ImplicitTokenGranter

  ImplicitTokenGranter子类,和AuthorizationCodeTokenGranter 、ResourceOwnerPasswordTokenGranter类一样,都是通过重写getOAuth2Authentication()方法实现对应功能的。不过在ImplicitTokenGranter重写的getOAuth2Authentication()方法中,不需要再做校验,直接获取SpringSecurity上下文中存储的用户认证信息即可,具体实现如下:

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest clientToken) {

	Authentication userAuth = SecurityContextHolder.getContext().getAuthentication();
	if (userAuth==null || !userAuth.isAuthenticated()) {
		throw new InsufficientAuthenticationException("There is no currently logged in user");
	}
	Assert.state(clientToken instanceof ImplicitTokenRequest, "An ImplicitTokenRequest is required here. Caller needs to wrap the TokenRequest.");

	OAuth2Request requestForStorage = ((ImplicitTokenRequest)clientToken).getOAuth2Request();

	return new OAuth2Authentication(requestForStorage, userAuth);
}

  TokenGranter接口的CompositeTokenGranter实现类,这里不再详细信息,就是代理真的实现类使用,可以组合多种TokenGranter实现类,循环调用即可。

  在TokenGranter接口的几个实现类中,仔细回想一下,其实真正实现token生成的其实是AuthorizationServerTokenServices对象,在RefreshTokenGranter实现类中是通过调用refreshAccessToken()方法实现,而在AuthorizationCodeTokenGranter、ImplicitTokenGranter和ResourceOwnerPasswordTokenGranter三个子类中,是通过重写getOAuth2Authentication()方法,获取对应的认证信息-OAuth2Authentication对象,然后再使用获取到的认证信息,调用AuthorizationServerTokenServices对象的createAccessToken()方法来生成token对象,而ClientCredentialsTokenGranter实现类则是直接使用了抽象类中的定义方法,实际上也是通过AuthorizationServerTokenServices对象的createAccessToken()方法来生成token对象,所以归根结底,生成token的方法又落到了AuthorizationServerTokenServices对象上。后续,我们专门一篇博文分析AuthorizationServerTokenServices的实现。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpringSecurity OAuth2 获取Token端点TokenEndpoint、Token授权TokenGranter接口 详解 的相关文章

随机推荐

  • matplotlib绘图中文乱码问题--解决方案(Windows)

    最近使用python绘图时 xff0c 出现中文乱码问题 xff0c 结合在网上搜索理解后 xff0c 按照如下步骤 xff0c 成功解决 解决方案 xff1a 步骤一 xff1a 找到Mircosoft YaHei UI字体文件 一般 在
  • Python数据分析之--运动员数据揭秘(一)

    在网易云课堂看了城市数据团的课程 xff0c 对理解利用pytthon进行数据分析的基本流程很有帮助 xff0c 因此进行复盘总结 xff0c 加深自己的理解 xff0c 巩固相关操作 分析资料及工具 xff1a Spyder Python
  • 缓慢变化维

    一 什么是缓慢变化维 xff1f 缓慢变化维 xff08 Slowly Changing Dimensions SCD xff09 它的提出是因为在现实世界中 xff0c 维度的属性并不是静态的 xff0c 它会随着时间的流失发生缓慢的变化
  • 一个小例子带你入门-Tableau

    声明 xff1a 本文是学习W3Cschool教程整理所得 xff0c 非原创 xff0c 原文链接 xff1a W3Cschool 创建任何Tableau数据分析报告涉及三个基本步骤 连接到数据源 它涉及定位数据并使用适当类型的连接来读取
  • SQL 必知必会--函数篇

    对SQL的基础函数做复习回顾 xff0c 本篇涉及的函数知识如下 xff1a 好了 xff0c 下面开始复习 xff1a SQL Aggregate 函数计算从列中取得的值 xff0c 返回一个单一的 值 Max 函数 作用 xff1a 返
  • WPS的Linux Mint版(Ubuntu)提示“系统缺失字体”的解决方法

    wps的Linux Mint版 Ubuntu 版安装成功后 xff0c 可能每次启动的时候都会提示 xff1a 系统缺失字体 xff0c 如图 xff1a 解决方法 1 首先下载字体包并解压 链接 https pan baidu com s
  • SQL必知必会--中级篇(二)

    接上一篇SQL必知必会 中级篇 xff08 一 xff09 xff0c 继续对sql知识进行整理复习 本篇包含知识点如图 xff1a 一 SQL 约束 用于规定表中的数据规 则 xff1b 如果存在违反约束的数据行为 xff0c 行为会被约
  • 静态网页个人简历

    程序员的简历是不用随身带的 首先 xff0c 作为程序员自己的简历是比别人特别的 xff1b 程序员应该是有思想 xff0c 有高情商的手工艺人 作为程序员简历是随身带的代码 xff0c 用代码书写的简历就像是一份随身携带着的简历 xff0
  • CMake之CMakeLists.txt编写入门

    自定义变量 主要有隐式定义和显式定义两种 隐式定义的一个例子是PROJECT指令 xff0c 它会隐式的定义 lt projectname gt BINARY DIR和 lt projectname gt SOURCE DIR两个变量 xf
  • 照相机成像原理 数码相机的成像原理

    照相机成像原理 数码相机的成像原理 1 1 数码相机 的成像原理 当打开相机的电源开关后 xff0c 主控程序芯片开始检查整个相机 xff0c 确定各个部件是否处于可工作状态 如果一切正常 xff0c 相机将处于待命状态 xff1b 若某一
  • MySQL 单表查询

    创建数据库并插入数据 创建表 xff0c 数据类型请自行查询 CREATE TABLE fruits id INT NOT NULL sid INT NOT NULL NAME CHAR 255 NOT NULL price DECIMAL
  • react ant Design pro Umi 项目左上角icon不显示

    今天本地运行项目没有问题 xff0c 打包发到远端发现logo icon不显示了 然后找了找资料说 LayoutSettings 哪里logo用链接图片可以本地图片有时候会加载异常 解决方法 xff1a 找到 app tsx 加个logo
  • ZeroMQ发布-订阅模式的应用(C++)

    我使用的ZeroMQ版本是4 2 0 xff0c 应用的是其发布 订阅模式 应该知道的细节 xff1a PUB SUB套接字是慢连接 xff0c 你无法得知SUB是何时开始接收消息的 就算你先打开了SUB套接字 xff0c 后打开PUB发送
  • Ubuntu 问题记录(1)| 关于卸载以源码方式安装的库

    Ubuntu 使用源码安装 lib 后 xff0c 如果要卸载 xff0c 则在 lib build 路径下使用 sudo make uninstall 之后再用 sudo updatedb 更新一下 lib 库 xff0c 再使用 loc
  • 注意字符数组最后会自动加\0

    今天做了一道考研题 规定数组大小为200 但是我没注意到后尾需要加 0 后来果断没有A过去 很伤心 反复不断地尝试怎么都不行 后来经一位仁兄点拨 瞬间豁然 include lt iostream gt include lt cstdio g
  • 在TypeScript中使用parseInt()

    在使用angular写一些东西的时候 xff0c 需要用到parseInt 方法来将时间戳转换成时分秒 xx时 xx分 xx秒 的格式 xff0c 但是因为angular所使用的是Typescript xff0c 而 parseInt st
  • CAS6.2.x ~ 准备(1)

    前言 CAS 企业单点登录 xff0c 目前最新版本是6 2 x Apereo 的 Central Authentication Service xff0c 通常称为CAS CAS是用于web的企业多语言单点登录解决方案 xff0c 并试图
  • MySql 安装,root初始化密码设置

    MySQL下载地址 xff1a https dev mysql com downloads mysql 2 解压MySQL压缩包 将以下载的MySQL压缩包解压到自定义目录下 我的解压目录是 C mysql 8 0 12 winx64 34
  • Python游戏项目--外星人入侵(一)

    一 安装Pygame 在终端输入 xff1a pip install user pygame 二 开始游戏项目 xff08 1 xff09 创建Pygame窗口及响应用户输入 创建一个名为alien invasion py 的文件 xff0
  • SpringSecurity OAuth2 获取Token端点TokenEndpoint、Token授权TokenGranter接口 详解

    1 前言 在 授权服务器是如何实现授权的呢 xff1f 中 xff0c 我们可以了解到服务端实现授权的流程 xff0c 同时知道 xff0c 当授权端点AuthorizationEndpoint生成授权码时 xff0c 就会重定向到客户端的