(三) Spring Security Oauth2.0 源码分析--认证中心全流程分析
创始人
2024-03-20 02:27:37
0

一 引言

Spring Security Oauth2.0 的认证中心可以简单的理解为是对Spring Security的加强,也是通过FilterChainProxy(其原理可参考前面的Security源码分析)对客户端进行校验后在达到自定义token颁发站点,进行token的颁发,具体流程如下:
在这里插入图片描述

  • 用户发起token申请请求(‘/oauth/token’),请求被FilterChainProxy过滤器拦截
  • 在FilterChainProxy中通过,通过ClientCredentialsTokenEndpointFilterBasicAuthenticationFilte对oauth的客户端的client_id和client secret的正确性
    ClientCredentialsTokenEndpointFilter: 获取请求参数中的client_id和client secret进行客户端的合法性校验
    BasicAuthenticationFilte: 通过解析请求头中Authorization参数,在通过Base64解密获得client_id和client secret进行客户端的合法性校验(在实际开发中我们一般采用这种方式,防止秘钥的直接暴露)
  • 请求通过FilterChainProxy的层层校验后达到oauth颁发TokenEndpoint的站点,TokenEndpoint会根据当前请求的grant_type匹配到相应的处理器,不同的处理器,根据不同的参数去解析出OAuth2Authentication
  • TokenService根据OAuth2Authentication在底层调用TokenStore去生成token,并根据不同的持久化策略,完成token的持久化
  • 返回token给请求,就可以拿到该凭证作为请求凭证了

二 源码解析

通过前面的文章分析,我们知道SpeingSecurity通过FilterChainProxy来完成相应的校验,我们断点看看他经历了那些校验

在这里插入图片描述
我们主要观察的是ClientCredentialsTokenEndpointFilter和BasicAuthenticationFilte这两个过滤器,里面的其他的过滤器,在前面的一些文章里面做了相应的介绍,这里就不过多解释了.

2.1 客户端认证流程

整合认证流程如下图所示:
在这里插入图片描述
这里我们以BasicAuthenticationFilte为例
先将client_id和client_secre通过如下方式base64进行加密
在这里插入图片描述
把加密的结果放在请求头Authorization中 以Basic+空格+加密结果发起请求
在这里插入图片描述
注意: 这里不要在请求参数中携带client_id和client_secre如果在参数中携带扎两个参数就会ClientCredentialsTokenEndpointFilter进行客户单合法性的校验,在BasicAuthenticationFilte不在进行合法性的校验
在这里插入图片描述

2.2.1 客户端认证流程源码详解

当用户通过用户名密码进行认证获取access_token的时候,首先需要认证的是客户端是否正确验证方式是通过用户设置Header的Authorization ,最终序列化成Basic编码发送给认证服务。认证服务器通过BasicAuthenticationFilter过滤器进行实现。
BasicAuthenticationFilter 类结构分析
BasicAuthenticationFilter 类继承了OncePerRequestFilter,而OncePerRequestFilter是Spring框架自带的基础过滤器抽象类。

public class BasicAuthenticationFilter extends OncePerRequestFilter {
......
}

OncePerRequestFilter 是Spring默认的基础过滤器抽象类,其使用的设计模式是模板方法。封装核心的过滤条件,将需要实现的细节,移交给子类实现:

public abstract class OncePerRequestFilter extends GenericFilterBean {// 通过 final 定义的模板方法@Overridepublic final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)throws ServletException, IOException {if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {throw new ServletException("OncePerRequestFilter just supports HTTP requests");}HttpServletRequest httpRequest = (HttpServletRequest) request;HttpServletResponse httpResponse = (HttpServletResponse) response;String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {// Proceed without invoking this filter...filterChain.doFilter(request, response);}else {// Do invoke this filter...request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);try {// 具体的实现方式交给子类去实现doFilterInternal(httpRequest, httpResponse, filterChain);}finally {// Remove the "already filtered" request attribute for this request.request.removeAttribute(alreadyFilteredAttributeName);}}}// 子类需要实现的抽象方法,这里的实现是:BasicAuthenticationFilter 的doFilterInternal 方法protected abstract void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException;
}

BasicAuthenticationFilter 核心方法和参数分析

public class BasicAuthenticationFilter extends OncePerRequestFilter {// 通过构造方法,引入AuthenticationManager认证管理器。其核心的实现就是:ProviderManagerprivate AuthenticationManager authenticationManager;// 构造方法public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {Assert.notNull(authenticationManager, "authenticationManager cannot be null");this.authenticationManager = authenticationManager;}//模板方法的核心实现,用于认证客户端的正确性@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain chain)throws IOException, ServletException {// 从Header 头信息中获取Authorization的值// 通过方法extractAndDecodeHeader(header, request) 反序列出用户名和密码final boolean debug = this.logger.isDebugEnabled();String header = request.getHeader("Authorization");if (header == null || !header.toLowerCase().startsWith("basic ")) {chain.doFilter(request, response);return;}try {String[] tokens = extractAndDecodeHeader(header, request);assert tokens.length == 2;String username = tokens[0];if (debug) {this.logger.debug("Basic Authentication Authorization header found for user '"+ username + "'");}// 校验当前客户端用户是不是需要重新认证if (authenticationIsRequired(username)) {// 封装UsernamePasswordAuthenticationToken对象,该对象实现了Authentication 接口。UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, tokens[1]);authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));// 通过构造函数引入的AuthenticationManager进行认证//具体的显现方式,根据UsernamePasswordAuthenticationToken所对应的认证策略// 这里使用的认证策略是DaoAuthenticationProviderAuthentication authResult = this.authenticationManager.authenticate(authRequest);if (debug) {this.logger.debug("Authentication success: " + authResult);}// 认证通过已经,将当前信息写入到SecurityContextHolder中SecurityContextHolder.getContext().setAuthentication(authResult);this.rememberMeServices.loginSuccess(request, response, authResult);onSuccessfulAuthentication(request, response, authResult);}}catch (AuthenticationException failed) {SecurityContextHolder.clearContext();if (debug) {this.logger.debug("Authentication request for failed: " + failed);}this.rememberMeServices.loginFail(request, response);onUnsuccessfulAuthentication(request, response, failed);if (this.ignoreFailure) {chain.doFilter(request, response);}else {this.authenticationEntryPoint.commence(request, response, failed);}return;}// 认证通过以后,调用下一个过滤器chain.doFilter(request, response);}private String[] extractAndDecodeHeader(String header, HttpServletRequest request)throws IOException {byte[] base64Token = header.substring(6).getBytes("UTF-8");byte[] decoded;try {decoded = Base64.getDecoder().decode(base64Token);}catch (IllegalArgumentException e) {throw new BadCredentialsException("Failed to decode basic authentication token");}String token = new String(decoded, getCredentialsCharset(request));int delim = token.indexOf(":");if (delim == -1) {throw new BadCredentialsException("Invalid basic authentication token");}return new String[] { token.substring(0, delim), token.substring(delim + 1) };}private boolean authenticationIsRequired(String username) {// Only reauthenticate if username doesn't match SecurityContextHolder and user// isn't authenticated// (see SEC-53)Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();if (existingAuth == null || !existingAuth.isAuthenticated()) {return true;}if (existingAuth instanceof UsernamePasswordAuthenticationToken&& !existingAuth.getName().equals(username)) {return true;}
}

通过基础认证服务器的核心代码模块分析可以知道,主要完成两件事情,第一:反序列化客户端的Header参数Authorization。第二:封装UsernamePasswordAuthenticationToken对象,调用认证管理器ProviderManager进行认证

AbstractUserDetailsAuthenticationProvider 类结构分析
AbstractUserDetailsAuthenticationProvider 实现了AuthenticationProvider的 supports(Class authentication)
方法

public abstract class AbstractUserDetailsAuthenticationProvider implementsAuthenticationProvider, InitializingBean, MessageSourceAware {......}

AbstractUserDetailsAuthenticationProvider 核心方法参数分析
AbstractUserDetailsAuthenticationProvider 主要实现了AuthenticationProvider的两个核心方法:
AuthenticationProvider,supports。其次抽象出具体是实现细节方法:retrieveUser。交给子类:
DaoAuthenticationProvider 进行实现

public abstract class AbstractUserDetailsAuthenticationProvider implementsAuthenticationProvider, InitializingBean, MessageSourceAware {// 实现	AuthenticationProvider 的认证方法public Authentication authenticate(Authentication authentication)throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));// Determine usernameString username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {// 调用子类实现的retrieveUser()的方法user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}catch (UsernameNotFoundException notFound) {logger.debug("User '" + username + "' not found");if (hideUserNotFoundExceptions) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}else {throw notFound;}}Assert.notNull(user,"retrieveUser returned null - a violation of the interface contract");}try {preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}catch (AuthenticationException exception) {if (cacheWasUsed) {// There was a problem, so try again after checking// we're using latest data (i.e. not from the cache)cacheWasUsed = false;user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}else {throw exception;}}postAuthenticationChecks.check(user);if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (forcePrincipalAsString) {
**加粗样式**			principalToReturn = user.getUsername();}return createSuccessAuthentication(principalToReturn, authentication, user);}//实现了AuthenticationProvider的supports方法public boolean supports(Class authentication) {//根据配置的策略方法为UsernamePasswordAuthenticationTokenreturn (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));}// 检索用户细节交给子类实现protected abstract UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException;
}

DaoAuthenticationProvider 核心方法参数分析

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";// 设置当前密码的加密模式private PasswordEncoder passwordEncoder;// 设置查询用户实现细节private UserDetailsService userDetailsService;public DaoAuthenticationProvider() {setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());}protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {// 通过查询用户实现细节类,查询当前客户端用户是否存在UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}private void prepareTimingAttackProtection() {if (this.userNotFoundEncodedPassword == null) {this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);}}private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {if (authentication.getCredentials() != null) {String presentedPassword = authentication.getCredentials().toString();this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);}}
}

ClientDetailsUserDetailsService 类说明

ClientDetailsUserDetailsService 实现了UserDetailsService,通过loadUserByUsername()方法查询当前客户端是否存在。

public class ClientDetailsUserDetailsService implements UserDetailsService {private final ClientDetailsService clientDetailsService;private String emptyPassword = "";public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {this.clientDetailsService = clientDetailsService;}/*** @param passwordEncoder the password encoder to set*/public void setPasswordEncoder(PasswordEncoder passwordEncoder) {this.emptyPassword = passwordEncoder.encode("");}// 通过 loadUserByUsername 查询当前的Client是否存在。public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ClientDetails clientDetails;try {clientDetails = clientDetailsService.loadClientByClientId(username);} catch (NoSuchClientException e) {throw new UsernameNotFoundException(e.getMessage(), e);}String clientSecret = clientDetails.getClientSecret();if (clientSecret== null || clientSecret.trim().length()==0) {clientSecret = emptyPassword;}return new User(username, clientSecret, clientDetails.getAuthorities());}}

我们采用的是JdbcClientDetailsService,通过查询数据库获得其具体配置
在这里插入图片描述

总结:
通过客户端认证源码分析可以得出,客户端的认证会发生在过滤器:BasicAuthenticationFilter中,其发生在用户的用户名密码认证之前。其内部认证通过ProviderManager策略模板,根据传入的Authentication类型指定认证的策略DaoAuthenticationProvider,通过DaoAuthenticationProvider查询当前客户端用户密码是否存在。我们项目采用的是:JdbcClientDetailsService,这里用户可以自己去实现客户端查询细节,通过启动配置类进行配置通过ClientDetailsServiceConfigurerwithClientDetails(ClientDetailsService clientDetailsService)方法进行设置。

三 token的获取

在这里插入图片描述

整个流程中主要核心分为:

  • 用户的用户名密码认证
  • 根据用户名,客户端信息,权限信息生成对应的Token

3.1 用户的用户名密码认证

访问/oauth/token url 接口

该接口是实现生成AccessToken的入口。

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)public ResponseEntity postAccessToken(Principal principal, @RequestParamMap 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);// 根据ClientId查询当前客户端的详细信息ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);// 根据客户端信息和请求参数,封装成TokenRequest对象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 clientthrow 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 stepif (!tokenRequest.getScope().isEmpty()) {logger.debug("Clearing scope of incoming token request");tokenRequest.setScope(Collections. 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)));}// 获取AccessTokenOAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);if (token == null) {throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());}return getResponse(token);}
}

从实现源码细节中可以看出,主要做了客户端校验,以及获取OAuth2AccessToken 的两件核心事件。在获取OAuth2AccessToken对象的过程中,首先需要做的是用户的用户名密码认证。

验证用户的用户名和密码

当前的认证模式为用户名密码认证方式,其Token的整体授权类的继承如下:
在这里插入图片描述
这里我们使用的是:ResourceOwnerPasswordTokenGranter 授权类。首先进行用户的用户名密码认证。其核心代码如下:

@Overrideprotected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {Map parameters = new LinkedHashMap(tokenRequest.getRequestParameters());String username = parameters.get("username");String password = parameters.get("password");// Protect from downstream leaks of passwordparameters.remove("password");// 封装成UsernamePasswordAuthenticationToken对象,找到对应的认证方式。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 grantthrow new InvalidGrantException(e.getMessage());}// 如果当前用户不存在,即抛出异常if (userAuth == null || !userAuth.isAuthenticated()) {throw new InvalidGrantException("Could not authenticate user: " + username);}// 当前用户存在即封装成OAuth2Request对象OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);		// 返回封装好的OAuth2Authentication实例对象return new OAuth2Authentication(storedOAuth2Request, userAuth);}

至此用户的用户名密码认证已经完成。接下来即是根据用户信息生成Token。

3.2 .生成OAuth2AccessToken对象

OAuth2AccessToken对象详解

这一步也是最关键的一步,即生成OAuth2AccessToken对象,里面封装了认证完成的所有信息,下面我们深入的去看下其源码。首先看下其类继承关系图:
在这里插入图片描述
通过类继承图可以发现,其默认实现是:DefaultOAuth2AccessToken。其核心源码是:

public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {private static final long serialVersionUID = 914967629530462926L;
// 生动的access_tokenprivate String value;
// 过期时间private Date expiration;
// 刷新token方式private OAuth2RefreshToken refreshToken;
// 当前权限private Set scope;
// 额外的增强参数private Map additionalInformation = Collections.emptyMap();
//构造函数public DefaultOAuth2AccessToken(String value) {this.value = value;}@SuppressWarnings("unused")private DefaultOAuth2AccessToken() {this((String) null);}// 构造函数public DefaultOAuth2AccessToken(OAuth2AccessToken accessToken) {this(accessToken.getValue());setAdditionalInformation(accessToken.getAdditionalInformation());setRefreshToken(accessToken.getRefreshToken());setExpiration(accessToken.getExpiration());setScope(accessToken.getScope());setTokenType(accessToken.getTokenType());}
}

DefaultTokenServices 生成Token

DefaultTokenServices 是Token的默认生成类,通过分析DefaultTokenServices生成类源码,我们可以清晰的知道Token的生成方式。下面我们看下其核心源码实现。其中标注数字的如:1,2,3等注解,都会进一步解析

// 首先该类加了注解,保证其事务的完整性
@Transactionalpublic OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {// 1.查询当前Token是否已经存在于数据库中OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);OAuth2RefreshToken refreshToken = null;// 如果Token已经存在,做一下的逻辑处理if (existingAccessToken != null) {// 如果当前Token 已经存在,且已经过期。if (existingAccessToken.isExpired()) {// 如果当前的RefreshToken不为null的情况下。移除当前RefreshTokenif (existingAccessToken.getRefreshToken() != null) {refreshToken = existingAccessToken.getRefreshToken();// The token store could remove the refresh token when the// access token is removed, but we want to// be sure...tokenStore.removeRefreshToken(refreshToken);}// 移除AccessTokentokenStore.removeAccessToken(existingAccessToken);}// 如果token没有过期,还是使用原来的Token,重新存储。为了防止有权限修改. {// Re-store the access token in case the authentication has changedtokenStore.storeAccessToken(existingAccessToken, authentication);return existingAccessToken;}}// Only create a new refresh token if there wasn't an existing one// associated with an expired access token.// Clients might be holding existing refresh tokens, so we re-use it in// the case that the old access token// expired.if (refreshToken == null) {refreshToken = createRefreshToken(authentication);}// But the refresh token itself might need to be re-issued if it has// expired.else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {refreshToken = createRefreshToken(authentication);}}// 2.创建OAuth2AccessToken 实例OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);tokenStore.storeAccessToken(accessToken, authentication);// In case it was modifiedrefreshToken = accessToken.getRefreshToken();if (refreshToken != null) {tokenStore.storeRefreshToken(refreshToken, authentication);}return accessToken;}

查询当前Token的TokenId的生成方式

查询当前Token是否已经存在于数据库中,最核心的功能模块是,怎么获取TokenId。我们通过源码分析,了解其TokenId的生成方式,主要通过DefaultAuthenticationKeyGenerator 类进行实现的:

public class DefaultAuthenticationKeyGenerator implements AuthenticationKeyGenerator {private static final String CLIENT_ID = "client_id";private static final String SCOPE = "scope";private static final String USERNAME = "username";// 封装核心参数,到map集合中public String extractKey(OAuth2Authentication authentication) {Map values = new LinkedHashMap();OAuth2Request authorizationRequest = authentication.getOAuth2Request();if (!authentication.isClientOnly()) {values.put(USERNAME, authentication.getName());}values.put(CLIENT_ID, authorizationRequest.getClientId());if (authorizationRequest.getScope() != null) {values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet(authorizationRequest.getScope())));}return generateKey(values);}protected String generateKey(Map values) {MessageDigest digest;try {digest = MessageDigest.getInstance("MD5");// 将核心的参数,变成字符串,在通过MD5加密byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));return String.format("%032x", new BigInteger(1, bytes));} catch (NoSuchAlgorithmException nsae) {throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).", nsae);} catch (UnsupportedEncodingException uee) {throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).", uee);}}
}

通过分析源码可以发现,其生成方式,通过封装核心参数。主要有:客户端名称,用户名称,用户具备的权限信息,通过MD5加密生成TokenID。通过这段源码分析,可以确定,一个客户端,可以产生多个access_token。只要其权限,用户名不同即可。

创建OAuth2AccessToken实例对象的具体实现

创建OAuth2AccessToken中,比较核心的模块是access_token的实现方式。下面我们通过源码分析access_token是如何产生的。其实现类是:DefaultTokenServices

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
// 默认生成access_token的方式是UUIDDefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());if (validitySeconds > 0) {token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));}token.setRefreshToken(refreshToken);token.setScope(authentication.getOAuth2Request().getScope());return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;}

通过源码分析,我们得出,其默认生成access_token的方式是:UUID.randomUUID().toString()

四 Token持久化

默认的情况下,SpringOauth2.0 提供4种方式存储。第一种是提供了基于mysql的存储,第二种是基于redis的存储。第三种基于jvm的存储,第四种基于Jwt的存储方式。这里我们主要分析的是mysql的持久化和redis的持久化。首先分析下存储的实现类。

4.1 token存储的接口详解

token的存储是通过TokenStore这个接口实现的,下面我们分析下TokenStore的方法参数。

public interface TokenStore {
//读取指定的用户身份认证OAuth2Authentication readAuthentication(OAuth2AccessToken token);
// 根据token读取指定的用户身份认证OAuth2Authentication readAuthentication(String token);
// 存储token信息和用户认证信息void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
// 根据tokenValue读取token信息OAuth2AccessToken readAccessToken(String tokenValue);
// 移除token信息void removeAccessToken(OAuth2AccessToken token);
// 存储刷新token信息void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);
// 读取刷新token信息OAuth2RefreshToken readRefreshToken(String tokenValue);
// 读取Token详细信息OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
// 通过客户端和用户名查询当前授权的所有token信息Collection findTokensByClientIdAndUserName(String clientId, String userName);
// 查询当前客户端下的所有用认证的token信息Collection findTokensByClientId(String clientId);
}

通过上面的分析,我们可以知道,当前存储的主键:token_id的生成规则是根据的:

	private static final String CLIENT_ID = "client_id";private static final String SCOPE = "scope";private static final String USERNAME = "username";

传输的这三个值做MD5生成的。所以,同一个客户端下,可以存在多个用户的token的信息。

4.2 TokenStore接口的实现详解

在这里插入图片描述
通过接口的实现可以得出,其主要的有四种方式来存储Token。

  • RedisTokenStore 通过Redis的方式进行存储
  • JdbcTokenStore 通过Jdbc序列化的方式进行存储
  • InMemoryTokenStore 直接将当前的Token信息存储在JVM中。
  • JwtTokenStroe 通过Jwt的方式进行存储
    上面的四种方式进行Token的持久化存储。其中InMemoryTokenStore是将当前的token信息存储到jvm中,重启服务后当前token信息将不复存在。所以,只能在测试开发时候使用。

4.2.1 redis中token存储的元数据详解

数据存储在redis中,并不像存储在mysql中那样可以做关联查询,并且根据redis中的数据结构。SpringOauth2.0 在redis中的存储结构如下:

  • auth_to_access
    OAuth2Authentication相关信息加密后的值,value为string结构这个主要是通过OAuth2Authentication来获取OAuth2AccessToken
  • auth:token
    value为string结构这个主要用来获取token的OAuth2Authentication,用来获取相应的权限信息
  • client_id_to_access:clientId
    value为list结构这个主要是存储了每个clientId申请的OAuth2AccessToken的集合
    方便用来审计和应急处理跟clientId相关的token
  • access:token
    value为string这个主要是通过token值来获取OAuth2AccessToken
  • uname_to_access:clientId:userId
    value的结构是list存储OAuth2AccessToken的集合主要是为了通过clientId,userId来获取OAuth2AccessToken集合,方便用来获取及revoke approval

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...