本文将会带你了解在 Spring Boot 中如何使用 Spring Security、JWT 和 MySQL 数据库实现基于 Token 的身份认证。
JWT (JSON Web Token)概览 {#jwt-json-web-token概览}
JWT 是 JSON Web Token 的缩写,是一种安全地在各方之间传输信息的开放标准。它是一种紧凑、自包含的数据传输方法,通常用于客户端和服务器之间的数据传输。
JWT 通常用于认证和授权,服务器通过验证 JWT 中包含的数字签名来验证用户。
JWT 由三部分组成:Header、Payload 和 Signature(签名)。
- Header 包含 Token 类型和 Token 签名算法的元数据。
- Payload 包含关于被验证用户或实体的声明(Claim)或陈述。这些声明可包括用户 ID、用户名或电子邮件地址等信息。
- Signature(签名)使用秘钥和 Header 及 Payload 生成,以确保 JWT 的完整性。
使用 JWT 的一个好处是它们是无状态的,这意味着服务器无需跟踪用户的身份认证状态。这可以提高可扩展性和性能。此外,JWT 可以在不同的域和服务中使用,只要它们共享相同的秘钥来验证签名即可。
Spring Security 概览 {#spring-security-概览}
Spring Security 是一个提供身份认证、授权和防护常见攻击的框架。它为确保 Web 和响应式应用程序的安全提供一流的支持,是保护基于 Spring 的应用程序的事实标准。
Spring Security 用于保护 Web 应用程序、REST API 和微服务的安全,为身份认证和授权提供内置支持。
数据库表结构 {#数据库表结构}

添加 Maven 依赖 {#添加-maven-依赖}
添加如下依赖到 Spring Boot 项目:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
配置 MySQL 数据库 {#配置-mysql-数据库}
通过如下命令在 MySQL 中创建一个数据库,名为 login_system:
create database login_system
使用 MySQL数据库,需要配置数据库 URL、用户名和密码,以便 Spring 能在启动时与数据库建立连接。编辑 src/main/resources/application.properties 文件,添加以下属性:
spring.datasource.url = jdbc:mysql://localhost:3306/login_system
spring.datasource.username = root
spring.datasource.password = root
Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
logging.level.org.springframework.security=DEBUG
Model 层 - 创建 JPA 实体 {#model-层---创建-jpa-实体}
创建 User 和 Role JPA 实体,并在它们之间建立 多对多(MANY-to-MANY) 关系。
使用 JPA 注解在 User 和 Role 实体之间建立 多对多 关系。
User {#user}
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")
)
private Set<Role> roles;
}
Role {#role}
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Repository 层 {#repository-层}
UserRepository {#userrepository}
import net.javaguides.todo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByEmail(String email);
Optional<User> findByUsernameOrEmail(String username, String email);
boolean existsByUsername(String username);
}
RoleRepository {#rolerepository}
import net.javaguides.todo.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Map;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
JWT 实现 {#jwt-实现}
在 Spring boot 项目中创建一个 security 包,并添加以下与 JWT 相关的类。
JwtAuthenticationEntryPoint {#jwtauthenticationentrypoint}
创建 JwtAuthenticationEntryPoint 类,如下:
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
AuthenticationEntryPoint 由 ExceptionTranslationFilter 用来启动身份认证方案。它是一个入口点,用于检查用户是否已通过身份认证,如果用户已经认证,则登录该用户,否则抛出异常(unauthorized)。通常情况下,在简单的应用程序中可以直接使用该类,但当在 REST、JWT 等中使用 Spring Security 时,就必须对其进行继承,以提供更好的 Spring Security 过滤器链(filter chain)管理。
JWT - 修改 application.properties {#jwt---修改-applicationproperties}
在 application.properties 文件中添加以下两个与 JWT 相关的属性:
app.jwt-secret=daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
app-jwt-expiration-milliseconds=604800000
JTW 工具类 - JwtTokenProvider {#jtw-工具类---jwttokenprovider}
创建一个名为 JwtTokenProvider 工具类,用于生成、验证 JWT 以及从 JWT 中提取信息。
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwt-secret}")
private String jwtSecret;
@Value("${app-jwt-expiration-milliseconds}")
private long jwtExpirationDate;
// 生成 JWT token
public String generateToken(Authentication authentication){
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);
String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(key())
.compact();
return token;
}
private Key key(){
return Keys.hmacShaKeyFor(
Decoders.BASE64.decode(jwtSecret)
);
}
// 从 Jwt token 获取用户名
public String getUsername(String token){
Claims claims = Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
return username;
}
// 验证 Jwt token
public boolean validateToken(String token){
try{
Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parse(token);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
generateToken 方法 {#generatetoken-方法}
public String generateToken(Authentication authentication){
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);
String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(key())
.compact();
return token;
}
generateToken(Authentication authentication) 方法根据提供的 Authentication 对象生成一个新的 JWT,该对象包含被验证用户的信息。它使用 Jwts.builder() 方法创建一个新的 JwtBuilder 对象,设置 JWT 的 subject(即用户名)、发布日期(issue date)和到期日期(expiration date),并使用 key() 方法对 JWT 进行签名。最后,它会以字符串形式返回 JWT。
getUsername(String token) {#getusernamestring-token}
// 从 Jwt token 获取用户名
public String getUsername(String token){
Claims claims = Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
return username;
}
getUsername(String token) 方法从提供的 JWT 中提取 username。该方法使用 Jwts.parserBuilder() 方法创建一个新的 JwtParserBuilder 对象,使用 key() 方法设置签名密钥(Signing Key),并使用 parseClaimsJws() 方法解析 JWT。然后,它会从 JWT 的 Claims 对象中获取 subject(即用户名),并以字符串形式返回。
validateToken(String token) {#validatetokenstring-token}
// 校验 Jwt token
public boolean validateToken(String token){
try{
Jwts.parserBuilder()
.setSigningKey(key())
.build()
.parse(token);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
validateToken(String token) 方法会验证所提供的 JWT。该方法使用 Jwts.parserBuilder() 方法创建一个新的 JwtParserBuilder 对象,使用 key() 方法设置签名密钥,并使用 parse() 方法解析 JWT。如果 JWT 有效,该方法会返回 true。如果 JWT 无效或已过期,该方法会使用 logger 对象输出错误信息并返回 false。
JwtAuthenticationFilter {#jwtauthenticationfilter}
在 Spring Boot 应用程序中创建 JwtAuthenticationFilter 类,该类可拦截传入的 HTTP 请求并验证包含在 Authorization 头中的 JWT Token。如果 Token 有效,Filter 就会在 SecurityContext 中设置当前用户的 Authentication。
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
private UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 从 request 获取 JWT token
String token = getTokenFromRequest(request);
// 校验 token
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){
// 从 token 获取 username
String username = jwtTokenProvider.getUsername(token);
// 加载与令 token 关联的用户
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
上述代码的关键点如下:
- 该类继承了 Spring 的
OncePerRequestFilter,可确保每个请求只执行一次过滤器。 - 构造函数需要两个依赖:
JwtTokenProvider和UserDetailsService,它们是通过 Spring 的构造函数依赖注入机制注入的。 doFilterInternal方法是 Filter 的主要逻辑。它使用getTokenFromRequest方法从AuthorizationHeader 中提取 JWT Token,使用JwtTokenProvider类验证 Token,并在SecurityContextHolder中设置 Authentication 信息。getTokenFromRequest方法会解析AuthorizationHeader,并返回 Token 部分。SecurityContextHolder用于存储当前 request 的Authentication信息。在这种情况下,Filter 会将UsernamePasswordAuthenticationToken与该 Token 关联的UserDetails和authorities(授权)设置在一起。
CustomUserDetailsService {#customuserdetailsservice}
创建一个 Service,根据 name 或 email 从数据库中加载用户详细信息。
创建一个 CustomUserDetailsService,它实现了 UserDetailsService 接口(Spring Security 内置接口),并提供了 loadUserByUername() 方法的实现:
import lombok.AllArgsConstructor;
import net.javaguides.todo.entity.User;
import net.javaguides.todo.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() -> new UsernameNotFoundException("User not exists by Username or Email"));
Set<GrantedAuthority> authorities = user.getRoles().stream()
.map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
return new org.springframework.security.core.userdetails.User(
usernameOrEmail,
user.getPassword(),
authorities
);
}
}
Spring Security 使用 UserDetailsService 接口,该接口包含 loadUserByUsername(String username) 方法,用于查找给定 username 的 UserDetails。
UserDetails 接口代表一个经过认证的用户对象,Spring Security 提供了 org.springframework.security.core.userdetails.User 的开箱即用实现。
Spring Security 配置 {#spring-security-配置}
创建 SpringSecurityConfig 类,并添加以下配置:
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@AllArgsConstructor
public class SpringSecurityConfig {
private UserDetailsService userDetailsService;
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers("/api/auth/**").permitAll();
authorize.anyRequest().authenticated();
});
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
@Configuration 注解表示该类定义了 Spring Application Context 的配置。
@AllArgsConstructor 注解来自 Lombok 库,它会生成一个包含所有用 @NonNull 注解的字段的构造函数。
passwordEncoder() 方法是一个 Bean,用于创建 BCryptPasswordEncoder 实例,对密码进行编码。
securityFilterChain() 方法是一个定义安全过滤器链(Security Filter Chain)的 Bean。HttpSecurity 参数用于配置应用程序的安全设置。在本例中,该方法禁用 CSRF 保护,并根据 HTTP 方法和 URL 授权请求。
authenticationManager() 方法是一个提供 AuthenticationManager 的 Bean。它从 AuthenticationConfiguration 实例中检索 Authentication Manager。
Service 层 {#service-层}
创建一个 service 包,并添加以下与 service 层相关的 AuthService 接口和 AuthServiceImpl 类。
AuthService 接口 {#authservice-接口}
import net.javaguides.todo.dto.LoginDto;
public interface AuthService {
String login(LoginDto loginDto);
}
AuthServiceImpl 类 {#authserviceimpl-类}
import net.javaguides.todo.dto.LoginDto;
import net.javaguides.todo.repository.RoleRepository;
import net.javaguides.todo.repository.UserRepository;
import net.javaguides.todo.security.JwtTokenProvider;
import net.javaguides.todo.service.AuthService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
public class AuthServiceImpl implements AuthService {
private AuthenticationManager authenticationManager;
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
private JwtTokenProvider jwtTokenProvider;
public AuthServiceImpl(
JwtTokenProvider jwtTokenProvider,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public String login(LoginDto loginDto) {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
loginDto.getUsernameOrEmail(), loginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtTokenProvider.generateToken(authentication);
return token;
}
}
这是 AuthService 接口的实现。它包含一个方法 login(),用于处理应用程序的登录功能。loginDto 对象包含用户输入的用户名(username)和密码(password)。
该类的构造函数需要四个参数:JwtTokenProvider、UserRepository、PasswordEncoder 和 AuthenticationManager。
在 login() 方法中,authenticationManager 会尝试将用户的 loginDto 凭证传递给 UsernamePasswordAuthenticationToken,从而对用户进行身份认证。如果认证成功,将使用 jwtTokenProvider 对象生成一个 Token 并返回给调用者。
该 service 类使用 @Service 注解,表明它是 Spring 服务组件,可由 Spring Context 自动发现。
Controller 层 - 返回 JWT Token 的 REST API(登录) {#controller-层---返回-jwt-token-的-rest-api登录}
创建 AuthController 类,并添加以下代码:
import lombok.AllArgsConstructor;
import net.javaguides.todo.dto.JWTAuthResponse;
import net.javaguides.todo.dto.LoginDto;
import net.javaguides.todo.service.AuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@AllArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private AuthService authService;
// Login REST API
@PostMapping("/login")
public ResponseEntity<JWTAuthResponse> authenticate(@RequestBody LoginDto loginDto){
String token = authService.login(loginDto);
JWTAuthResponse jwtAuthResponse = new JWTAuthResponse();
jwtAuthResponse.setAccessToken(token);
return ResponseEntity.ok(jwtAuthResponse);
}
}
这段代码定义了一个用于用户身份认证的 REST API 端点。它通过 /api/auth/login URL 接收 POST 请求,请求体中的登录凭证是一个 JSON 对象。LoginDto 对象用于将 JSON 对象映射到 Java 对象。
AuthController 类有一个构造函数,用于接收 AuthService 的实例,AuthService 提供了身份认证逻辑。
authenticate 方法接收 LoginDto 对象作为参数,并调用 AuthService 的 login 方法来执行身份认证。如果验证成功,login 方法会返回一个 JWT Token。然后,该 Token 被封装在一个 JWTAuthResponse 对象中,并作为响应返回。
@PostMapping 注解将方法映射为 HTTP POST 方法。@RequestBody 注解表示请求体应映射到 LoginDto 对象。
SQL 脚本 {#sql-脚本}
在测试 Spring Security 和 JWT 之前,请确保使用以下 SQL 脚本将数据插入相应的表中:
INSERT INTO `users` VALUES
(1,'ramesh@gmail.com','ramesh','$2a$10$5PiyN0MsG0y886d8xWXtwuLXK0Y7zZwcN5xm82b4oDSVr7yF0O6em','ramesh'),
(2,'admin@gmail.com','admin','$2a$10$gqHrslMttQWSsDSVRTK1OehkkBiXsJ/a4z2OURU./dizwOQu5Lovu','admin');
INSERT INTO roles VALUES (1,'ROLE_ADMIN'),(2,'ROLE_USER');
INSERT INTO users_roles VALUES (2,1),(1,2);
Hibernate 会自动创建数据库表,因此无需手动创建。
使用 Postman 测试 {#使用-postman-测试}
测试返回 JWT Token 的登录 REST API,如下:

一切OK!
参考:https://www.javaguides.net/2023/05/spring-boot-spring-security-jwt-mysql.html
51工具盒子