SpringOauth2.0源码分析之认证流程分析(一)
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的生成存储等。
阅读更多- SpringOauth2.0源码分析之客户端认证(三)
- Shiro源码分析-----认证流程/授权流程----------Subject
- Spring Core Container 源码分析三:Spring Beans 初始化流程分析
- Spring 源码分析《Bean的获取与创建流程》
- Spring源码分析: SpringMVC启动流程与DispatcherServlet请求处理流程
- spring源码之旅(2)_applicationcontext启动流程分析
- Spring源码分析【5】-Spring MVC处理流程
- SpringOauth2.0源码分析之Token持久化(五)
- Spring Boot源码分析之启动流程
- Spring源码分析:Bean加载流程概览及配置文件读取
- SpringOauth2.0源码分析之ProviderManager(二)
- 【Spring源码分析】Bean加载流程概览(转)
- Django rest framework 的认证流程(源码分析)
- 源码分析shiro认证授权流程
- SpringMVC DispatcherServlet执行流程及源码分析
- Shiro源码分析-----认证流程/授权流程----------Subject
- shiro认证授权流程源码分析
- Spring源码分析:Bean加载流程概览及配置文件读取
- Spring MVC @ModelAttribute 源码分析流程