Above address: Spring Security OAuth 2 (1) (password,authorization_code,refresh_token,client_credentials) gets token
The last blog wrote a simple token authentication server of OAuth2, which only implements four ways to acquire token. It does not deal with exception handling, unauthorized handling, data integrity checking before token generation and so on. This article makes some supplements to these contents:
Authorization Server Configurer Adapter for OAUth2 has three main methods:
-
AuthorizationServerSecurityConfigurer:
Security constraints for configuring Token Endpoint
-
ClientDetailsServiceConfigurer:
Configure the client detailed service. The client details are initialized here.
-
AuthorizationServerEndpointsConfigurer:
Configure authorization and token access endpoints and token services
1. Client Information Completeness Check before Request
For incomplete requests with incomplete data, they can be returned directly to the front end without subsequent validation. client information is usually encoding in Authorization in Base64, for example, before encoding
client_name:111 (client_id:client_secret Base64 Code) Basic Y2xpZW50X25hbWU6MTEx
Create a new Client Details Authentication Filter to inherit OncePerRequestFilter
/** * @Description Client without complete client processing * @Author wwz * @Date 2019/07/30 * @Param * @Return */ @Component public class ClientDetailsAuthenticationFilter extends OncePerRequestFilter { private ClientDetailsService clientDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Only when you get token, you need to carry client information with you, leaving the others alone. if (!request.getRequestURI().equals("/oauth/token")) { filterChain.doFilter(request, response); return; } String[] clientDetails = this.isHasClientDetails(request); if (clientDetails == null) { ResponseVo resultVo = new ResponseVo(HttpStatus.UNAUTHORIZED.value(), "Client information is not included in the request"); HttpUtilsResultVO.writerError(resultVo, response); return; } this.handle(request, response, clientDetails, filterChain); } private void handle(HttpServletRequest request, HttpServletResponse response, String[] clientDetails, FilterChain filterChain) throws IOException, ServletException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { filterChain.doFilter(request, response); return; } MyClientDetails details = (MyClientDetails) this.getClientDetailsService().loadClientByClientId(clientDetails[0]); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details.getClientId(), details.getClientSecret(), details.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(request, response); } /** * Determine whether the request header contains client information and does not contain null Base64 encoding returned */ private String[] isHasClientDetails(HttpServletRequest request) { String[] params = null; String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (header != null) { String basic = header.substring(0, 5); if (basic.toLowerCase().contains("basic")) { String tmp = header.substring(6); String defaultClientDetails = new String(Base64.getDecoder().decode(tmp)); String[] clientArrays = defaultClientDetails.split(":"); if (clientArrays.length != 2) { return params; } else { params = clientArrays; } } } String id = request.getParameter("client_id"); String secret = request.getParameter("client_secret"); if (header == null && id != null) { params = new String[]{id, secret}; } return params; } public ClientDetailsService getClientDetailsService() { return clientDetailsService; } public void setClientDetailsService(ClientDetailsService clientDetailsService) { this.clientDetailsService = clientDetailsService; } }
Then add a filter chain to Authorization Server Security Configurer
/** * Security constraints for configuring Token Endpoint */ @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { // Access Interface for Loading client clientDetailsAuthenticationFilter.setClientDetailsService(clientDetailsService); // Filter before client authentication oauthServer.addTokenEndpointAuthenticationFilter(clientDetailsAuthenticationFilter); oauthServer .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients(); // Allow form login }
Verification effect:
Not carrying client information
Carry client information
2. Custom exception return format
The exception return format that comes with OAuth2 is:
{ "error": "invalid_grant", "error_description": "Bad credentials" }
This format is not very friendly for the front-end. The format we expect is:
{ "code":401, "msg":"msg" }
The following is the concrete realization:
New MyOAuth2 WebResponseException Translator to Implement WebResponseException Translator Interface Rewrite the ResponseEntity < Oauth2Exception > translate (Exception) method; the exception sent by authentication is captured here, and the exception occurred by authentication can be captured here. Here we can encapsulate our exception information into a uniform format and return it. How to deal with it varies from project to project? Here I copy D directly. Implementation of efault WebResponse Exception Translator
/** * @Description WebResponseExceptionTranslator * @Author wwz * @Date 2019/07/30 * @Param * @Return */ @Component public class MyOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> { private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); @Override public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e); // Exception stack gets OAuth2Exception exception Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType( OAuth2Exception.class, causeChain); // There is OAuth2Exception in the exception stack if (ase != null) { return handleOAuth2Exception((OAuth2Exception) ase); } ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase != null) { return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e)); } ase = (AccessDeniedException) throwableAnalyzer .getFirstThrowableOfType(AccessDeniedException.class, causeChain); if (ase instanceof AccessDeniedException) { return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase)); } ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer .getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain); if (ase instanceof HttpRequestMethodNotSupportedException) { return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase)); } // Server internal error if the above exception is not included return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e)); } private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException { int status = e.getHttpErrorCode(); HttpHeaders headers = new HttpHeaders(); headers.set("Cache-Control", "no-store"); headers.set("Pragma", "no-cache"); if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) { headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary())); } MyOAuth2Exception exception = new MyOAuth2Exception(e.getMessage(), e); ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(exception, headers, HttpStatus.valueOf(status)); return response; } public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) { this.throwableAnalyzer = throwableAnalyzer; } @SuppressWarnings("serial") private static class ForbiddenException extends OAuth2Exception { public ForbiddenException(String msg, Throwable t) { super(msg, t); } public String getOAuth2ErrorCode() { return "access_denied"; } public int getHttpErrorCode() { return 403; } } @SuppressWarnings("serial") private static class ServerErrorException extends OAuth2Exception { public ServerErrorException(String msg, Throwable t) { super(msg, t); } public String getOAuth2ErrorCode() { return "server_error"; } public int getHttpErrorCode() { return 500; } } @SuppressWarnings("serial") private static class UnauthorizedException extends OAuth2Exception { public UnauthorizedException(String msg, Throwable t) { super(msg, t); } public String getOAuth2ErrorCode() { return "unauthorized"; } public int getHttpErrorCode() { return 401; } } @SuppressWarnings("serial") private static class MethodNotAllowed extends OAuth2Exception { public MethodNotAllowed(String msg, Throwable t) { super(msg, t); } public String getOAuth2ErrorCode() { return "method_not_allowed"; } public int getHttpErrorCode() { return 405; } } }
Define your own OAuth2Exception format MyOAuth2Exception
/** * @Description Exception format * @Author wwz * @Date 2019/07/30 * @Param * @Return */ @JsonSerialize(using = MyOAuthExceptionJacksonSerializer.class) public class MyOAuth2Exception extends OAuth2Exception { public MyOAuth2Exception(String msg, Throwable t) { super(msg, t); } public MyOAuth2Exception(String msg) { super(msg); } }
Serialization class MyOAuth2Exception JacksonSerializer defining exceptions
/** * @Description Serialization of Defining Exception MyOAuth2Exception * @Author wwz * @Date 2019/07/11 * @Param * @Return */ public class MyOAuthExceptionJacksonSerializer extends StdSerializer<MyOAuth2Exception> { protected MyOAuthExceptionJacksonSerializer() { super(MyOAuth2Exception.class); } @Override public void serialize(MyOAuth2Exception value, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException { jgen.writeStartObject(); jgen.writeObjectField("code", value.getHttpErrorCode()); jgen.writeStringField("msg", value.getSummary()); jgen.writeEndObject(); } }
Add defined exception handling to Authorization Server Endpoints Configurer configuration
/** * Configure authorization and token access endpoints and token services */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(tokenStore()) // Configure token storage .userDetailsService(userDetailsService) // Configure custom user privilege data. Not configuring will cause token to fail to refresh .authenticationManager(authenticationManager) .tokenServices(defaultTokenServices())// Loading token configuration .exceptionTranslator(webResponseExceptionTranslator); // Custom exception return }
Demonstration effect:
3. Custom unauthorized access processor
The default unauthorized access return format is:
{ "error": "access_denied", "error_description": "Access is not allowed" }
The format we expect is:
{ "code":401, "msg":"msg" }
Create a new MyAccessDeniedHandler to implement AccessDeniedHandler and customize the return information:
/** * @Description Unauthorized Access Processor * @Author wwz * @Date 2019/07/30 * @Param * @Return */ @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseVo resultVo = new ResponseVo(); resultVo.setMessage("No access!"); resultVo.setCode(403); HttpUtilsResultVO.writerError(resultVo, response); } }
Increase in Resource Server Configurer Adapter resource allocation
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // Unauthorized Processor
Because I added annotation privileges to the request, only ROLE_USER users can access it, and then I logged in to ROLE_ADMIN users, so I have no right to deal with it.
@GetMapping("/hello") @PreAuthorize("hasRole('ROLE_USER')") public String hello(Principal principal) { return principal.getName() + " has hello Permission"; }
4. Custom token invalid processor
The default token invalid return information is:
{ "error": "invalid_token", "error_description": "Invalid access token: 78df4214-8e10-46ae-a85b-a8f5247370a" }
The format we expect is:
{ "code":403, "msg":"msg" }
New MyTokenException Entry Point to Realize Authentication Entry Point
/** * @Description Invalid Token Return Processor * @Author wwz * @Date 2019/07/30 * @Param * @Return */ @Component public class MyTokenExceptionEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { Throwable cause = authException.getCause(); response.setStatus(HttpStatus.OK.value()); response.setHeader("Content-Type", "application/json;charset=UTF-8"); try { if (cause instanceof InvalidTokenException) { HttpUtilsResultVO.writerError(new ResponseVo(405, "token Invalid"), response); //response.getWriter().write(JSONObject.toJSONString(new ResultVo(405, "token invalidation")); }else { HttpUtilsResultVO.writerError(new ResponseVo(405, "token Defect"), response); // response.getWriter().write(JSONObject.toJSONString(new ResultVo(405, "token missing")); } } catch (IOException e) { e.printStackTrace(); } } }
In resource allocation, inject:
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.authenticationEntryPoint(tokenExceptionEntryPoint); // token failure processor resources.resourceId("auth"); // Setting the resource id determines whether the resource has permission by the scope of the client }
Display effect: