1. introduction
- Prerequisite: Understand the general process of spring security oauth2 (have a certain understanding of the content of the filter)
- Main ideas:
- First, access intercepted resources with expired token
- Call exception handler when authentication failure returns 401
- Token refresh with exception handler and refresh_token
- The successful refresh accesses the intercepted resource again by request. getRequest Dispatcher
2. Source code analysis core filter OAuth 2Authentication Processing Filter
- This filter is closely related to the various operations of our token, and it is not clear that we can refer to others'blogs for understanding. https://blog.csdn.net/u013815546/article/details/77046453
- Following is the filter method for this filter, from which you can know that when an exception is thrown when authorization fails, it will be caught and the endpoint exception handler is called through authenticationEntryPoint.commence(), which is the class we want to override.
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. Analysis of default endpoint exception handlers
- From the filter source code, we can see that the exception handler has a default implementation class.
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean { private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class); private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint(); ... }
By looking at this default processor, we can see that doHandle's method is mainly invoked in it.
public class OAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint { ... public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { doHandle(request, response, authException); } ... }
- Looking at the details of doHandle again, we can see that there are three main functions of this filter:
- Resolving exception types
- Extending some attributes and contents of response
- Response refresh cache returns directly
protected final void doHandle(HttpServletRequest request, HttpServletResponse response, Exception authException) throws IOException, ServletException { try { ResponseEntity<?> result = exceptionTranslator.translate(authException); result = enhanceResponse(result, authException); exceptionRenderer.handleHttpEntityResponse(result, new ServletWebRequest(request, response)); response.flushBuffer(); } ... }
4. Rewrite exception handlers
- By analyzing the default exception handler, we can conclude that if we need an exception (401 exception), we can use our custom method to deal with it, and if other exception, we can let the original exception handler handle it. The general idea is as follows:
- Analyzing exceptions by exceptionTranslator.translate(authException) and judging the status of exceptions
- If it's not a 401 exception, call the default exception handler directly.
- If an exception is 401, a token refresh request is initiated to the authorization server
- If the token refresh is successful, request resources again through request. getRequestDispatcher (request. getRequestURI (). forward (request, response)
- If the token refresh fails, either jump to the login page (or jump to the login page via response.sendirect if the web is available), or return the error message (json).
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 { //Resolve exceptions, if 401, handle them 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 the exception is refreshed, sit back and deal with it further. if(map.get("error")!=null){ // Returns error information in the specified format 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(); //If it's a web page, jump to the landing page //response.sendRedirect("login"); }else{ //If the refresh succeeds, store the cookie and jump to the page you originally wanted to visit for(Object key:map.keySet()){ response.addCookie(new Cookie(key.toString(),map.get(key).toString())); } request.getRequestDispatcher(request.getRequestURI()).forward(request,response); } }else{ //If it is not a 401 exception, continue processing other exceptions by default super.commence(request,response,authException); } } catch (Exception e) { e.printStackTrace(); } } }
5. Set the processor on the filter
- Because spring security follows the adapter design pattern, we can configure the adapter directly from the configuration class.
@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()); }
6. actual combat
6.1 Get token from authorized server
- First, write the login controller, get token from the authorization server through restTemplate, and save the cookie.
PostMapping(value = "/login") public ResponseEntity<OAuth2AccessToken> login(@RequestBody @Valid LoginDTO loginDTO, BindingResult bindingResult, HttpServletResponse response) throws Exception { if (bindingResult.hasErrors()) { throw new Exception("Logon Information Format Error"); } else { //Http Basic Verification String clientAndSecret = oAuth2ClientProperties.getClientId() + ":" + oAuth2ClientProperties.getClientSecret(); //Note here that it's Basic, not Bearer. clientAndSecret = "Basic " + Base64.getEncoder().encodeToString(clientAndSecret.getBytes()); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Authorization", clientAndSecret); //Authorization request information 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); //Get 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; } }
- Then I get token here by using idea's HTTP Client tool to simulate the request
-
Get access_token request (/oauth/token)
Request parameters: client_id, client_secret, grant_type, username, password
6.2 Analog Failure token Access Resource Server
- When using invalid token to access resources, we can find that the breakpoint reaches the exception handler directly, which shows that token is invalid and has entered the exception handler for processing. Finally, we can get the latest token by refresh_token to access the resources successfully again.
-
Refresh token request (/oauth/token)
Request parameters: grant_type, refresh_token, client_id, client_secret
Where grant_type is a fixed value: grant_type=refresh_token
7. summary
This time, due to the lack of in-depth understanding of spring security oauth2, leading to a toss in the search for an exception throw solution. The overall idea is not complicated, but the most common request forwarding is used, but it needs to have a certain understanding of the filter chain. Interruption point is a good choice to look at slowly.