Integrating JWT and Spring Security with a Custom SM4 PasswordEncoder in Spring Boot
This article demonstrates how to secure a Spring Boot 2.7.7 application using JWT, Spring Security, and a custom SM4‑based PasswordEncoder, covering dependency setup, security configuration, custom authentication components, token validation filter, and a login endpoint implementation.
The example uses the following technology stack: Spring Boot 2.7.7, MyBatis‑Plus 3.5.3.1, Hutool 5.7.22, Redis, MySQL 8, the Chinese national encryption algorithm SM4, JWT, and Lombok.
JWT Overview
JSON Web Token (JWT) is a widely used solution for user authentication.
Basic JWT Generation and Authentication Flow
A simplified sequence diagram illustrates the process of creating and validating a JWT.
Spring Security Overview
Spring Security is a powerful and highly customizable authentication and access‑control framework.
Spring Security provides a robust and extensible security framework.
Adding Spring Security Dependency
Include the following dependency in pom.xml :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>Spring Security Configuration Notes
Note: Starting with Spring Boot 2.7.0, WebSecurityConfigurerAdapter is deprecated.
Typical configuration items include:
AuthenticationProvider implementation: custom authentication logic.
Filter: validates token validity.
AuthenticationManager: processes authentication requests.
PasswordEncoder: handles password hashing and verification.
SecurityFilterChain: the filter chain.
Custom PasswordEncoder
The PasswordEncoder interface encrypts passwords and verifies them. Spring Security offers several implementations such as BCryptPasswordEncoder , NoOpPasswordEncoder , Pbkdf2PasswordEncoder , and MessageDigestPasswordEncoder .
Implementing a SM4‑based PasswordEncoder
Steps to create a custom SM4 PasswordEncoder :
Add the Bouncy Castle dependency:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.71</version>
</dependency>Create Sm4PasswordEncoder.java :
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.SmUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class Sm4PasswordEncoder implements PasswordEncoder {
// key length must be 16
private static final String KEY = "KeyMustBe16Size.";
@Override
public String encode(CharSequence rawPassword) {
return SmUtil.sm4(KEY.getBytes(StandardCharsets.UTF_8)).encryptHex(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return Objects.equals(rawPassword.toString(),
SmUtil.sm4(KEY.getBytes(StandardCharsets.UTF_8)).decryptStr(encodedPassword, StandardCharsets.UTF_8));
}
}Register the custom PasswordEncoder bean in the security configuration.
Custom Token Validation Filter
Implement UserDetailsService to load user details, then create a filter that extracts the JWT from the Authorization header, retrieves the username, loads the user, and sets the authentication in the security context.
import cn.ddcherry.springboot.demo.constant.AuthConstant;
import cn.ddcherry.springboot.demo.util.JwtUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Validate token validity
*/
@Slf4j
public class TokenFilter extends OncePerRequestFilter {
@Resource
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getToken(request);
if (StrUtil.isNotEmpty(token)) {
String username = JwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StrUtil.isNotEmpty(bearerToken) && bearerToken.startsWith(AuthConstant.AUTHORIZATION_BEARER)) {
return bearerToken.replace(AuthConstant.AUTHORIZATION_BEARER, StrUtil.EMPTY);
}
return null;
}
}AuthenticationProvider Configuration
Define a DaoAuthenticationProvider bean that uses the custom UserDetailsService and the SM4 PasswordEncoder :
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}AuthenticationManager Bean
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}Security Filter Chain
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/test/**").permitAll()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(tokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}Login API
The LoginController receives username and password , authenticates via AuthenticationManager , generates a JWT, and returns the token together with the authenticated user information.
@RestController
@AllArgsConstructor
@RequestMapping("/api/auth")
public class LoginController {
private final AuthenticationManager authenticationManager;
@PostMapping("/login")
public Result
> login(String username, String password) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = JwtUtil.createToken(username, new HashMap<>());
AuthUser authUser = (AuthUser) authentication.getPrincipal();
Map
resultMap = new HashMap<>(16);
resultMap.put("token", token);
resultMap.put("user", authUser);
return Result.success(resultMap);
}
}Running the application and invoking the /api/auth/login endpoint returns a JWT on successful authentication; failure returns an error response.
Source: juejin.cn/post/7250357906713215037
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.