SpringSecurity结合JwtToken验证
简介:本文在SpringSecurity基础公共之上,整合JwtToken功能,本文是后端部分。
对于SpringSecurity基本功能,可以看这篇文章:SpringSecurity入门案例——基本功能讲解
项目代码地址:https://gitee.com/geek-li-hua/code-in-blog.git
项目准备
添加依赖
在 pom.xml 中添加下列依赖:
- jjwt-api
- jjwt-impl
- jjwt-jackson
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
- 完整的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springsecurity-back-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springsecurity-back-demo</name>
<description>springsecurity-back-demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Starter JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Project Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- MySQL Connector/J -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<!-- MyBatis Plus Boot Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- MyBatis Plus Generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.0</version>
</dependency>
<!-- JJWT API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<!-- JJWT Implementation -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<!-- JJWT Jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
实现 JwtUtil
类
JwtUtil
类为jwt
工具类,用来创建、解析 jwt token
@Component
public class JwtUtil {
// 设置JWT的有效期为14天
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14;
// 设置JWT的密钥
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
// 生成UUID
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
// 创建JWT
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
// 构建JWT的Builder
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
// 使用HS256算法进行签名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成密钥
SecretKey secretKey = generalKey();
// 获取当前时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 如果未指定有效期,则使用默认的14天
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
// 计算过期时间
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
// 构建JWT
return Jwts.builder()
.setId(uuid) // 设置JWT的唯一标识
.setSubject(subject) // 设置JWT的主题
.setIssuer("sg") // 设置JWT的签发者
.setIssuedAt(now) // 设置JWT的签发时间
.signWith(signatureAlgorithm, secretKey) // 使用密钥进行签名
.setExpiration(expDate); // 设置JWT的过期时间
}
// 生成密钥
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
// 解析JWT
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey) // 使用密钥进行验证
.build()
.parseClaimsJws(jwt) // 解析JWT
.getBody(); // 获取JWT的内容
}
}
实现 JwtAuthenticationTokenFilter
类
实现 JwtAuthenticationTokenFilter
类,用来验证 jwt token
,如果验证成功,则将 User 信息注入上下文中。
/**
* JwtAuthenticationTokenFilter 是用于JWT身份验证的过滤器。
* 它继承了 OncePerRequestFilter 类,确保过滤器每个请求只应用一次。
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private IUserService userService;
/**
* 对每个传入的请求调用此方法,执行JWT身份验证过程。
*
* @param request 表示传入请求的 HttpServletRequest 对象。
* @param response 表示要发送的 HttpServletResponse 对象。
* @param filterChain 用于调用链中的下一个过滤器的 FilterChain 对象。
* @throws ServletException 处理请求时发生异常。
* @throws IOException 发生I/O异常。
*/
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取JWT令牌
String token = request.getHeader("Authorization");
// 如果令牌为空或不以"Bearer "开头,则跳过身份验证过程
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 移除令牌中的"Bearer "前缀
token = token.substring(7);
String userid;
try {
// 解析JWT令牌以获取声明,包括用户ID
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
// 如果在令牌解析过程中发生异常,则抛出运行时异常
throw new RuntimeException(e);
}
// 使用用户ID从用户服务中检索用户
User user = userService.getById(Integer.parseInt(userid));
// 如果用户不存在,则抛出运行时异常
if (user == null) {
throw new RuntimeException("用户名未登录");
}
// 创建表示已认证用户的 UserDetailsImpl 对象
UserDetailsImpl loginUser = new UserDetailsImpl(user);
// 创建包含已认证用户的身份验证令牌,并将其设置在安全上下文中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 调用链中的下一个过滤器
filterChain.doFilter(request, response);
}
}
配置config.SecurityConfig
类
放行登录、注册等接口。
/**
* SecurityConfig 是用于配置 Spring Security 的类。
* 它使用 @Configuration 注解将其标记为配置类,并使用 @EnableWebSecurity 注解启用 Web 安全功能。
* 它继承了 WebSecurityConfigurerAdapter 类,用于自定义 Spring Security 的配置。
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 配置密码编码器的 Bean。
*
* @return 用于密码编码的 PasswordEncoder 对象。
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置 AuthenticationManager 的 Bean。
*
* @return AuthenticationManager 对象。
* @throws Exception 如果配置 AuthenticationManager 失败。
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置 Spring Security 的 HTTP 安全性。
*
* @param http HttpSecurity 对象,用于配置 HTTP 安全性。
* @throws Exception 如果配置 HttpSecurity 失败。
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/account/token/", "/user/account/register/").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
// 将 JwtAuthenticationTokenFilter 添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
实现接口API
实现API需要三个相关类或接口:
- 在
service
下创建一个接口。 - 在
service
下创建一个类实现这个接口。 - 在
controller
下创建一个类进行操作。
1. 实现 LoginService
验证用户名密码,验证成功后返回 jwt token
(令牌)
创建接口:在 service
下 创建 user 创建 account
新建一个接口 LoginService
import java.util.Map;
public interface LoginService {
public Map<String, String> getToken(String username, String password);
}
创建实现类:在 service
下 impl
下实现一个LoginServiceImpl
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
/**
* 通过用户名和密码获取令牌。
*
* @param username 用户名。
* @param password 密码。
* @return 包含令牌信息的 Map 对象。
*/
@Override
public Map<String, String> getToken(String username, String password) {
// 创建一个 UsernamePasswordAuthenticationToken 对象,用于进行身份验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 使用 AuthenticationManager 对象进行身份验证
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 从身份验证结果中获取认证的用户信息
UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal();
User user = loginUser.getUser();
// 创建 JWT 令牌
String jwt = JwtUtil.createJWT(user.getId().toString());
// 创建一个 Map 对象,用于存储令牌信息
Map<String, String> map = new HashMap<>();
map.put("error_message", "success");
map.put("token", jwt);
return map;
}
}
创建一个:LoginController
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/account/token/")
public Map<String, String> getToken(@RequestParam Map<String, String> map) {
// 从请求参数中获取用户名和密码
String username = map.get("username");
String password = map.get("password");
// 打印用户名和密码
System.out.println(username + ' ' + password);
// 调用登录服务的方法获取令牌
return loginService.getToken(username, password);
}
}
2.配置InforService类
根据令牌返回用户信息。
创建接口:在 service下 新建一个接口 InfoService
public interface InfoService {
/*
* 根据token返回用户信息
* */
public Map<String, String> getInfo();
}
创建实现类:在 service
下 impl
下新建一个实现类InfoServiceImpl
/**
* 这个类是InfoService接口的实现类。
* 它提供了根据token获取用户信息的方法。
*/
@Service
public class InfoServiceImpl implements InfoService {
/**
* 根据token返回用户信息。
* @return 包含用户信息的map
*/
@Override
public Map<String, String> getInfo() {
// 从安全上下文中获取认证token
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
// 从认证token中获取已登录的用户详情
UserDetailsImpl loginUser = (UserDetailsImpl) authentication.getPrincipal();
User user = loginUser.getUser();
// 创建一个map来存储用户信息
Map<String, String> map = new HashMap<>();
map.put("error_message", "success");
map.put("id", user.getId().toString());
map.put("username", user.getUsername());
return map;
}
}
创建controller
:在 controller
创建一个InfoController
/**
* 这个类是一个REST控制器,用于处理与用户信息相关的请求。
*/
@RestController
public class InfoController {
@Autowired
private InfoService infoService;
/**
* 获取用户信息。
* @return 包含用户信息的map
*/
@GetMapping("/user/account/info/")
public Map<String, String> getInfo() {
// 调用infoService的getInfo()方法来获取用户信息
return infoService.getInfo();
}
}
3. 配置RegisterService
类
注册账号
创建接口:在 service下 新建一个接口 RegisterService
public interface RegisterService {
/*
* 注册账号
*
* @param username 用户名
* @param password 密码
* */
public Map<String, String> register(String username, String password, String confirmedPassword);
}
创建实现类:在 service下 新建一个实现类RegisterServiceImpl
/**
* 这个类是RegisterService接口的实现类。
* 它提供了注册用户的方法。
*/
@Service
public class RegisterServiceImpl implements RegisterService {
@Autowired
private UserMapper userMapper; // 用户数据访问对象
@Autowired
private PasswordEncoder passwordEncoder; // 密码加密器
/**
* 注册新用户。
* @param username 用户名
* @param password 密码
* @param confirmedPassword 确认密码
* @return 包含注册结果的map
*/
@Override
public Map<String, String> register(String username, String password, String confirmedPassword) {
Map<String, String> map = new HashMap<>(); // 创建一个map对象用于存储注册结果
// 检查用户名是否为空
if (username == null) {
map.put("error_message", "用户名不能为空");
return map;
}
// 检查密码是否为空
if (password == null || confirmedPassword == null) {
map.put("error_message", "密码不能为空");
return map;
}
// 删除用户名首尾的空白字符
username = username.trim();
// 检查用户名是否为空
if (username.length() == 0) {
map.put("error_message", "用户名不能为空");
return map;
}
// 检查密码是否为空
if (password.length() == 0 || confirmedPassword.length() == 0) {
map.put("error_message", "密码不能为空");
return map;
}
// 检查用户名长度是否超过限制
if (username.length() > 100) {
map.put("error_message", "用户名长度不能大于100");
return map;
}
// 检查密码长度是否超过限制
if (password.length() > 100 || confirmedPassword.length() > 100) {
map.put("error_message", "密码不能大于100");
return map;
}
// 检查两次输入的密码是否一致
if (!password.equals(confirmedPassword)) {
map.put("error_message", "两次输入的密码不一致");
return map;
}
// 查询用户名是否已存在
QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
queryWrapper.eq("username", username);
List<User> users = userMapper.selectList(queryWrapper);
if (!users.isEmpty()) {
map.put("error_message", "用户名已存在");
return map;
}
// 添加一个新用户
String encodedPassword = passwordEncoder.encode(password); // 对密码进行加密处理
User user = new User(null, username, encodedPassword); // 创建一个新的用户对象
userMapper.insert(user); // 将用户对象插入数据库
map.put("error_message", "success"); // 注册成功
return map;
}
}