1. 程式人生 > >spring security oauth2 自動重新整理續簽token (refresh token)

spring security oauth2 自動重新整理續簽token (refresh token)

1.引言

本篇文章適合瞭解過oauth2,有用過spring security oauth2的讀者並且瞭解過濾器鏈路的人。本篇文章的思路是首先獲取資源伺服器的某一個資源,當認證失敗返回401的時候我們通過異常處理器的嘗試通過refresh token 進行token的重新整理,如果重新整理成功則通過請求轉發的方式沿路訪問資源伺服器的某一個資源。如果重新整理失敗那麼返回401,並且結果通知客戶端。如果是web端可以直接跳轉到登陸頁面,如果是app端則返回錯誤資訊。

2.首先從原始碼分析核心過濾器OAuth2AuthenticationProcessingFilter

下面是此過濾器的過濾方法,可以知道當授權失敗丟擲異常的時候將會被catch,並且通過authenticationEntryPoint.commence()呼叫端點異常處理器,這個處理器將是我們重寫的關鍵

	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 {

			Authentication authentication = tokenExtractor.extract(request);
			
            ...
			
		catch (OAuth2Exception failed) {
			SecurityContextHolder.clearContext();

			if (debug) {
				logger.debug("Authentication request failed: " + failed);
			}
			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);
	}

3.重寫端點異常處理器

重寫端點異常處理器只需要繼承OAuth2AuthenticationEntryPoint即可,通過原始碼我們也可以知道過濾器預設呼叫的是OAuth2AuthenticationEntryPoint處理器,在這裡不展示了。接著我們只需要對關鍵的方法commence進行重寫即可,大概思路就是首先判斷異常返回的狀態嗎是不是401,如果不是則呼叫父類的預設方法commence處理其他非401的異常,如果是401異常,那麼我們通過RestTemplate發起重新整理Token的請求,如果重新整理成功則通過request.getRequestDispatcher().forward()轉發到原來的資源伺服器的資源上進行資源訪問並且將獲取的token存入cookie,如果重新整理失敗則返回錯誤資訊(web的話也可以通過response.sendirect跳轉到登陸頁面)

public class LLGAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Autowired
    private OAuth2ClientProperties oAuth2ClientProperties;
    @Autowired
    private BaseOAuth2ProtectedResourceDetails baseOAuth2ProtectedResourceDetails;
    private WebResponseExceptionTranslator<?> exceptionTranslator = new DefaultWebResponseExceptionTranslator();
    @Autowired
    RestTemplate restTemplate;
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        try {
            //解析異常,如果是401則處理
            ResponseEntity<?> result = exceptionTranslator.translate(authException);
            if (result.getStatusCode() == HttpStatus.UNAUTHORIZED) {
                MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
                formData.add("client_id", oAuth2ClientProperties.getClientId());
                formData.add("client_secret", oAuth2ClientProperties.getClientSecret());
                formData.add("grant_type", "refresh_token");
                Cookie[] cookie=request.getCookies();
                for(Cookie coo:cookie){
                    if(coo.getName().equals("refresh_token")){
                        formData.add("refresh_token", coo.getValue());
                    }
                }
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
                Map map = restTemplate.exchange(baseOAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST,
                            new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
                //如果重新整理異常,則坐進一步處理
                if(map.get("error")!=null){
                    // 返回指定格式的錯誤資訊
                    response.setStatus(401);
                    response.setHeader("Content-Type", "application/json;charset=utf-8");
                    response.getWriter().print("{\"code\":1,\"message\":\""+map.get("error_description")+"\"}");
                    response.getWriter().flush();
                    //如果是網頁,跳轉到登陸頁面
                    //response.sendRedirect("login");
                }else{
                    //如果重新整理成功則儲存cookie並且跳轉到原來需要訪問的頁面
                    for(Object key:map.keySet()){
                        response.addCookie(new Cookie(key.toString(),map.get(key).toString()));
                    }
                    request.getRequestDispatcher(request.getRequestURI()).forward(request,response);
                }
            }else{
                //如果不是401異常,則以預設的方法繼續處理其他異常
                super.commence(request,response,authException);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

4.將處理器配置進過濾器上

由於spring security遵循介面卡的設計模式,所以我們可以直接從配置類上配置此處理器

@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public abstract class ResServerConfig extends ResourceServerConfigurerAdapter {

    ...

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
       
        resources.authenticationEntryPoint(new LLGAuthenticationEntryPoint());
      
    }

5.實戰

5.1向授權伺服器獲取token

首先編寫登陸控制器,通過restTemplate向授權伺服器獲取token並且存入cookie

PostMapping(value = "/login")
    public ResponseEntity<OAuth2AccessToken> login(@RequestBody @Valid LoginDTO loginDTO, BindingResult bindingResult, HttpServletResponse response) throws Exception {
        if (bindingResult.hasErrors()) {
            throw new Exception("登入資訊格式錯誤");
        } else {
            //Http Basic 驗證
            String clientAndSecret = oAuth2ClientProperties.getClientId() + ":" + oAuth2ClientProperties.getClientSecret();
            //這裡需要注意為 Basic 而非 Bearer
            clientAndSecret = "Basic " + Base64.getEncoder().encodeToString(clientAndSecret.getBytes());
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.set("Authorization", clientAndSecret);
            //授權請求資訊
            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.put("username", Collections.singletonList(loginDTO.getUsername()));
            map.put("password", Collections.singletonList(loginDTO.getPassword()));
            map.put("grant_type", Collections.singletonList(oAuth2ProtectedResourceDetails.getGrantType()));
            map.put("scope", oAuth2ProtectedResourceDetails.getScope());
            //HttpEntity
            HttpEntity httpEntity = new HttpEntity(map, httpHeaders);
            //獲取 Token
            ResponseEntity<OAuth2AccessToken> body = restTemplate.exchange(oAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST, httpEntity, OAuth2AccessToken.class);
            OAuth2AccessToken oAuth2AccessToken = body.getBody();
            response.addCookie(new Cookie("access_token", oAuth2AccessToken.getValue()));
            response.addCookie(new Cookie("refresh_token", oAuth2AccessToken.getRefreshToken().getValue()));
            return body;
        }
    }

之後通過idea的Http client 工具模擬請求獲取token

  • 獲取access_token請求(/oauth/token)  請求所需引數:client_id、client_secret、grant_type、username、password

5.2用失效token訪問資源伺服器

使用失效的token訪問資源的時候,可以發現端點直接到達異常處理器,可以看出token確實是失效的並且進入了處理器進行處理,並且最終通過refresh_token獲取到最新的token再次訪問獲取到資源

  • 重新整理token請求(/oauth/token)  請求所需引數:grant_type、refresh_token、client_id、client_secret  其中grant_type為固定值:grant_type=refresh_token

6.總結

本次由於對spring security oauth2瞭解不深入,導致在尋找異常丟擲解決方法的時候折騰了一下,整體的思路並不複雜,只是用到了最普通的請求轉發,但是需要對過濾器鏈有一定了解,打斷點慢慢看是不錯的選擇。