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; } }