Spring Security OAuth2 requests to carry client information verification, custom exception return, no right to handle, token failure handling

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:

This article's source address

Authorization Server Configurer Adapter for OAUth2 has three main methods:

  1. AuthorizationServerSecurityConfigurer:

    Security constraints for configuring Token Endpoint

  2. ClientDetailsServiceConfigurer:

    Configure the client detailed service. The client details are initialized here.

  3. 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:

Keywords: Programming encoding Spring JSON

Added by pleisar on Wed, 31 Jul 2019 06:39:51 +0300