OAuth2 源码分析(二.授权码模式源码)

上一章介绍了与OAuth2相关的核心类,让我们再复习一遍,如果有遗忘的地方请移步到上一章查看。

  • 四大角色:ResouceServer   AuthorizationServer    client     user
  • OAuth2AccessToken  OAuth2Authentiaction
  • OAuth2Request    TokenRequest   AuthorizationRequest
  • TokenGranter   TokenStore   TokenExtractor   DefaultTokenServices
  • ResourceServerConfigurerAdapter      AuthorizationServerConfigurerAdapter
  • TokenEndPoint(/oauth/token)    AuthorizationEndPoint(/oauth/authorize)

上面介绍的全是乐高积木的小部件,如何把积木拼起来才是关键。说到oauth2不可避免的就要聊到5种授权模式。

(1)授权码模式(Authorization Code) 
(2)授权码简化模式(Implicit) 
(3)Pwd模式(Resource Owner Password Credentials) 
(4)Client模式(Client Credentials) 
(5)扩展模式(Extension)

无论哪一种,核心思想都是client向authorization server发出请求,请求参数有client_id,client_secret,scope,response_type或response_uri等,authorization server经过验证后,返回client一个access_token。凭借这个access_token,client再去resource server中获取资源。这章只介绍授权码模式的流程

1.授权码模式流程图

è¿éåå¾çæè¿°

没了解原理前是看着挺费劲的,一般的验证只需要client发出一次请求就能获得access_token,但授权码模式多了一个Authorization code。这样就分两步走,第一步请求/oauth/authorize获得Authorization code,第二步请求/oauth/token获得access_token。

这里就以登录csdn为例来解释授权码模式。

1.打开csdn登录页面,选择QQ登录。此时client为csdn,qq为authroization server和resouce server。qq授权服务器里存储了很多client信息,csdn只是众多client中的一个。

                                                                   

2.页面跳转至QQ授权页面,url地址是

https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=100270989&redirect_uri=https%3A%2F%2Fpassport.csdn.net%2Faccount%2Flogin%3Foauth_provider%3DQQProvider&state=test

    里面有3个关键的参数,response_type,client_id以及redirect_uri。点击了下图中授权并登录的按钮后,页面自动跳转到了csdn主页,且获取到了QQ相关信息。看似很简单的跳转其实包含了很多步骤。

                           

                                                                      

3.用户填写用户名、密码后,点击授权并登录,首先访问qq授权服务器的/login路径,spring security验证username和password后给用户发放JSessionId的cookie,session中存储了Authentication。

4.再访问qq授权服务器/oauth/authorize,请求参数有response_type,redirect_uri,client_id,验证通过后请求重定向到redirect_uri,且传递Authorization code。

5.redirect_uri路径指向的是client中的一个endpoint,client接收到了code,表明client信息已经在QQ授权服务器验证成功。再凭借这个code值外加client_id,client_secret,grant_type=authorization_code,code,redirect_uri等参数,去访问QQ的/oauth/token,返回access_token。

6.获得access_token后,client再去找qq的资源服务器要资源。

     一句话概括,就是按顺序依次获得authentication ---> Authorization code  ----> access_token。

2. 源码分析

为了方便理解,这里先给出来自github lexburner的例子,项目地址是https://github.com/lexburner/oauth2-demo

项目内有Aiqiyi和qq两个服务,分别是client和authorization server,操作说明详见readme.md,这里不做赘述。

                                                       

2.1 第一次请求/oauth/authorize

请求的完整url如下,有参数client_id,response_type,redirect_uri。

http://localhost:8080/oauth/authorize?client_id=aiqiyi&response_type=code&redirect_uri=http://localhost:8081/aiqiyi/qq/redirect

访问AuthorizationEndPoint中的/oauth/authorize,里面会判断client信息和用户信息,如果user没有Authentication,则会报错,跳转到ExceptionTranslationFilter类中,请求转发到/login路径,并将现请求路径存储到session的saverequest中。

@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
			SessionStatus sessionStatus, Principal principal) {

		AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

		Set<String> responseTypes = authorizationRequest.getResponseTypes();

		if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
			throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
		}
                // 判断clientId是否为空
		if (authorizationRequest.getClientId() == null) {
			throw new InvalidClientException("A client id must be provided");
		}

		try {
                     // 判断user是否认证,认证失败则跳转到/login路径
			if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
				throw new InsufficientAuthenticationException(
						"User must be authenticated with Spring Security before authorization can be completed.");
			}

                // 验证client信息
			ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

           ...
    }
}

2.2 ExceptionTranslationFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
                 // 进入到此方法中
				handleSpringSecurityException(request, response, chain, ase);
			}
			...
		}
	}



private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			logger.debug(
					"Authentication exception occurred; redirecting to authentication entry point",
					exception);
             // 没有验证身份信息,跳转到/login界面
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
        ...
}


protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
        
		SecurityContextHolder.getContext().setAuthentication(null);
                // 将saveRequest存到session中,方便身份验证成功后调用
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
                // 请求重定向到/login
		authenticationEntryPoint.commence(request, response, reason);
}

2.3 requestCache.saveRequest(request,response)

requestCache的常用的实现类是HttpSessionRequestCache,一般是访问url时系统判断用户未获得授权,ExceptionTranslationFilter会存储savedRequest到session中,名为“SPRING_SECURITY_SAVED_REQUEST”。

SavedRequest里面包含原先访问的url地址、cookie、header、parameter等信息,一旦Authentication认证成功,successHandler.onAuthenticationSuccess(SavedRequestAwareAuthenticationSuccessHandler)会从session中抽取savedRequest,继续访问原先的url。

public class HttpSessionRequestCache implements RequestCache {
	static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST";  
  /**
	 * HttpSessionRequestCache Stores the current request, provided the configuration properties allow it.
	 */
	public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
		if (requestMatcher.matches(request)) {
			DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
					portResolver);

			if (createSessionAllowed || request.getSession(false) != null) {
				// Store the HTTP request itself. Used by
				// AbstractAuthenticationProcessingFilter
				// for redirection after successful authentication (SEC-29)
				request.getSession().setAttribute(this.sessionAttrName, savedRequest);
				logger.debug("DefaultSavedRequest added to Session: " + savedRequest);
			}
		}
		else {
			logger.debug("Request not saved as configured RequestMatcher did not match");
		}
	}
}

2.4 重定向到/login

由于是第一次访问qq认证服务器,所以需要用户登录校验身份。在WebSecurityConfigurerAdapter的继承类中,找到存储在缓存中的用户名密码,填写完毕。

                                    

点击“Sign In”按钮后,post请求/login路径,按照FilterChainProxy的filter链运行到UsernamePasswordAuthenticationFilter,验证通过后执行successHandler.onAuthenticationSuccess(request, response, authResult),获取session中的savedrequest,重定向到原先的地址/oauth/authorize,并附带完整请求参数。

public class SavedRequestAwareAuthenticationSuccessHandler extends
		SimpleUrlAuthenticationSuccessHandler {
	protected final Log logger = LogFactory.getLog(this.getClass());

	private RequestCache requestCache = new HttpSessionRequestCache();

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
             // HttpSessionRequestCache.getRequest ,找名为SPRING_SECURITY_SAVED_REQUEST的session
		SavedRequest savedRequest = requestCache.getRequest(request, response);

		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}

		clearAuthenticationAttributes(request);

		// Use the DefaultSavedRequest URL
           // 获得原先存储在SavedRequest中的redirectUrl,即/oauth/authorize
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}

	public void setRequestCache(RequestCache requestCache) {
		this.requestCache = requestCache;
	}
}

2.5 第二次请求/oauth/authorize

这次请求就硬气多了,请求中携带了Authentication的session,系统验证通过,生成授权码,存储在InMemoryAuthorizationCodeServices中的concurrenthashmap中,且返回给请求参数中的redirect_uri,即http://localhost:8081/aiqiyi/qq/redirect。

http://localhost:8080/oauth/authorize?client_id=aiqiyi&response_type=code&redirect_uri=http://localhost:8081/aiqiyi/qq/redirect
@RequestMapping(value = "/oauth/authorize")
	public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
			SessionStatus sessionStatus, Principal principal) {
                ...
            if (authorizationRequest.isApproved()) {
				if (responseTypes.contains("token")) {
					return getImplicitGrantResponse(authorizationRequest);
				}
				if (responseTypes.contains("code")) {
                    // 返回code给redirect_uri
					return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
							(Authentication) principal));
				}
			}
                ...
}
public class InMemoryAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {

	protected final ConcurrentHashMap<String, OAuth2Authentication> authorizationCodeStore = new ConcurrentHashMap<String, OAuth2Authentication>();

	@Override
	protected void store(String code, OAuth2Authentication authentication) {
		this.authorizationCodeStore.put(code, authentication);
	}

	@Override
	public OAuth2Authentication remove(String code) {
		OAuth2Authentication auth = this.authorizationCodeStore.remove(code);
		return auth;
	}

}

到了这里我们总结下刚才都发生了什么。首先aiqiyi向qq发出/oauth/authorize的请求,qq服务器的AuthorizationEndPoint判断用户是否登录,如果没有登录则先跳转到/login界面,同时存储首次request的信息,保存在session中。用户登录并授权后,程序自动获取刚存储在session中的savedrequest,再次访问/oauth/authorize。验证client信息和user信息成功后,重定向到redirect_uri,并传参数code。

aiqiyi接到code后,再附带client_id,client_secret,grant_type,redirect_uri等信息post请求/oauth/token,从而获得access_token。

     

2.6 client接收code并向oauth server请求/oauth/token

以下代码自行写在client的controller里,用于接收qq服务端传递来的code,并请求/oauth/token。

@RequestMapping("/aiqiyi/qq/redirect")
    public String getToken(@RequestParam String code){
        log.info("receive code {}",code);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> params= new LinkedMultiValueMap<>();
        params.add("grant_type","authorization_code");
        params.add("code",code);
        params.add("client_id","aiqiyi");
        params.add("client_secret","secret");
        params.add("redirect_uri","http://localhost:8081/aiqiyi/qq/redirect");
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
        ResponseEntity<String> response = restTemplate.postForEntity("http://localhost:8080/oauth/token", requestEntity, String.class);
        String token = response.getBody();
        log.info("token => {}",token);
        return token;
    }

2.7 TokenEndPoint生成access_token

具体代码在上一章已介绍,这里不做详述。注意的是会从InMemoryAuthorizationCodeServices中提取hashmap验证code是否正确。

3 总结

github地址:https://github.com/lexburner/oauth2-demo

猜你喜欢

转载自blog.csdn.net/qq_30905661/article/details/82424552