您的位置:首页 > 编程语言 > Java开发

SpringOauth2.0源码分析之认证流程分析(一)

2018-10-23 12:42 531 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014730165/article/details/83305106

1.概述

本专题的源码分析均以用户名密码认证模式进行叙述。在此先了解下用户名密码认证的协议流程说明:


(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
(B)步骤:客户端发出https请求。

从用户名密码认证方式可以看出,在获取access_token的过程中,将用户名和密码完全交给客户端,然后客户端向服务器认证,这种方式是对客户端充分的信任的情况下才能使用。用户名密码认证,是将code码认证中的资源拥有者确认授权的过程进行了整合。减少了授权的操作步骤,当然也带来了一定的不安全性。

2.代码实现

用户名密码认证的实现方式,请参考: SpringOauth2.0 用户名密码验证方式(二)

3.流程说明:

3.1 客户端请求,采用PostMan的方式
3.1.1 请求参数:

首先需要添加客户端的基础认证方式,这里Type选择:Base Auth ,用户名和密码为客户端用户名和密码。

在请求参数中,需要输入,认证类型:password,scope类型:all,用户的用户名和密码

3.2 SpringOauth2.0 过滤链

当请求:http://localhost:8080/oauth/token 接口,并不会直接请求到这个接口。而是先经过SpringSecurity的一系列的过滤器,其中具体使用哪个过滤器,通过FilterChainProxy类进行控制。SpringSecurity的过滤器链默认是12个,如下:

可以发现所有的过滤器都被存放到一个additionalFilters的List集合中,默认的大小是12个。其中绝大部分都是SpringSecurity自身带的过滤器。使用哪个过滤器是通过FilterChainProxy进行代理实现:核心代码如下:

@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
// currentPosition 当前过滤器的偏移量
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
// 每次调用过滤器,都会自增偏移量
currentPosition++;
// 获取下一个过滤链
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
}

从FilterChainProxy的源码可以看出,其内部维护了一个偏移量:private int currentPosition = 0 记录当前过滤链的位置,获取下一个过滤链,通过 Filter nextFilter = additionalFilters.get(currentPosition - 1) 获取。过滤器的顺序,就是上面所说的additionalFilters 集合中的顺序。过滤器的实现细节分析,后续文章会详细说明。

3.3 SpringOauth2.0 客户端认证

在用户名密码模式认证中,首先需要进行客户端认证。客户端的认证逻辑是通过:BasicAuthenticationFilter 进行具体的客户端认证:

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
// 获取客户端请求的:Authorization basic 中的参数。客户端用户名和密码,进行解码
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 authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
// 通过认证管理器,进行客户端用户名密码认证
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
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);
}

BasicAuthenticationFilter 通过解析前端传递过来的客户端的用户名和密码,进行反序列化,获取明文。然后通过AuthenticationManager进行认证。这里的认证其实就是查询当前系统中,是否存在当前客户端。实现原理后期分析。认证通过以后,会把当前信息写入到SecurityContextHolder.getContext().setAuthentication()中。进行保存。

3.3 获取 access_token

经过前面所有的过滤器以后,才真正的请求到客户端发起的请求地址:/oauth/token。这个请求地址,是SpringOauth2.0给封装好的。具体的实现代码在类:TokenEndpoint 中。具体核心代码如下:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 判断当前客户端是否认证成功
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
// 封装TokenRequest对象
TokenRequest 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)));
}
// 获取授权Token。封装在OAuth2AccessToken 中
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}

首先验证当前的client是否已经认证成功,如果认证失败的话,直接抛出异常。认证通过,将当前的客户端信息和参数信息封装成TokenRequest对象。根据TokenRequest对象,调用授权方法,最终生成OAuth2AccessToken对象。其中封装了当前用户的权限认证和access_token信息。

4.结语

至此,SpringOauth2.0的用户名密码认证模式的大致流程已经走完。通过过滤器:BasicAuthenticationFilter,验证客户端用户名密码。过滤器链验证通过之后,会请求到真正的接口:/oauth/token。在这个接口里面,主要实现用户名密码认证,以及access_token的生成存储等。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: