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

https://blog.csdn.net/qq_30905661/article/details/82424552

上一章介绍了与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或redirect_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中。


 
 
  1. @RequestMapping(value = "/oauth/authorize")
  2. public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
  3. SessionStatus sessionStatus, Principal principal) {
  4. AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
  5. Set<String> responseTypes = authorizationRequest.getResponseTypes();
  6. if (!responseTypes.contains( "token") && !responseTypes.contains( "code")) {
  7. throw new UnsupportedResponseTypeException( "Unsupported response types: " + responseTypes);
  8. }
  9. // 判断clientId是否为空
  10. if (authorizationRequest.getClientId() == null) {
  11. throw new InvalidClientException( "A client id must be provided");
  12. }
  13. try {
  14. // 判断user是否认证,认证失败则跳转到/login路径
  15. if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
  16. throw new InsufficientAuthenticationException(
  17. "User must be authenticated with Spring Security before authorization can be completed.");
  18. }
  19. // 验证client信息
  20. ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
  21. ...
  22. }
  23. }

2.2 ExceptionTranslationFilter


 
 
  1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  2. throws IOException, ServletException {
  3. HttpServletRequest request = (HttpServletRequest) req;
  4. HttpServletResponse response = (HttpServletResponse) res;
  5. try {
  6. chain.doFilter(request, response);
  7. logger.debug( "Chain processed normally");
  8. }
  9. catch (IOException ex) {
  10. throw ex;
  11. }
  12. catch (Exception ex) {
  13. // Try to extract a SpringSecurityException from the stacktrace
  14. Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
  15. RuntimeException ase = (AuthenticationException) throwableAnalyzer
  16. .getFirstThrowableOfType(AuthenticationException.class, causeChain);
  17. if (ase == null) {
  18. ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
  19. AccessDeniedException.class, causeChain);
  20. }
  21. if (ase != null) {
  22. // 进入到此方法中
  23. handleSpringSecurityException(request, response, chain, ase);
  24. }
  25. ...
  26. }
  27. }
  28. private void handleSpringSecurityException(HttpServletRequest request,
  29. HttpServletResponse response, FilterChain chain, RuntimeException exception)
  30. throws IOException, ServletException {
  31. if (exception instanceof AuthenticationException) {
  32. logger.debug(
  33. "Authentication exception occurred; redirecting to authentication entry point",
  34. exception);
  35. // 没有验证身份信息,跳转到/login界面
  36. sendStartAuthentication(request, response, chain,
  37. (AuthenticationException) exception);
  38. }
  39. ...
  40. }
  41. protected void sendStartAuthentication(HttpServletRequest request,
  42. HttpServletResponse response, FilterChain chain,
  43. AuthenticationException reason) throws ServletException, IOException {
  44. SecurityContextHolder.getContext().setAuthentication( null);
  45. // 将saveRequest存到session中,方便身份验证成功后调用
  46. requestCache.saveRequest(request, response);
  47. logger.debug( "Calling Authentication entry point.");
  48. // 请求重定向到/login
  49. authenticationEntryPoint.commence(request, response, reason);
  50. }

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。


 
 
  1. public class HttpSessionRequestCache implements RequestCache {
  2. static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST";
  3. /**
  4. * HttpSessionRequestCache Stores the current request, provided the configuration properties allow it.
  5. */
  6. public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
  7. if (requestMatcher.matches(request)) {
  8. DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
  9. portResolver);
  10. if (createSessionAllowed || request.getSession( false) != null) {
  11. // Store the HTTP request itself. Used by
  12. // AbstractAuthenticationProcessingFilter
  13. // for redirection after successful authentication (SEC-29)
  14. request.getSession().setAttribute( this.sessionAttrName, savedRequest);
  15. logger.debug( "DefaultSavedRequest added to Session: " + savedRequest);
  16. }
  17. }
  18. else {
  19. logger.debug( "Request not saved as configured RequestMatcher did not match");
  20. }
  21. }
  22. }

2.4 重定向到/login

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

                                    

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


 
 
  1. public class SavedRequestAwareAuthenticationSuccessHandler extends
  2. SimpleUrlAuthenticationSuccessHandler {
  3. protected final Log logger = LogFactory.getLog( this.getClass());
  4. private RequestCache requestCache = new HttpSessionRequestCache();
  5. @Override
  6. public void onAuthenticationSuccess(HttpServletRequest request,
  7. HttpServletResponse response, Authentication authentication)
  8. throws ServletException, IOException {
  9. // HttpSessionRequestCache.getRequest ,找名为SPRING_SECURITY_SAVED_REQUEST的session
  10. SavedRequest savedRequest = requestCache.getRequest(request, response);
  11. if (savedRequest == null) {
  12. super.onAuthenticationSuccess(request, response, authentication);
  13. return;
  14. }
  15. String targetUrlParameter = getTargetUrlParameter();
  16. if (isAlwaysUseDefaultTargetUrl()
  17. || (targetUrlParameter != null && StringUtils.hasText(request
  18. .getParameter(targetUrlParameter)))) {
  19. requestCache.removeRequest(request, response);
  20. super.onAuthenticationSuccess(request, response, authentication);
  21. return;
  22. }
  23. clearAuthenticationAttributes(request);
  24. // Use the DefaultSavedRequest URL
  25. // 获得原先存储在SavedRequest中的redirectUrl,即/oauth/authorize
  26. String targetUrl = savedRequest.getRedirectUrl();
  27. logger.debug( "Redirecting to DefaultSavedRequest Url: " + targetUrl);
  28. getRedirectStrategy().sendRedirect(request, response, targetUrl);
  29. }
  30. public void setRequestCache(RequestCache requestCache) {
  31. this.requestCache = requestCache;
  32. }
  33. }

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
 
 

 
 
  1. @RequestMapping(value = "/oauth/authorize")
  2. public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
  3. SessionStatus sessionStatus, Principal principal) {
  4. ...
  5. if (authorizationRequest.isApproved()) {
  6. if (responseTypes.contains( "token")) {
  7. return getImplicitGrantResponse(authorizationRequest);
  8. }
  9. if (responseTypes.contains( "code")) {
  10. // 返回code给redirect_uri
  11. return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
  12. (Authentication) principal));
  13. }
  14. }
  15. ...
  16. }

 
 
  1. public class InMemoryAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {
  2. protected final ConcurrentHashMap<String, OAuth2Authentication> authorizationCodeStore = new ConcurrentHashMap<String, OAuth2Authentication>();
  3. @Override
  4. protected void store(String code, OAuth2Authentication authentication) {
  5. this.authorizationCodeStore.put(code, authentication);
  6. }
  7. @Override
  8. public OAuth2Authentication remove(String code) {
  9. OAuth2Authentication auth = this.authorizationCodeStore.remove(code);
  10. return auth;
  11. }
  12. }

到了这里我们总结下刚才都发生了什么。首先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。


 
 
  1. @RequestMapping( "/aiqiyi/qq/redirect")
  2. public String getToken(@RequestParam String code){
  3. log.info( "receive code {}",code);
  4. HttpHeaders headers = new HttpHeaders();
  5. headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
  6. MultiValueMap<String, String> params= new LinkedMultiValueMap<>();
  7. params.add( "grant_type", "authorization_code");
  8. params.add( "code",code);
  9. params.add( "client_id", "aiqiyi");
  10. params.add( "client_secret", "secret");
  11. params.add( "redirect_uri", "http://localhost:8081/aiqiyi/qq/redirect");
  12. HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
  13. ResponseEntity<String> response = restTemplate.postForEntity( "http://localhost:8080/oauth/token", requestEntity, String.class);
  14. String token = response.getBody();
  15. log.info( "token => {}",token);
  16. return token;
  17. }

2.7 TokenEndPoint生成access_token

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

3 总结

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

 

 

  •                     <li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#csdnc-thumbsup"></use>
                        </svg><span class="name">点赞</span>
                        <span class="count">3</span>
                        </a></li>
                        <li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{&quot;mod&quot;:&quot;popu_824&quot;}"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#icon-csdnc-Collection-G"></use>
                        </svg><span class="name">收藏</span></a></li>
                        <li class="tool-item tool-active is-share"><a href="javascript:;" data-report-click="{&quot;mod&quot;:&quot;1582594662_002&quot;}"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#icon-csdnc-fenxiang"></use>
                        </svg>分享</a></li>
                        <!--打赏开始-->
                                                <!--打赏结束-->
                                                <li class="tool-item tool-more">
                            <a>
                            <svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg>
                            </a>
                            <ul class="more-box">
                                <li class="item"><a class="article-report">文章举报</a></li>
                            </ul>
                        </li>
                                            </ul>
                </div>
                            </div>
            <div class="person-messagebox">
                <div class="left-message"><a href="https://blog.csdn.net/qq_30905661">
                    <img src="https://profile.csdnimg.cn/1/1/D/3_qq_30905661" class="avatar_pic" username="qq_30905661">
                                            <img src="https://g.csdnimg.cn/static/user-reg-year/1x/5.png" class="user-years">
                                    </a></div>
                <div class="middle-message">
                                        <div class="title"><span class="tit"><a href="https://blog.csdn.net/qq_30905661" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}" target="_blank">炸天总指挥</a></span>
                                            </div>
                    <div class="text"><span>发布了30 篇原创文章</span> · <span>获赞 18</span> · <span>访问量 2万+</span></div>
                </div>
                                <div class="right-message">
                                            <a href="https://im.csdn.net/im/main.html?userName=qq_30905661" target="_blank" class="btn btn-sm btn-red-hollow bt-button personal-letter">私信
                        </a>
                                                            <a class="btn btn-sm  bt-button personal-watch" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}">关注</a>
                                    </div>
                            </div>
                    </div>
    </article>
    
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hello_world!

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值