Spring Security Actual Dry Goods: Using JWT Certified Access Interface

Keywords: Java Spring JSON SSL

1. Preface

Welcome to Read Spring Security Actual Dry Series .Previously, I explained how to write my own Jwt generator and how to return to Json Web Token after user authentication has passed.Today we'll look at how to use Jwt access authentication in requests.The DEMO get method is at the end.

2. Common Http authentication methods

To use Jwt in Http requests, we must understand the common Http authentication methods.

2.1 HTTP Basic Authentication

HTTP Basic Authentication, also known as Basic Authentication, simply uses the Base64 algorithm to encrypt the user name and password, and puts the encrypted information in the request Header. In essence, it is not safe to transmit the user name and password in clear text, so it is best used in Https environment.Its certification process is as follows:

The client initiates a GET request and the server responds with 401 Unauthorized, www-Authenticate specifies the authentication algorithm, and realm specifies the security domain.Clients will then typically pop up to prompt for user name and password, enter user name password and put it into Header to request again, and the server will respond to the client with 200 status code after successful authentication.

2.2 HTTP Digest Authentication

To compensate for the weakness of BASIC authentication, there is HTTP Digest Authentication.It is also called digest authentication.It uses random numbers plus MD5 algorithm to digest user names and passwords, similar to Http Basic Authentication, but more complex:

Step 1: As with basic authentication, just return the response with the WWW-Authenticate header field.This field contains the temporary consulting code (random number, nonce) required for the authentication of the question response method.The first field, WWW-Authenticate, must contain information for both realm and nonce fields.Clients authenticate by returning these two values to the server.Nonce is an arbitrary random string that is generated each time a 401 response is returned.This string is generally recommended as a base 64 encoded hexadecimal number, but the actual content depends on the server's implementation

Step 2: The client receiving the 401 status code returns a response that contains the Authorizationinformation of the first field required for DIGEST authentication.The first field, Authorization, must contain field information for username, realm, nonce, uri, and response, where realm and nonce are the fields in the response previously received from the server.

Step 3: Receive the server that contains the Authorization request for the first field, and verify that the authentication information is correct.A response containing the Request-URI resource is returned after the authentication has passed.

And then Authorization-Info in the first field writes some information about the success of the certification.

2.3 SSL Client Authentication

SSL client authentication is what we usually call HTTPS.The security level is high, but CA certificate fees are incurred.There are some important concepts involved in the process of SSL authentication, such as the public key of the digital certificate authority, the private and public key of the certificate, the asymmetric algorithm (used with the private and public keys of the certificate), the symmetric key, and the symmetric algorithm (used with the symmetric key).Relatively complex. Not much here.

2.4 Form Form Certification

Form forms are not authenticated as HTTP specifications.So the ways of implementation are also diverse, in fact, our usual scanner login and mobile authentication code login belong to the category of form login.Form authentication typically works with cookies, sessions, and is now used by many Web sites.The user fills in the user name and password in the login page. After the server authenticates, the sessionId is returned to the browser. The browser saves the sessionId to the browser's Cookie.Because HTTP is stateless, browsers use cookies to save sessionId.The next time the client will carry the sessionId value with it in the request it sends, the server will discover the existence of the sessionId and use it as an index to get the authentication information of the user who has the server for authentication.Authentication provides access to resources.

We are in Spring Security Actual Dry Goods: Return to JWT Token after login In fact, this article also obtains Jwt by submitting a Form. Jwt actually has the same effect as sessionId, but Jwt naturally carries some user information, and sessionId needs to get user information further.

2.5 Json Web Token's Authentication Method Bearer Authentication

We obtain a Json Web Token through form authentication, so how do we use it? Usually we use Jwt as a token using Bearer Authentication.Bearer Authentication is a token-based HTTP authentication scheme in which a user requests access to a restricted resource from the server with a Token as a credential, and a specific resource can be accessed if the check passes.Originally part of OAuth 2.0 in RFC 6750, it can sometimes be used alone.
We use Bear Token by placing a Bearer <token>formatted encrypted string (Json Web Token) in the Authorization field of the request header.Note that there is an empty character bit between the Bearer prefix and Token, and similar to the base authentication, Bearer Authentication can only be used on HTTPS (SSL).

3. Implement interface Jwt authentication in Spring Security

Next we are the highlight of our series, the Jwt certification of the interface.

3.1 Define the Json Web Token filter

Regardless of the authentication method mentioned above, we can use Filter in Spring Security to handle it.The Spring Security default base configuration does not provide a filter for Bearer Authentication processing, but it does provide a filter for Basic Authentication processing:

org.springframework.security.web.authentication.www.BasicAuthenticationFilter

BasicAuthenticationFilter Inherited OncePerRequestFilter . So we imitate it BasicAuthenticationFilter To achieve one's own JwtAuthenticationFilter . The complete code is as follows:

 package cn.felord.spring.security.filter;
 
 import cn.felord.spring.security.exception.SimpleAuthenticationEntryPoint;
 import cn.felord.spring.security.jwt.JwtTokenGenerator;
 import cn.felord.spring.security.jwt.JwtTokenPair;
 import cn.felord.spring.security.jwt.JwtTokenStorage;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpHeaders;
 import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.CredentialsExpiredException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.List;
 import java.util.Objects;
 
 /**
  * jwt Authentication Interceptor is used to intercept requests to extract jwt authentication
  *
  * @author dax
  * @since 2019/11/7 23:02
  */
 @Slf4j
 public class JwtAuthenticationFilter extends OncePerRequestFilter {
     private static final String AUTHENTICATION_PREFIX = "Bearer ";
     /**
      * Authentication is responded to by this endpoint if it fails
      */
     private AuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
     private JwtTokenGenerator jwtTokenGenerator;
     private JwtTokenStorage jwtTokenStorage;
 
 
     public JwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
         this.jwtTokenGenerator = jwtTokenGenerator;
         this.jwtTokenStorage = jwtTokenStorage;
     }
 
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
         // If certified
         if (SecurityContextHolder.getContext().getAuthentication() != null) {
             chain.doFilter(request, response);
             return;
         }
         // Get the header to parse out the jwt and authenticate No token to go directly to the next filter because the SecurityContext won't let you go without permission
         String header = request.getHeader(HttpHeaders.AUTHORIZATION);
         if (StringUtils.hasText(header) && header.startsWith(AUTHENTICATION_PREFIX)) {
             String jwtToken = header.replace(AUTHENTICATION_PREFIX, "");
 
 
             if (StringUtils.hasText(jwtToken)) {
                 try {
                     authenticationTokenHandle(jwtToken, request);
                 } catch (AuthenticationException e) {
                     authenticationEntryPoint.commence(request, response, e);
                 }
             } else {
                 // No token with safety header
                 authenticationEntryPoint.commence(request, response, new AuthenticationCredentialsNotFoundException("token is not found"));
             }
 
         }
         chain.doFilter(request, response);
     }
 
     /**
      * Specific authentication methods do not carry token for anonymous access
      * Some logic supplements itself with the basic functionality here
      *
      * @param jwtToken jwt token
      * @param request  request
      */
     private void authenticationTokenHandle(String jwtToken, HttpServletRequest request) throws AuthenticationException {
 
         // A valid token will be resolved based on my implementation
         JSONObject jsonObject = jwtTokenGenerator.decodeAndVerify(jwtToken);
 
         if (Objects.nonNull(jsonObject)) {
             String username = jsonObject.getStr("aud");
 
             // Get token from cache
             JwtTokenPair jwtTokenPair = jwtTokenStorage.get(username);
             if (Objects.isNull(jwtTokenPair)) {
                 if (log.isDebugEnabled()) {
                     log.debug("token : {}  is  not in cache", jwtToken);
                 }
                 // Fail if none of the caches exist
                 throw new CredentialsExpiredException("token is not in cache");
             }
             String accessToken = jwtTokenPair.getAccessToken();
 
             if (jwtToken.equals(accessToken)) {
                   // Parse permission set here
                 JSONArray jsonArray = jsonObject.getJSONArray("roles");
 
                 String roles = jsonArray.toString();
 
                 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(roles);
                 User user = new User(username, "[PROTECTED]", authorities);
                 // Build user authentication token
                 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
                 usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                 // Place in Security Context
                 SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
             } else {
                 // token mismatch
                 if (log.isDebugEnabled()){
                     log.debug("token : {}  is  not in matched", jwtToken);
                 }
 
                 throw new BadCredentialsException("token is not matched");
             }
         } else {
             if (log.isDebugEnabled()) {
                 log.debug("token : {}  is  invalid", jwtToken);
             }
             throw new BadCredentialsException("token is invalid");
         }
     }
 }

Looking specifically at the Code Notes section, there are some areas of logic that are tailored to your business.Anonymous access must not take Token!

3.2 Configure JwtAuthenticationFilter

First inject the filter JwtAuthenticationFilter into the Spring IoC container, then be sure to place the JwtAuthenticationFilter order before the UsernamePasswordAuthenticationFilter:

        @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.csrf().disable()
                     .cors()
                     .and()
                     // session generation policy uses stateless policy
                     .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                     .and()
                     .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                     .and()
                     .authorizeRequests().anyRequest().authenticated()
                     .and()
                     .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
                     // jwt must be configured before UsernamePasswordAuthenticationFilter
                     .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                     // Return error message after successful login if jwt token fails
                     .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
                     .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
 
         }

4. Use Jwt for request validation

Write a restricted interface, and here is http://localhost:8080/foo/test.Direct requests will be 401.We get Token in the following way:

Then use Jwt in Postman:

Ultimately, it certifies success and accesses resources.

5. Refresh Jwt Token

We are in Spring Security Actual Dry Goods: Hand-on instructions for implementing JWT Token Json Web Token is already implemented as a paired logic.AccessToken is used for interface requests and refreshToken is used to refresh accessToken.We can also define a Filter that refers to the JwtAuthenticationFilter above.This request only carries a refreshToken, and our intercept URI in the Filter matches the refresh endpoint that we defined.Similarly, verify Token and return to the Token pair as if the login was successful.No more code demonstrations here.

6. Summary

This is a series of original articles, there are always students who don't look carefully can't catch their minds and have a lot of criticism.The meal needs to be eaten one bite at a time. There is no ready-made food to eat. It comes all this way. What's urgent?Originality is not easy, focus is the power. Spring Security Actual Dry Series Each piece has different points of knowledge, and they are related to each other.Look back where you don't understand.Spring Security is not difficult to learn. The key is that you find the right idea or not.This DEMO can be obtained by following the public number Felordcn in reply to ss08.

Focus on Public Number: Felordcn for more information

Personal blog: https://felord.cn

Posted by rklapwijk on Thu, 07 Nov 2019 12:52:41 -0800