OAuth2 源码分析(三.密码模式源码)

上章介绍了授权码模式,现在再来介绍密码模式,简单的如同砍瓜切菜。

所谓密码模式,即用户提供username,password,clientId,clientSecret,grantType=password等信息,请求/oauth/token,获得access_token,用户即可通过access_token访问资源。

还是以oauth2-demo-master项目为例,只用添加client的认证方法password。

OAuth2 源码分析(三.密码模式源码)_第1张图片

启动QQ项目,程序自动会启动几个endpoints,如/oauth/token,/oauth/authorize等。

1. 访问/oauth/token

此时FilterChainProxy的filter顺序如下。重要的Filter有ClientCredentialsTokenEndpointFilterBasicAuthenticationFilter,前者从request parameters中抽取client信息,后者从header Authorization Basic XXXX中抽取client信息。

                                              OAuth2 源码分析(三.密码模式源码)_第2张图片

1.1 parameters

用postman发送请求,所有参数写在parameters中,返回access_token。

localhost:8080/oauth/token?username=250577914&password=123456&grant_type=password&client_id=aiqiyi&client_secret=secret

OAuth2 源码分析(三.密码模式源码)_第3张图片

ClientCredentialsTokenEndpointFilter会从parameter中抽取client_id,client_secret信息,并进行client的身份验证。

    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {

		if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
		}

		String clientId = request.getParameter("client_id");
		String clientSecret = request.getParameter("client_secret");

		// If the request is already authenticated we can assume that this
		// filter is not needed
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null && authentication.isAuthenticated()) {
			return authentication;
		}

		if (clientId == null) {
			throw new BadCredentialsException("No client credentials presented");
		}

		if (clientSecret == null) {
			clientSecret = "";
		}

		clientId = clientId.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
				clientSecret);

		return this.getAuthenticationManager().authenticate(authRequest);

	}

1.2 Basic Auth

Postman发送请求如下,Basic Auth中填写client_id和client_secret信息。点击update requests后,postman会将client信息用base64加密,写在header——Authorization中。

OAuth2 源码分析(三.密码模式源码)_第4张图片

OAuth2 源码分析(三.密码模式源码)_第5张图片

这样一来,ClientCredentialsTokenEndpointFilter会由于参数中没有client_id自动跳过。BasicAuthenticationFilter会获取header中的Authorization Basic,提取出客户端信息。

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
					throws IOException, ServletException {
		final boolean debug = this.logger.isDebugEnabled();

		String header = request.getHeader("Authorization");

		if (header == null || !header.startsWith("Basic ")) {
			chain.doFilter(request, response);
			return;
		}

		try {
                     // Base64 反解码
			String[] tokens = extractAndDecodeHeader(header, request);
			assert tokens.length == 2;

			String username = tokens[0];


			if (authenticationIsRequired(username)) {
				UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
						username, tokens[1]);
				authRequest.setDetails(
						this.authenticationDetailsSource.buildDetails(request));
				Authentication authResult = this.authenticationManager
						.authenticate(authRequest);

		

				SecurityContextHolder.getContext().setAuthentication(authResult);

				this.rememberMeServices.loginSuccess(request, response, authResult);

				onSuccessfulAuthentication(request, response, authResult);
			}

		}
		catch (AuthenticationException failed) {
			 ...
		}

		chain.doFilter(request, response);
	}

无论是哪一种方式访问/oauth/token,都事先验证了client信息,并作为authentication存储在SecurityContextHolder中。传递到TokenEndPoint的principal是client,paramters包含了user的信息和grantType。

OAuth2 源码分析(三.密码模式源码)_第6张图片

2. 携带Access_token访问api

2.1 access_token放在parameter中

OAuth2AuthentiactionProcessingFilter,从request中提取access_token,构建PreAuthenticatedAuthenticationToken并验证。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
			ServletException {

		final boolean debug = logger.isDebugEnabled();
		final HttpServletRequest request = (HttpServletRequest) req;
		final HttpServletResponse response = (HttpServletResponse) res;

		try {
                    // 从request中提取access_token
			Authentication authentication = tokenExtractor.extract(request);
			
			if (authentication == null) {
				if (stateless && isAuthenticated()) {
					SecurityContextHolder.clearContext();
				}
				
			}
			else {
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
				if (authentication instanceof AbstractAuthenticationToken) {
					AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
					needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
				}

                     // OAuth2AuthenticationManager 验证PreAuthenticatedAuthenticationToken
				Authentication authResult = authenticationManager.authenticate(authentication);

				
				eventPublisher.publishAuthenticationSuccess(authResult);
				SecurityContextHolder.getContext().setAuthentication(authResult);

			}
		}
		catch (OAuth2Exception failed) {
			SecurityContextHolder.clearContext();

			eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
					new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

			authenticationEntryPoint.commence(request, response,
					new InsufficientAuthenticationException(failed.getMessage(), failed));

			return;
		}

		chain.doFilter(request, response);
	}

tokenExtractor.extract(request)实际调用的是BearTokenExtractor里的extract方法,从Authorization header   “Bearer  xxxx”中抽取token,或者从request parameters抽取名为“access_token”的参数值。

@Override
	public Authentication extract(HttpServletRequest request) {
		String tokenValue = extractToken(request);
		if (tokenValue != null) {
                          // 记为PreAuthenticatedAuthenticationToken
			PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
			return authentication;
		}
		return null;
	}

 构建后的authentication参数如下,tokenType="Bearer",principal=token值。再根据OAuth2AuthenticationManager验证该authentication的合法性。

OAuth2 源码分析(三.密码模式源码)_第7张图片

2.2 access_token存于header中

postman发送如下请求,将access_token写在Authorization的header里,前缀是Bearer。

OAuth2 源码分析(三.密码模式源码)_第8张图片

同样是OAuth2AuthenticationProcessingFilter拦截,从request header中提取token,记为PreAuthenticatedAuthenticationToken,用OAuth2AuthenticationManager进行验证。

3. 访问/oauth/check_token

当oauthserver和resourceserver不在一个应用程序时,访问resource,会自动转交到oauthserver的/oauth/check_token,获得access_token的验证结果。

3.1 配置ResouceServer

配置application.yml

server:
  port: 8081
security:
  oauth2:
    client:
      client-id: aiqiyi
      client-secret: secret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
      user-logout-uri: http://localhost:8080/oauth/logout
    resource:
      id: qq
      token-info-uri: http://localhost:8080/oauth/check_token
      prefer-token-info: true
      filter-order: 3
  basic:
    enabled: false

配置ResouceServerConfigurerAdapter

@Configuration
@EnableResourceServer
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

	@Value("${security.oauth2.resource.id}")
	public String RESOURCE_ID;



	@Autowired
    public RemoteTokenServices remoteTokenServices;
	
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 存入resource_id,到时候与oauthserver中client对应的resourceid进行比对
		resources.resourceId(RESOURCE_ID).stateless(true);
		resources.tokenServices(remoteTokenServices);
	}
}

 

 

 

你可能感兴趣的:(spring,security)