(三) Spring Security Oauth2.0 源码分析--认证中心全流程分析

news2024/10/4 23:05:21

一 引言

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 定义的模板方法
	@Override
	public 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认证管理器。其核心的实现就是:ProviderManager
	private AuthenticationManager authenticationManager;
  // 构造方法
	public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
		this.authenticationManager = authenticationManager;
	}

 //模板方法的核心实现,用于认证客户端的正确性
	@Override
	protected 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所对应的认证策略
				// 这里使用的认证策略是DaoAuthenticationProvider
				Authentication 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 implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
		......
		}

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

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
		// 实现	AuthenticationProvider 的认证方法
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		// Determine username
		String 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) {
	//根据配置的策略方法为UsernamePasswordAuthenticationToken
			return (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<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);
		// 根据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 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)));
		}
		// 获取AccessToken
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}
		return getResponse(token);
	}
}

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

验证用户的用户名和密码

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

@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");
		// 封装成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 grant
			throw 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_token
	private String value;
// 过期时间
	private Date expiration;
// 刷新token方式
	private OAuth2RefreshToken refreshToken;
// 当前权限
	private Set<String> scope;
// 额外的增强参数
	private Map<String, Object> 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等注解,都会进一步解析

// 首先该类加了注解,保证其事务的完整性
@Transactional
	public 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的情况下。移除当前RefreshToken
				if (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);
				}
				// 移除AccessToken
				tokenStore.removeAccessToken(existingAccessToken);
			}
			// 如果token没有过期,还是使用原来的Token,重新存储。为了防止有权限修改
			. {
				// Re-store the access token in case the authentication has changed
				tokenStore.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 modified
		refreshToken = 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<String, String> values = new LinkedHashMap<String, String>();
		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<String>(authorizationRequest.getScope())));
		}
		return generateKey(values);
	}

	protected String generateKey(Map<String, String> 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的方式是UUID
		DefaultOAuth2AccessToken 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<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);
// 查询当前客户端下的所有用认证的token信息
	Collection<OAuth2AccessToken> 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/64013.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ARM异常处理(4):SVC和PendSV的作用详解

SVC(Supervisor Call)和PendSV(Pendable Service Call)是针对软件和操作系统的两个异常。 1 SVC SVC用于生成系统函数调用&#xff0c;例如&#xff0c;用户程序不允许直接访问硬件&#xff0c;操作系统可以通过SVC提供对硬件的访问。因此&#xff0c;当用户程序想要使用某些…

STC 51单片机56——摇摇棒

主要代码&#xff1a; //增减图像时&#xff0c;需要修改 switch_show&#xff08;&#xff09;和 H对应参数 //所用单片机&#xff1a;STC15W408S 内部Rc 11.0592MHz #include <STC15.H> #include <intrins.h> #define POSITIVE 0 #define OPPOSE 1 //sbi…

Vector-常用CAN工具 - CANoe入门到精通_02

​咱们话接上回&#xff0c;前面已经介绍了CANoe的基本信息和硬件环境搭建&#xff0c;下面我们就要打开CANoe&#xff0c;配置CANoe工程了&#xff0c;只有完成了CANoe工程配置才能真正的使用CANoe完成测试&#xff0c;本次主要介绍的是手工测试环境&#xff0c;至于自动化测试…

[附源码]Python计算机毕业设计SSM教师职称评定系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【OpenCV学习】第5课:图像模糊(均值滤波,高斯滤波)

参考文章链接:https://blog.csdn.net/qq_30460949/article/details/121990114 仅自学做笔记用,后续有错误会更改 理论 1.Smooth/blur是图像处理中最简单和常用的操作之一 2.使用该操作的原因之一就是为了给图像预处理的时候减低噪声 3.使用Smooth/Blur操作其背后是数学的卷积…

ContentProvider与ContentResolver

目录&#xff1a;一、什么是ContentProvider&#xff1a;二、如何使用ContentProvider&#xff1a;第一步.创建类继承ContentProvider&#xff1a;第二步.注册ContentProvider&#xff1a;三、URI&#xff1a;1.常用方法&#xff1a;2.解析URI&#xff1a;&#xff08;1&#x…

Android Gradle 学习笔记(二)环境安装

1. 官网安装 Gradle 安装地址&#xff1a;Gradle | Installation 最新的 Gradle 版本是 7.6&#xff0c; 需要 Java 8及以上的版本支持&#xff0c;官网有两种版本可下载&#xff1a; 二进制版本完全版本&#xff08;源码 文档&#xff09; 一般情况下&#xff0c;下载二进…

助力企业降本增效,「WorkPlus SE专业版」正式发布!

移动互联网时代&#xff0c;数字化成为了企业提质降本增效的价值路径。大中小型企业积极主动地借助各种在线办公产品&#xff0c;实现沟通协作和运营管理的优化升级。 随着企业规模的扩大和办公业态多样化发展&#xff0c;会面临着业务流程繁杂、数据利用率低、IT成本高、泄密…

RFID警用装备管理系统-公安警用装备管理可视化系统

一、方案背景 我国警用装备物资种类多、数量大、价值高&#xff0c;还包含枪支、弹药、爆炸物等需要重点监管的物资&#xff0c;在公安机关应对紧急情况、处理突发事件过程中发挥了极为重要的作用。但是&#xff0c;由于缺乏有效的信息化管理手段&#xff0c;还存在管理方式落后…

Tealium 分析

文章目录1.0 调用流程1.1 初始化流程1.2 发送数据流程2.0 Tealium 的设计2.1 总体设计2.1.1 Tealium Core2.1.2 Visitor2.1.3 Crash reporter2.1.4 Ad identifler2.1.5 Lifecycle2.1.6 Location2.1.7 InstallReferrer2.1.8 Hosteddatalayer2.1.8 Dispatcher2.2 网络设计2.3 本地…

黄菊华老师,Java Servlet毕业设计毕设辅导课(4):Servlet 实例

Servlet 实例 Servlet 是服务 HTTP 请求并实现 javax.servlet.Servlet 接口的 Java 类。Web 应用程序开发人员通常编写 Servlet 来扩展 javax.servlet.http.HttpServlet&#xff0c;并实现 Servlet 接口的抽象类专门用来处理 HTTP 请求。 Hello World 示例代码 下面是 Servl…

为什么工业设计公司价格这么高?

随着经济的不断增长&#xff0c;各种工业设计公司逐渐出现&#xff0c;但价格不同&#xff0c;有些价格高&#xff0c;有些价格低&#xff0c;让一些人到处比较价格&#xff0c;低价格压低别人的高价格。有些人会想&#xff0c;为什么工业设计公司在设计产品时价格这么高&#…

ORB-SLAM2 ---- Tracking::UpdateLastFrame函数

目录 1.函数作用 2.步骤 3.code 4.函数解释 4.1 利用参考关键帧更新上一帧在世界坐标系下的位姿 4.2 对于双目或rgbd相机&#xff0c;为上一帧生成新的临时地图点 1.函数作用 更新上一帧位姿&#xff0c;在上一帧中生成临时地图点。 单目情况&#xff1a;只计算了上一帧…

【富文本编辑器】简记功能:neditor上传操作时提交额外数据

目录 编辑器下载&#xff08;本文使用版本v2.1.19&#xff09; 功能需求 解决思路 相关代码 调用实例的html neditor.config.js&#xff08;搜索修改内容&#xff1a;/* 设置额外请求参数 */&#xff09; 完成&#xff0c;如有其它方法&#xff0c;欢迎一起讨论 编辑器下…

使用 Fluent Bit 实现云边统一可观测性

本文基于 KubeSphere 可观测性与边缘计算负责人霍秉杰在北美 KubeCon 的 Co-located event Open Observability Day 闪电演讲的内容进行整理。 整理人&#xff1a;米开朗基杨、大飞哥 Fluent Operator 简介 2019 年 1 月 21 日&#xff0c;KubeSphere 社区为了满足以云原生的方…

NX上配置TLD的环境---对opencv的版本没有要求

一、TLD工程编译及运行 1.1 源码下载 网上的TLD有两个版本&#xff0c;一个是Zdenek Kalal自己使用matlabvs混合编程实现的&#xff0c;另外一个是 arthurv利用c和opencv实现的。 我利用的是arthurv版本的Tracking-Learning-Detection 连接&#xff1a;https://github.com/al…

基于Android平台的手机安全卫士的设计与实现

目 录 第1章 引言 1 1.1 研究背景及意义 1 1.2 安全软件的现状 1 1.3 本文主要工作 2 1.4 本文的组织结构 2 第2章 Android的相关技术介绍及分析 3 2.1 搭建Android开发环境 3 2.1.1 搭建Ubuntu系统下Java开发环境 3 2.1.2 搭建Ubuntu系统下Android开发环境 3 2.2 Android项目目…

「企企通」完成Pre-D轮融资,加速采购供应链工业软件和 SaaS 网络生态构建

企企通作为领先的采购供应链工业软件和SaaS生态平台&#xff0c;在一年内再次宣布获得Pre-D轮融资&#xff0c;全年合计融资额达数亿元人民币&#xff0c;目前还有意向投资机构在进行&#xff0c;并开始启动IPO的筹备工作。本轮投资由华映资本独家投资。华映资本是企企通C2轮融…

flutter系列之:flutter中的变形金刚Transform

文章目录简介Transform简介Transform的使用总结简介 虽然我们在开发APP的过程中是以功能为主&#xff0c;但是有时候为了美观或者其他的特殊的需求&#xff0c;需要对组件进行一些变换。在Flutter中这种变换就叫做Transform。 flutter的强大之处在于&#xff0c;可以对所有的…

IBM MQ 通道

一&#xff0c;定义 通道是分布式队列管理器在IBM MQ MQI 客户端和IBM MQ服务器之间或两个IBM MQ服务器之间使用的逻辑通信链接。 通道是提供从一个队列管理器到另一个队列管理器的通信路径的对象。通道在分布式队列中用于将消息从一个队列管理器移动到另一个队列管理器&#x…