Pig4Cloud之登陆验证(一)客户端认证处理
前端登陆
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.$store
.dispatch("LoginByUsername", this.loginForm)
.then(() => {
this.$router.push({path: this.tagWel.value});
})
.catch(() => {
this.refreshCode();
});
}
});
}
看一下LoginByUsername
,在/src/store/modules/user.js中
const scope = 'server'
export const loginByUsername = (username, password, code, randomStr) => {
const grant_type = 'password'
let dataObj = qs.stringify({'username': username, 'password': password})
let basicAuth = 'Basic ' + window.btoa(website.formLoginClient)
// 保存当前选中的 basic 认证信息
setStore({
name: 'basicAuth',
content: basicAuth,
type: 'session'
})
return request({
url: '/auth/oauth2/token',
headers: {
isToken: false,
Authorization: basicAuth
},
method: 'post',
params: {randomStr, code, grant_type, scope},
data: dataObj
})
}
客户端认证
当访问 OAuth2 相关接口时(/oauth2/token
、/oauth2/introspect
、/oauth2/revoke
),授权服务器需要进行客户端认证。
Spring Authorization Server 截至目前支持如下五种客户端认证方式:client_secret_basic
、client_secret_post
、client_secret_jwt
、private_key_jwt
、none
(针对公共客户端)
1. OAuth2ClientAuthenticationFilter
实现客户端认证的拦截器就是 OAuth2ClientAuthenticationFilter
。它继承了OncePerRequestFilter
。
1.1 OncePerRequestFilter
OncePerRequestFilter
是Spring Boot里面的一个过滤器抽象类,其同样在Spring Security里面被广泛用到,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤。OncePerRequestFilter可以保证一次外部请求,只执行一次过滤方法,对于服务器内部之间的forward等请求,不会再次执行过滤方法。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (!this.skipDispatch(httpRequest) && !this.shouldNotFilter(httpRequest)) {
if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
this.doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
filterChain.doFilter(request, response);
} else {
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
this.doFilterInternal(httpRequest, httpResponse, filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
} else {
filterChain.doFilter(request, response);
}
} else {
throw new ServletException("OncePerRequestFilter just supports HTTP requests");
}
}
- 调用 getAlreadyFilteredAttributeName()
方法如下。本质上就是通过 Class 对象再加上一个后缀形成一个 attribute 字符串返回。这个字符串很重要,它将作为每次执行的唯一标识符。
protected String getAlreadyFilteredAttributeName() {
String name = this.getFilterName();
if (name == null) {
name = this.getClass().getName();
}
return name + ".FILTERED";
}
- 请求判断
判断 request 的 attribute 是否存在 alreadyFilteredAttributeName,存在则处理逻辑是 chain.doFilter(),也就是跳过该过滤器,继续执行其他过滤器链,不存在则说明 request 需要过滤,那么会将 alreadyFilteredAttributeName 填入 request 中,接着执行本过滤器的过滤逻辑。 - 最终处理
request.removeAttribute(alreadyFilteredAttributeName);
去掉增加的无用属性,回归原始请求,防止干扰后续参数获取等操作。
1.2 doFilterInternal
OAuth2AuthorizationEndpointFilter
覆写了OncePerRequestFilter
的doFilterInternal
方法
其核心代码如下:
@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) {
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);
}
}
其核心逻辑就是通过 authenticationConverter
从 request 中解析出客户端认证信息,构建成 Authentication,再通过 authenticationManager
对 Authentication 进行认证。
1.3 DelegatingAuthenticationConverter
authenticationConverter
的类型实际上是 DelegatingAuthenticationConverter
,它持有一个 AuthenticationConverter
列表(不同的认证请求,其参数不同,所以会有不同的AuthenticationConverter
实现类)。
public final class DelegatingAuthenticationConverter implements AuthenticationConverter {
private final List<AuthenticationConverter> converters;
/**
* Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
*
* @param converters a {@code List} of {@link AuthenticationConverter}(s)
*/
public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {
Assert.notEmpty(converters, "converters cannot be empty");
this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
}
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
for (AuthenticationConverter converter : this.converters) {
Authentication authentication = converter.convert(request);
if (authentication != null) {
return authentication;
}
}
return null;
}
}
DelegatingAuthenticationConverter
在解析请求时会遍历 AuthenticationConverter
列表,当某个 AuthenticationConverter
解析成功时,立即返回,这也能确定此请求是什么认证方式,后续再执行对应的认证逻辑。
代理转发到ClientSecretBasicAuthenticationConverter
对header进行解析,获取凭证credentials
再次解析credentials
,获得clientID
和clientSecret
,然后构建OAuth2ClientAuthenticationToken
返回认证对象。
1.4 buildDetails
设置认证对象的detail,调用的是WebAuthenticationDetailsSource
的buildDetails
方法,设置请求的上下文。
public class WebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
public WebAuthenticationDetailsSource() {
}
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new WebAuthenticationDetails(context);
}
}
1.5 validateClientIdentifier验证客户端标识符
private static void validateClientIdentifier(Authentication authentication) {
if (!(authentication instanceof OAuth2ClientAuthenticationToken)) {
return;
}
// As per spec, in Appendix A.1.
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#appendix-A.1
// The syntax for client_id is *VSCHAR (%x20-7E):
// -> Hex 20 -> ASCII 32 -> space
// -> Hex 7E -> ASCII 126 -> tilde
OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
String clientId = (String) clientAuthentication.getPrincipal();
for (int i = 0; i < clientId.length(); i++) {
char charAt = clientId.charAt(i);
if (!(charAt >= 32 && charAt <= 126)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
}
}
}
1.6 this.authenticationManager.authenticate认证
可以看到authenticationManager
的类型实际上是 ProviderManager
,它持有一个 AuthenticationProvider
列表(不同的认证方式,其认证逻辑不同,所以会有不同的AuthenticationProvider
实现类)。
AuthenticationManager
接口的默认实现为 ProviderManager
authenticationManager
调用authenticate
实际上是调用ProviderManager
的authenticate
调用 AuthenticationProvider
中的 supports(Class<?> authentication)`` 方法,判断是否支持当前的
Authentication请求。只有支持当前
Authentication请求的
AuthenticationProvider` 才会继续后续逻辑处理。
然后调用 AuthenticationProvider
中的 authenticate
方法进行身份认证。因为在1.3中返回的是OAuth2ClientAuthenticationToken
对象,此处的provider为ClientSecretAuthenticationProvider
在ClientSecretAuthenticationProvider
中调用RegisteredClientRepository
,通过clientId
去数据库查询client
RegisteredClientRepository
的findByClientId
方法
YouxinRemoteRegisteredClientRepository
类为我们自己实现的方法,它实现了RegisteredClientRepository
接口,通过clientid来查询数据库,注册客户端并返回Builder实例。
如果判断无误,则返回OAuth2ClientAuthenticationToken
对象。
1.7 copyDetails拷贝逻辑
如果认证成功且返回的结果不为 null,则执行 authentication details
的拷贝逻辑。
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
......
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
如果result不为null,则返回result
则又回到了1.6的authenticate
方法。
如果发生 AccountStatusException
或 InternalAuthenticationServiceException
异常,则会通过Spring事件发布器AuthenticationEventPublisher
发布异常事件。
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
......
private void prepareException(AuthenticationException ex, Authentication auth) {
eventPublisher.publishAuthenticationFailure(ex, auth);
}
如果异常为其它类型的 AuthenticationException
,则将此异常设置为lastException
并返回。
catch (AuthenticationException e) {
lastException = e;
}
如果认证结果为 null,且存在父 AuthenticationManager
,则调用父 AuthenticationManager
进行同样的身份认证操作,其处理逻辑基本同上。
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
如果认证结果不为 null,同时,此时的 eraseCredentialsAfterAuthentication
参数为 true,且此时认证后的Authentication
实现了 CredentialsContainer
接口,那么即调用 CredentialsContainer
接口的凭据擦除方法,即eraseCredentials
,擦除相关凭据信息。
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
其中,有一个防止重复发布 AuthenticationSuccessEvent
事件的处理,即 parentResult
为空。如果 parentResult
为 null,则代表父 AuthenticationManager
不存在或者没有身份认证成功,也即没有发布过 AuthenticationSuccessEvent
事件。此时,便由此处发布 AuthenticationSuccessEvent
事件。
如果lastException
为 null,则代表当前的 Authentication
并没有对应支持的 Provider
。此时,便会抛出相应异常。
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
如同防止重复发布 AuthenticationSuccessEvent
事件的处理一样,也有一个防止 AbstractAuthenticationFailureEvent
事件重复发布的逻辑处理。如果 parentException
为 null,则代表父AuthenticationManager
不存在、没有进行身份认证或者发布过 AbstractAuthenticationFailureEvent
事件,此时,便由此处发布 AbstractAuthenticationFailureEvent
事件。
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
最后,抛出 lastException。
1.8 认证成功onAuthenticationSuccess
认证成功,执行代码YouxinAuthenticationSuccessEventHandler
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
// 写入登录成功的日志
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();
if (MapUtil.isNotEmpty(map)) {
sendSuccessEventLog(request, accessTokenAuthentication, map);
}
// 清除账号历史锁定次数
clearLoginFailureTimes(map);
// 输出token
sendAccessTokenResponse(response, authentication);
}
......
private void sendAccessTokenResponse(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);
// 无状态 注意删除 context 上下文的信息
SecurityContextHolder.clearContext();
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}
然后继续向下执行
正式接收登录请求
2. OAuth2TokenEndpointFilter
OAuth2TokenEndpointFilter
会接收通过上文 OAuth2ClientAuthenticationFilter
客户端认证的请求
2.1组装认证对象
AuthenticationConverter
会根据请求中的参数和授权类型组装成对应的授权认证对象,此处同客户端认证中的1.3,最终指向了OAuth2ResourceOwnerBaseAuthenticationConverter
数据校验无误后返回一个OAuth2ResourceOwnerPasswordAuthenticationToken
对象。
2.2认证管理器进行认证
逻辑同客户端认证中的1.6
此时请求来到了OAuth2ResourceOwnerBaseAuthenticationProvider
的authenticate
方法。
2.2.1 构建UsernamePasswordAuthenticationToken
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);
return new UsernamePasswordAuthenticationToken(username, password);
}
类的关系图
2.2.2 交由spring security认证
认证逻辑同客户端认证的1.6,此处调用的是AbstractUserDetailsAuthenticationProvider
然后对header进行校验,提取token,校验token并返回UsernamePasswordAuthenticationToken
查询用户,通过数据库查询并返回UserDetails
2.2.3 认证成功后调用generatAuthenticationToken
生成新的令牌
@NotNull
private OAuth2AccessTokenAuthenticationToken generatAuthenticationToken(T resouceOwnerBaseAuthentication,
OAuth2ClientAuthenticationToken clientPrincipal, RegisteredClient registeredClient,
Set<String> authorizedScopes, Authentication usernamePasswordAuthentication) {
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
.authorizationGrant(resouceOwnerBaseAuthentication);
// @formatter:on
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
// 0.4.0 新增的方法
.authorizedScopes(authorizedScopes);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
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 -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
if (this.refreshTokenGenerator != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
}
else {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
}
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,
Objects.requireNonNull(authorization.getAccessToken().getClaims()));
}
认证成功,调用认证成功方法,并输出token