token认证源码解析
总结
前言
1.token认证
1.1 认证入口
用户可以自定义登录端口,默认是oauth2/token,同时新版的security不是直接通过controller来接收token请求,而是通过filterChain来依次处理。上篇有介绍认证的filterChain如下
对应的requestMatcher
其中可以看到OAuth2TokenEndpointFilter,显而易见这个就是取代了以前的TokenEndpoint。同时上图中比较重要的filter有:
OAuth2ClientAuthenticationFilter
OAuth2TokenEndpointFilter
接下来将重点分析这两个filter。
1.2 OAuth2ClientAuthenticationFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//❶请求匹配
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
//❷组装认证对象
Authentication authenticationRequest = this.authenticationConverter.convert(request);
if (authenticationRequest instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authenticationRequest).setDetails(
this.authenticationDetailsSource.buildDetails(request));
}
if (authenticationRequest != null) {
validateClientIdentifier(authenticationRequest);
//❸验证client
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
//❹验证成功后置处理
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
}
filterChain.doFilter(request, response);
} catch (OAuth2AuthenticationException ex) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
❶this.requestMatcher.matches,基本上每个filterChain和filter都有自己的requestMatcher
❷authenticationConverter.convert
public Authentication convert(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}
String[] parts = header.split("\s");
if (!parts[0].equalsIgnoreCase("Basic")) {
return null;
}
byte[] decodedCredentials;
try {
decodedCredentials = Base64.getDecoder().decode(
parts[1].getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException ex) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
}
String credentialsString = new String(decodedCredentials, StandardCharsets.UTF_8);
String[] credentials = credentialsString.split(":", 2);
String clientID;
String clientSecret;
try {
clientID = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8.name());
clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8.name());
} catch (Exception ex) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
}
return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,
OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request));
}
❸authenticationManager.authenticate验证client
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
}
}
return result;
}
同样它也是组合模式,代理了很多AuthenticationProvider
其中ClientSecretAuthenticationProvider即匹配上了第二步生成的认证对象OAuth2ClientAuthenticationToken
@Override
public boolean supports(Class<?> authentication) {
return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
}
接下来就是最核心的认证逻辑了。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientAuthenticationToken clientAuthentication =
(OAuth2ClientAuthenticationToken) authentication;
String clientId = clientAuthentication.getPrincipal().toString();
//❶查询登记的client
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
String clientSecret = clientAuthentication.getCredentials().toString();
//❷比对clientSecret
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}
//❸校验client的有效期
if (registeredClient.getClientSecretExpiresAt() != null &&
Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
throwInvalidClient("client_secret_expires_at");
}
//❹返回认证后的client认证对象
return new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
}
其中❶registeredClientRepository.findByClientId,查询数据库中登记的client信息
❹this.authenticationSuccessHandler.onAuthenticationSuccess
成功后置处理
private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder authentication to %s",
authentication.getClass().getSimpleName()));
}
}
将认证后的client信息放入了上下文中。
1.2 OAuth2TokenEndpointFilter
接下来开始正式处理请求。
public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//❶请求匹配
if (!this.tokenEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
String[] grantTypes = request.getParameterValues(OAuth2ParameterNames.GRANT_TYPE);
//❷组装认证对象
Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);
//❸认证处理
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
//❹认证成功后置处理
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);
} catch (OAuth2AuthenticationException ex) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
大体是和上面讲的OAuth2ClientAuthenticationFilter流程是一样的,不过它没有继续往后请求了,少了filterChain.doFilter,所以请求到这里就结束了。
❶requestMatcher.matches
❷authenticationConverter.convert,组装认证对象
回头看第二篇介绍认证认证服务的FilterChain
然后accessTokenRequestConverter
这样基于密码和短信的认证转换器就接入进来了。基于密码的认证转换器:OAuth2ResourceOwnerPasswordAuthenticationConverter
1.2.1OAuth2ResourceOwnerPasswordAuthenticationConverter(自定义转换器)
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
//❶校验类型(密码,手机号等)
if (!support(grantType)) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// ❷校验个性化参数
checkParams(request);
// ❸获取当前已经认证的客户端信息
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// ❹扩展信息
Map<String, Object> additionalParameters = parameters.entrySet().stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
&& !e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
//❺创建token
return buildToken(clientPrincipal, requestedScopes, additionalParameters);
}
❶support(grantType) 校验类型,如密码:password,手机号:app
❷checkParams校验个性化参数,如密码模式则校验用户名和密码不能为空
❸SecurityContextHolder.getContext().getAuthentication(),获取当前已经认证的client信息。在讲OAuth2ClientAuthenticationFilter时,最后一步则是将认证的client信息放入上下文。
❹获取扩展信息,如账号密码等。
❺buildToken,创建认证对象。
@Override
public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,
Set requestedScopes, Map additionalParameters) {
return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal,
requestedScopes, additionalParameters);
}
到这里认证对象组装完毕,接下来就是认证处理,回到filter那里。第❸步
authenticationManager.authenticate
同样还是被ProviderManager代理,
这里也扩展了如下三个provider:
PigDaoAuthenticationProvider(密码provider)
OAuth2ResourceOwnerPasswordAuthenticationProvider(密码provider)
OAuth2ResourceOwnerSmsAuthenticationProvider(短信provider)
首先看自定义的PigDaoAuthenticationProvider
support的认证对象是spring security核心包提供的UsernamePasswordAuthenticationToken,暂时还不符合
接着自定的OAuth2ResourceOwnerPasswordAuthenticationProvider就是匹配自己定义的认证对象
这里大家可能会有个疑问,为什么要自定义两个密码认证provider,不直接定义一个,后面再解答。
1.2.2 OAuth2ResourceOwnerPasswordAuthenticationProvider(自定义认证器)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
T resouceOwnerBaseAuthentication = (T) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
resouceOwnerBaseAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
checkClient(registeredClient);
Map<String, Object> reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();
try {
//❶构建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);
LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);
//❷认证usernamePasswordAuthenticationToken对象
Authentication usernamePasswordAuthentication = authenticationManager
.authenticate(usernamePasswordAuthenticationToken);
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrant(resouceOwnerBaseAuthentication);
// @formatter:on
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
.withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 0.4.0 新增的方法
.authorizedScopes(authorizedScopes);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
//❸生成accessToken
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.id(accessToken.getTokenValue())
.token(accessToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
((ClaimAccessor) generatedAccessToken).getClaims()))
// 0.4.0 新增的方法
.authorizedScopes(authorizedScopes)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
}
else {
authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
}
//----- Refresh token -----
...
//❹持久化token
this.authorizationService.save(authorization);
}
catch (Exception ex) {
throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex);
}
}
密码登录:
短信登录:
❷authenticationManager.authenticate认证usernamePasswordAuthenticationToken对象,这里就该我们上面提到的自定义PigDaoAuthenticationProvider上场了。这个实现和旧版的基本上一致,需要实现认证信息的获取方式(如数据库)
以及认证信息的校验。(如密码,手机验证码的匹配)
❸tokenGenerator.generate 生成accessToken
public abstract class AbstractTokenGranter implements TokenGranter {
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
新版版认证服务就删掉了tokenService,提供了OAuth2TokenGenerator,默认的有
Pig项目就自定以了TokenGenerator:CustomeOAuth2AccessTokenGenerator
public OAuth2AccessToken generate(OAuth2TokenContext context) {
String issuer = null;
if (context.getAuthorizationServerContext() != null) {
issuer = context.getAuthorizationServerContext().getIssuer();
}
//❶从上下文获取已经认证的client信息
RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now();
//❷过期时间配置在登记的client配置中
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
// @formatter:off
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(context.getPrincipal().getName())
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt)
.id(UUID.randomUUID().toString());
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
}
// @formatter:on
if (this.accessTokenCustomizer != null) {
// @formatter:off
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
.registeredClient(context.getRegisteredClient())
.principal(context.getPrincipal())
.authorizationServerContext(context.getAuthorizationServerContext())
.authorizedScopes(context.getAuthorizedScopes())
.tokenType(context.getTokenType())
.authorizationGrantType(context.getAuthorizationGrantType());
if (context.getAuthorization() != null) {
accessTokenContextBuilder.authorization(context.getAuthorization());
}
if (context.getAuthorizationGrant() != null) {
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
}
// @formatter:on
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
this.accessTokenCustomizer.customize(accessTokenContext);
}
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
//❸生成token对象
return new CustomeOAuth2AccessTokenGenerator.OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
UUID.randomUUID().toString(), accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(),
context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
}
这里生成的token的value是一个随机的uuid,其他认证信息也包含在token对象中,token的有效期是通过登记的client信息配置的。
❹authorizationService.save(authorization)持久化token.
在老版本中,我们通过扩展tokenGranter来扩展token的状态维护。新版本没有了tokenGranter,提供了一个更加友好的扩展类OAuth2AuthorizationService。
这里Pig项目也是实现了Redis的token持久化方式。
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
authorization, between, TimeUnit.SECONDS);
}
}
redis中token的有效期就是前面从登记的client信息中获取的。
到这里整个OAuth2TokenEndpointFilter差不多就结束了,但是这里都是filter,如何将token信息返回回去了?
我们看最后一步❹authenticationSuccessHandler.onAuthenticationSuccess
private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder =
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}
最终的响应如下:
2. 总结
然后整个认证通过filter传递下去,每个filter职责都大致一样(如下),这样对于源码也更有可读性。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//❶请求匹配
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
//❷组装认证对象
Authentication authenticationRequest = this.authenticationConverter.convert(request);
//❸验证认证对象
Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
//❹验证成功后置处理
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
}
filterChain.doFilter(request, response);
} catch (OAuth2AuthenticationException ex) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
客户端认证过程:
token生成过程
(ps,公众号推文图片分片率有限,大家如果想要原文件,可以私信我,我看见消息会回复的哈)

原文始发于微信公众号(小李的源码图):拥抱Spring全新OAuth解决方案(三)–登录认证
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145240.html