1. Introduction
The security default dependency is very different from the configuration before Security5.7. We will dig deep into the differences between these two versions and how they affect the security architecture of modern web applications. In particular, we will focus on analyzing how JWT (JSON Web Tokens) filters work and how it is combined with anonymous access to provide more flexible security controls for applications.
2. Environment
- JDK 17
- SpringBoot 3.2
- Security 6.3
3. Maven dependency
<!-- SecuritySafety --> <dependency> <groupId></groupId> <artifactId>spring-boot-starter-security</artifactId> <version>3.2.2</version> </dependency> <!-- jwtInterface authentication --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>
4. Know JWT
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact, self-contained way to safely transmit information between parties as JSON objects. This information can be verified and trusted because it is digitally signed.
1. JWT composition
The JSON Web Token consists of three parts, connected by dots (.) between them. A typical JWT looks like this:
- Part 1: The header typically consists of two parts: the type of token ("JWT") and the algorithm name (such as: HMAC SHA256 or RSA, etc.), and then, encode this JSON with Base64 to get the first part of the JWT.
- Part 2: payload It contains a declaration (required), a declaration is a declaration about an entity (usually a user) and other data.
- Part 3: Signature is used to verify whether the message has been changed during delivery, and for tokens signed with private keys, it can also verify whether the sender of the JWT is what it calls the sender.
Note: Do not place sensitive information in JWT's payload or header unless they are encrypted.
{ alg: "RS256" }. { //Storing custom user information, properties can be expanded by custom login_name: "admin", user_id: "xxxxx", ... }. [signature]
- The request header should look like this: Authorization: Bearer
5. Meet
1. The difference between the old version (Security5.7 version)
The default Security upgrade in SpringBoot3 has undergone a great change in writing. One of the most significant changes is the change in how the WebSecurityConfigurerAdapter class is used. This class is widely used in Spring Security for custom security configurations. Here are the main differences and changes in writing:
- Abandoned WebSecurityConfigurerAdapter:
In the version, WebSecurityConfigurerAdapter is a common method to implement secure configuration. Users customize security configurations by inheriting this class and overriding their methods. When it comes to Spring Security, the WebSecurityConfigurerAdapter is marked as deprecated, meaning it may be removed in future versions. This change is intended to promote the use of a more modern configuration approach, namely, component configuration.
- Component configuration is recommended for the new version:
In Spring Security, it is recommended to use a component configuration. This means you can create a configuration class that no longer needs to inherit the WebSecurityConfigurerAdapter.
You can directly define one or more SecurityFilterChain Beans to configure security rules. This approach is more flexible and more consistent with the overall style of Spring Framework.
2. The default filter
All supported filters are defined in the constructor of the class of the spring-security-config-6.2. package and the execution order is determined.
FilterOrderRegistration() { Step order = new Step(INITIAL_ORDER, ORDER_STEP); put(, ()); put(, ()); put(, ()); (); // gh-8105 put(, ()); put(, ()); put(, ()); put(, ()); put(, ()); put(, ()); put(, ()); ( "..OAuth2AuthorizationRequestRedirectFilter", ()); ( "..Saml2WebSsoAuthenticationRequestFilter", ()); put(, ()); put(, ()); ("", ()); ("..OAuth2LoginAuthenticationFilter", ()); ( "..Saml2WebSsoAuthenticationFilter", ()); put(, ()); (); // gh-8105 put(, ()); put(, ()); put(, ()); put(, ()); ( ".", ()); put(, ()); put(, ()); put(, ()); put(, ()); put(, ()); put(, ()); ("..OAuth2AuthorizationCodeGrantFilter", ()); put(, ()); put(, ()); put(, ()); put(, ()); put(, ()); }
3. Register SecurityFilterChain
private final String[] permitUrlArr = new String[]{"xxx"}; /** * Configure Spring Security Security Security Chain. */ @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { //Initialize the jwt filter and set the jwt public key var jwtTokenFilter = new JwtTokenFilter(); //Close the default login page (); ("Register JWT Certification SecurityFilterChain"); var chain = httpSecurity // Custom permission blocking rules .authorizeHttpRequests((requests) -> { //().permitAll(); //Release all requests!!! //Allow anonymous access requests //Customize anonymous access address and put it in permitAllUrl .requestMatchers(permitUrlArr).permitAll() // Except for the anonymous access address stated above, all other requests need to be authenticated .anyRequest() .authenticated(); }) // Disable HTTP response header .headers(headersCustomizer -> {headersCustomizer .cacheControl(cache -> ()) .frameOptions(options -> ());}) //Session is set to stateless, based on token, so session does not need .sessionManagement(session -> session .sessionCreationPolicy()) //Add a custom JWT authentication filter to verify the validity of jwt in the header, and insert it before UsernamePasswordAuthenticationFilter .addFilterBefore(jwtTokenFilter, ) //Disable form login .formLogin(formLogin -> ()) //Disable httpBasic login .httpBasic(httpBasic -> ()) //Disable rememberMe .rememberMe(rememberMe -> ()) // Disable CSRF because session is not used .csrf(csrf -> ()) //Allow cross-domain requests .cors(()) .build(); return chain; }
6. Customize JWT authentication filter based on OncePerRequestFilter
The advantage of using OncePerRequestFilter is that it can ensure that a request passes only once a filter. You can verify jwt in filter. After the verification is successful, the Security context needs to be marked. It is very important that mark certification has been passed. If the authentication is completed and the label is not marked, the subsequent filter still believes that the unauthentication causes no permission failure.
1. Marking authentication is successful
//Add to Spring context, marked as authenticated statusJwtAuthenticationToken jwtToken = new JwtAuthenticationToken(null); (true); //Tag authentication passed().setAuthentication(jwtToken);
7. Problems encountered
1. After joining Security6, the login page continues to appear
There are two settings to complete when closing the default login page. You can delete the loading of the DefaultLoginPageConfigurer class, or call the formLogin() function, as follows:
@Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { //Close the default login page (); var chain = httpSecurity //Disable form login .formLogin(formLogin -> ()) .build(); return chain; }
2. After configuring the anonymous accessed URL, still execute a custom filter.
If there is a problem that the custom filter is still executed after configuring the URL to anonymous access. That's the reason for this custom filter.
Only configured anonymous access through (…).permitAll(); can only work with the default filter if you want
To work for custom deleters, you also need to build a WebSecurityCustomizer Bean object and configure the address to be accessed anonymously based on anonymous functions.
Here is a writing method recommended by the official website. Here we recommend using two locations, anonymous access addresses configured, and using a public array for management.
It can ensure consistency of the configuration of the two locations.
/** Other addresses that do not require authentication */ private final String[] permitUrlArr = new String[]{ "/login" ,"/error" // Static resources ,"/static/**.ico" ,"/static/**.js" ,"/static/**.css" //Match springdoc ,"/" ,"/webjars/**" //Match the swagger path (default) , "/" , "/swagger-ui/" , "/v3/api-docs/**" , "/swagger-ui/**" //Monitoring and testing , "/actuator/**" }; @Bean public WebSecurityCustomizer ignoringCustomize(){ return (web) -> () .requestMatchers(permitUrlArr); } @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { //Initialize the jwt filter and set the jwt public key var jwtTokenFilter = new JwtTokenFilter(); //Close the default login page (); ("Register JWT Certification SecurityFilterChain"); var chain = httpSecurity // Custom permission blocking rules .authorizeHttpRequests((requests) -> { //Allow anonymous access requests //Customize anonymous access address and put it in permitAllUrl .requestMatchers(permitUrlArr).permitAll() // Except for the anonymous access address stated above, all other requests need to be authenticated .anyRequest() .authenticated(); }).build(); return chain; }
8. The main code for completing JWT authentication
Currently, it is an authentication of existing jwt. The jwt issued is based on RSA encryption content and needs to be decrypted using a public key. The public key is generally configured in the yml file. Key logic design 3 parts: SecuritConfig, JwtTokenFilter, JwtUtil.
1. JwtUtil
The public key is issued by the unified authentication center and is currently written in yml, with the format as follows:
: | -----BEGIN PUBLIC KEY----- xxxxxxxx -----END PUBLIC KEY-----
The JwtUtil class provides verification methods. For performance reasons, singleton pattern is used, and the validator only needs to be instantiated once.
public class JwtUtil { private static JwtUtil instance = new JwtUtil(); private static JWTVerifier jwtVerifier; //The key value of the public key in the configuration file private static final String jwtPublicKeyConfig=""; private JwtUtil() {} /** * Initialize the JWT validator based on a fixed configuration file * @return */ public static JwtUtil getInstance(){ if (jwtVerifier == null){ String publicKey = (jwtPublicKeyConfig); return getInstance(publicKey); } return instance; } /** * Initialize the JWT validator based on a custom public key * @return */ public static JwtUtil getInstance(String publicKey) { if (jwtVerifier == null){ initVerifier(publicKey); } return instance; } // Static initialization function private static synchronized void initVerifier(String publicKey) { if (jwtVerifier != null) return; //Replace with the actual Base64 encoded RSA public key string String publicKeyStr = ("\\s", "") // Remove all whitespace characters, including line breaks .replace("-----BEGINPUBLICKEY-----", "") .replace("-----ENDPUBLICKEY-----", ""); // Convert Base64-encoded public key string to PublicKey object byte[] encodedPublicKey = ().decode(publicKeyStr); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedPublicKey); KeyFactory keyFactory = null; try { keyFactory = ("RSA"); PublicKey pubKey = (keySpec); // Create an Algorithm object using the public key to verify the signature of the token Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) pubKey, null); // parse and verify tokens jwtVerifier = (algorithm).build(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (InvalidKeySpecException e) { throw new RuntimeException(e); }catch (Exception e){ throw new RuntimeException(e); } } /** * Parsing and validating JWT tokens. * * @param token JWT token string * @return Decoded JWT object * @throws Exception If parsing or verification fails, an exception is thrown */ public DecodedJWT verifyToken(String token) { return (token); } }
2. JwtTokenFilter
This class is the main logic of verification, and completes jwt verification and certified annotation.
public class JwtTokenFilter extends OncePerRequestFilter { private static Logger logger = (); private JwtUtil jwtUtil; //Get the configuration in yml public String getConfig(String configKey) { var bean = (); var val = (configKey); return val; } public JwtTokenFilter() throws ServletException { String jwtPubliKey = getConfig(""); initTokenFilter(jwtPubliKey); } public JwtTokenFilter(String jwtPubliKey) throws ServletException { initTokenFilter(jwtPubliKey); } @Override protected void initFilterBean() throws ServletException { } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { var pass = doTokenFilter(request,response,filterChain); if(!pass){ return; } (request,response); } /** * Initialize the Token filter. * @throws ServletException If an error occurs during initialization, a ServletException exception is thrown */ public void initTokenFilter(String publicKey) throws ServletException { ("Initialize TokenFilter"); if((publicKey)){ throw new ServletException("jwtPublicKey is null"); } ("jwtPublicKey:{}",publicKey); jwtUtil = (publicKey); ("Initialize JwtUtil complete"); } protected Boolean doTokenFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; // Get token from request header String token = ("Authorization"); if((token)){ ("jwt tokenEmpty,{} {}",(),()); // Verification failed, return 401 status code (HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); return false; } // Assuming that the token starts with "Bearer", this prefix needs to be removed if (("Bearer")) { token = ("Bearer\s+",""); } (()); try { // Call JwtUtils for token verification DecodedJWT jwtDecode = (token); //Add to Spring context, marked as authenticated status JwtAuthenticationToken jwtToken = new JwtAuthenticationToken(null); (true); ().setAuthentication(jwtToken); //Write login information into spring security context } catch (JWTVerificationException ex) { ("jwt token illegal"); (HttpServletResponse.SC_UNAUTHORIZED, "Illegal token:"+()); return false; } catch (Exception ex) { throw ex; } ("Token verification passed"); return true; } public static class JwtAuthenticationToken extends AbstractAuthenticationToken { private User userInfo; public JwtAuthenticationToken(User user) { super(null); =user; } @Override public User getPrincipal() { return userInfo; } @Override public Object getCredentials() { throw new UnsupportedOperationException(); } @Override public boolean implies(Subject subject) { return (subject); } } }
3. SecuritConfig
This class completes the configuration of addresses that require anonymous access, and also injects custom filters.
@Configuration public class SecurityConfig { private static final Logger logger = (); /** Other addresses that do not require authentication */ private final String[] permitUrlArr = new String[]{ "/login" ,"/error" // Static resources ,"/static/**.ico" ,"/static/**.js" ,"/static/**.css" //Match springdoc ,"/" ,"/webjars/**" //Match the swagger path (default) , "/" , "/swagger-ui/" , "/v3/api-docs/**" , "/swagger-ui/**" //Monitoring and testing , "/actuator/**" }; @Bean public WebSecurityCustomizer ignoringCustomize(){ return (web) -> () .requestMatchers(permitUrlArr); } @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { //Initialize the jwt filter and set the jwt public key var jwtTokenFilter = new JwtTokenFilter(); //Close the default login page (); ("Register JWT Certification SecurityFilterChain"); var chain = httpSecurity // Custom permission blocking rules .authorizeHttpRequests((requests) -> { //().permitAll(); //Release all requests!!! //Allow anonymous access requests //Customize anonymous access address and put it in permitAllUrl .requestMatchers(permitUrlArr).permitAll() // Except for the anonymous access address stated above, all other requests need to be authenticated .anyRequest() .authenticated(); }) // Disable HTTP response header .headers(headersCustomizer -> {headersCustomizer .cacheControl(cache -> ()) .frameOptions(options -> ());}) //Session is set to stateless, based on token, so session does not need .sessionManagement(session -> session .sessionCreationPolicy()) //Add a custom JWT authentication filter to verify the validity of jwt in the header, and insert it before UsernamePasswordAuthenticationFilter .addFilterBefore(jwtTokenFilter, ) //Disable form login .formLogin(formLogin -> ()) //Disable httpBasic login .httpBasic(httpBasic -> ()) //Disable rememberMe .rememberMe(rememberMe -> ()) // Disable CSRF because session is not used .csrf(csrf -> ()) //Allow cross-domain requests .cors(()) .build(); return chain; } @Bean public FilterRegistrationBean disableSpringBootErrorFilter(ErrorPageFilter filter){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); (filter); (false); return filterRegistrationBean; } }
Summarize
The above support introduces the access (JWT analysis and authentication) of existing JWT unified authentication systems, and does not involve JWT generation and management related content.
The current user information is based on JWT dynamic analysis, so there is no user information stored in the Security context based on AbstractAuthenticationToken. JwtAuthenticationToken already supports the storage of custom user information, and only needs to be passed in as needed. Use the().getAuthentication().getPrincipal(); method to obtain user information based on the Security context.
This is the end of this article about access implementation of JWT authentication. For more related content related to SpringBoot3 access implementation of JWT authentication, please search for my previous articles or continue browsing the following related articles. I hope everyone will support me in the future!