SpringBoot3整合“Spring Security+JWT”快速实现demo示例与Apifox测试
目录
一、pom文件中引入核心依赖。
二、工具类准备。(JWT生成、BCrypt加密)
三、配置文件application.yml。(spring、mybatis基础配置)
四、编写SecurityConfig核心配置类。
五、编写JWT认证过滤器。
六、相关实体类。
(1)请求与响应的封装实体类。
(2)用户实体类。(对应数据库表user)
(3)spring security的用户信息包装类。(重要)
七、相关Controller。
(1)登录接口。(设置放行,并由security自动校验用户名与密码)
(2)测试接口。(受保护。必须携带合法且未过期的JWT才能访问)
八、相关Service。
(1)User相关接口与实现类。
(2)Spring Security专用的用户加载服务类。(重要)
九、相关Mapper。
十、springboot启动类。
十一、使用Apifox模拟测试。
(1)登录相关测试。
(2)访问受保护的接口。
一、pom文件中引入核心依赖。
<!--构建springboot项目时根据选择的版本自动生成的起步start依赖--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.7</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.hyl</groupId><artifactId>springSecurityDemo</artifactId><version>0.0.1-SNAPSHOT</version><name>springSecurityDemo</name><description>springSecurityDemo</description><properties><java.version>21</java.version></properties>
<dependencies><!--spring web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.4</version></dependency><!--mysql driver--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!--spring security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><!-- JWT 处理的接口和抽象类--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><!--JWT 的创建、解析和验证--><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-jackson --><!--JWT 的 JSON 序列化和反序列化--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency></dependencies>
二、工具类准备。(JWT生成、BCrypt加密)
- JWT生成工具类。
package com.hyl.security.utils;import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function;/*** JWT工具类:生成令牌、解析令牌、验证令牌*/ @Component public class JwtUtil {// 密钥(实际项目中不要硬编码,可放在配置文件中)//HS256 算法强制要求密钥长度≥256 位private static final String SECRET_KEY = "suisuipingansuisuipingansuisuipingansuisuipingansuisuipinganFIVE";// 令牌有效期(10小时,根据需求调整)private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 10;// 从令牌中提取用户名public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);}// 从令牌中提取过期时间public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}// 通用方法:从令牌中提取特定声明public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);return claimsResolver.apply(claims);}// 解析令牌获取所有声明private Claims extractAllClaims(String token) {return Jwts.parser().setSigningKey(SECRET_KEY) // 使用密钥验证签名.parseClaimsJws(token).getBody();}// 检查令牌是否已过期private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}// 生成令牌(传入用户信息)public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>(); // 可添加自定义声明(如角色)return createToken(claims, userDetails.getUsername());}// 构建JWT令牌(设置声明、主题、过期时间并签名)private String createToken(Map<String, Object> claims, String subject) {return Jwts.builder().setClaims(claims) // 设置自定义声明.setSubject(subject) // 设置主题(通常是用户名).setIssuedAt(new Date(System.currentTimeMillis())) // 设置签发时间.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 设置过期时间.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用HS256算法和密钥签名.compact(); // 生成紧凑的URL安全字符串}// 验证令牌有效性(用户名匹配且未过期)public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));} }
- BCrypt加密测试工具类。(将生成密文存储到数据库中方便测试)
package com.hyl.security.utils;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/*** 密码加密工具类 - 用于生成BCrypt加密的密码* 运行main方法可以获取指定明文的加密结果*/ public class PasswordEncoderUtil {public static void main(String[] args) {// 需要加密的明文密码(修改这里)String rawPassword = "123";// 创建BCrypt编码器BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();// 生成加密后的密码String encodedPassword = encoder.encode(rawPassword);// 输出结果(复制这个结果到数据库中)System.out.println("明文密码: " + rawPassword);System.out.println("BCrypt加密后的密码: " + encodedPassword);} }
- 比如测试明文:123。得到BCrypt加密的字符串。
三、配置文件application.yml。(spring、mybatis基础配置)
#启动端口号 server:port: 9090#spring相关基础配置 spring:application:name: springSecurityDemodatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8&allowPublicKeyRetrieval=trueusername: rootpassword: root123#mybatis相关基础配置 mybatis:mapper-locations: classpath:mapper/*.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true
四、编写SecurityConfig核心配置类。
package com.hyl.security.config;import com.hyl.security.filter.JwtAuthFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration // 配置类 @EnableWebSecurity // 启用Spring Security安全机制 public class SecurityConfig {// 注入JWT过滤器(构造器注入)private final JwtAuthFilter jwtAuthFilter;public SecurityConfig(JwtAuthFilter jwtAuthFilter) {this.jwtAuthFilter = jwtAuthFilter;}// 配置安全过滤链(核心配置)@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 禁用CSRF:JWT是无状态的,不需要CSRF保护.csrf(csrf -> csrf.disable())// 配置接口访问权限.authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll() // 登录接口允许匿名访问.anyRequest().authenticated() // 其他接口必须认证(如:/api/test))// 配置会话管理:JWT不需要会话,用无状态模式.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 添加JWT过滤器:在用户名密码验证前执行.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}// 密码加密器:存储密码时用BCrypt加密@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 认证管理器:处理登录时的用户名密码验证@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();} }
五、编写JWT认证过滤器。
package com.hyl.security.filter;import com.hyl.security.utils.JwtUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; 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.web.filter.OncePerRequestFilter; import java.io.IOException;/*** JWT认证过滤器:拦截请求并验证JWT令牌* 执行时机:每个请求都会经过该过滤器(除了配置的放行接口)*/ @Component public class JwtAuthFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtil jwtUtil; // JWT工具类@Autowiredprivate UserDetailsService userDetailsService; // 加载用户信息的服务@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {// 1. 从请求头获取令牌(格式:Authorization: Bearer <令牌>)String authHeader = request.getHeader("Authorization");String jwt = null;String username = null;// 检查请求头是否包含Bearer令牌if (authHeader != null && authHeader.startsWith("Bearer ")) {// 截取令牌(去掉"Bearer "前缀,注意有个空格)jwt = authHeader.substring(7);try {// 从令牌中解析出用户名username = jwtUtil.extractUsername(jwt);} catch (Exception e) {// 令牌无效时(如过期、篡改),这里会抛出异常logger.error("JWT令牌验证失败:" + e.getMessage());}}// 2. 验证令牌并设置用户认证信息// 如果用户名不为空,且当前上下文没有认证信息(未登录)if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {// 从数据库加载用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(username);// 验证令牌是否有效(签名正确+未过期)if (jwtUtil.validateToken(jwt, userDetails)) {// 创建认证对象(Spring Security需要的格式)UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());// 设置请求详情(如IP地址等)authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 将认证信息存入上下文:表示用户已登录SecurityContextHolder.getContext().setAuthentication(authToken);}}// 3. 继续执行后续过滤器(让请求到达目标接口)filterChain.doFilter(request, response);} }
六、相关实体类。
(1)请求与响应的封装实体类。
- AuthenticationRequest:最大作用是接收登录时传递的账号、密码的数据值。
package com.hyl.security.entity;public class AuthenticationRequest {private String username;private String password;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {return "AuthenticationRequest{" +"username='" + username + '\'' +", password='" + password + '\'' +'}';} }
- AuthenticationResponse:最大作用是将生成的JWT令牌响应成功回去。
package com.hyl.security.entity;public class AuthenticationResponse {private String jwt;public AuthenticationResponse(String jwt) {this.jwt = jwt;}public String getJwt() {return jwt;} }
(2)用户实体类。(对应数据库表user)
package com.hyl.security.entity;public class User {private Long id;private String username;private String password;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", password='" + password + '\'' +'}';} }
(3)spring security的用户信息包装类。(重要)
package com.hyl.security.entity;import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections;/*** Spring Security需要的用户信息包装类* 作用:将数据库的User转换为Spring Security能识别的用户对象*/ public class CustomUserDetails implements UserDetails {private final User user; // 数据库用户实体public CustomUserDetails(User user) {this.user = user;}// 获取用户权限(这里简化为无权限,实际项目可添加角色)@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return Collections.emptyList(); // 暂时返回空列表}// 获取密码(从数据库用户对象中获取)@Overridepublic String getPassword() {return user.getPassword();}// 获取用户名(从数据库用户对象中获取)@Overridepublic String getUsername() {return user.getUsername();}// 以下方法都返回true,表示用户状态正常(新手默认即可)@Overridepublic boolean isAccountNonExpired() { return true; } // 账号未过期@Overridepublic boolean isAccountNonLocked() { return true; } // 账号未锁定@Overridepublic boolean isCredentialsNonExpired() { return true; } // 密码未过期@Overridepublic boolean isEnabled() { return true; } // 账号启用 }
七、相关Controller。
(1)登录接口。(设置放行,并由security自动校验用户名与密码)
- AuthController。处理登录请求。成功后返回JWT令牌。
package com.hyl.security.controller;import com.hyl.security.entity.AuthenticationRequest; import com.hyl.security.entity.AuthenticationResponse; import com.hyl.security.service.impl.CustomUserDetailsService; import com.hyl.security.utils.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;/*** 认证控制器:处理登录请求*/ @RestController @RequestMapping("/api/auth") public class AuthController {// 认证管理器(由Spring自动配置)@Autowiredprivate AuthenticationManager authenticationManager;// 加载用户信息的服务@Autowiredprivate CustomUserDetailsService customUserDetailsService;// JWT工具类@Autowiredprivate JwtUtil jwtUtil;/*** 登录接口:验证用户名密码并返回JWT令牌*/@PostMapping("/login")public ResponseEntity<?> login(@RequestBody AuthenticationRequest request) {try {// 1. 验证用户名密码(Spring Security自动调用CustomUserDetailsService查询用户)authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getUsername(),request.getPassword()));} catch (BadCredentialsException e) {// 密码错误时返回401return ResponseEntity.status(401).body("登录失败:用户名或密码错误");}// 2. 验证通过,生成JWT令牌UserDetails userDetails = customUserDetailsService.loadUserByUsername(request.getUsername());String jwt = jwtUtil.generateToken(userDetails);// 3. 返回令牌return ResponseEntity.ok(new AuthenticationResponse(jwt));} }
(2)测试接口。(受保护。必须携带合法且未过期的JWT才能访问)
- TestController。受保护的接口。
- 必须携带JWT令牌,且key=Authorization,value=Bearer <生成的jwt字符串>。
package com.hyl.security.controller;import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;/*** 测试受保护的接口:需要携带JWT令牌才能访问*/ @RestController @RequestMapping("/api/test") public class TestController {// 访问该接口需要在请求头添加:Authorization: Bearer <你的令牌>@GetMappingpublic String test() {return "访问成功!这是受保护的接口内容";} }
八、相关Service。
(1)User相关接口与实现类。
- UserService接口。
package com.hyl.security.service;import com.hyl.security.entity.User;public interface UserService {/*** 根据用户名查询用户* @param username* @return*/User selectByUsername(String username); }
- UserServiceImpl实现类。
package com.hyl.security.service.impl;import com.hyl.security.entity.User; import com.hyl.security.mapper.UserMapper; import com.hyl.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic User selectByUsername(String username) {return userMapper.selectByUsername(username);} }
(2)Spring Security专用的用户加载服务类。(重要)
package com.hyl.security.service.impl;import com.hyl.security.entity.CustomUserDetails; import com.hyl.security.entity.User; import com.hyl.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; 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;/*** Spring Security专用的用户加载服务* 作用:当需要验证用户时,Spring Security会自动调用该类查询用户*/ @Service public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserService userService; // 注入用户服务// 根据用户名查询用户(Spring Security会自动调用该方法)@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 从数据库查询用户User user = userService.selectByUsername(username);// 2. 如果用户不存在,抛出异常(Spring Security会自动处理为登录失败)if (user == null) {throw new UsernameNotFoundException("用户名不存在:" + username);}// 3. 将数据库用户转换为Spring Security需要的用户对象return new CustomUserDetails(user);} }
九、相关Mapper。
- UserMapper。
package com.hyl.security.mapper;import com.hyl.security.entity.User;public interface UserMapper {User selectByUsername(String username); }
- .UserMapper.xml。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.hyl.security.mapper.UserMapper"><select id="selectByUsername" resultType="com.hyl.security.entity.User" parameterType="java.lang.String">select * from `user` where username = #{username}</select></mapper>
十、springboot启动类。
- 启动springboot服务。开启mapper扫描。
package com.hyl.security;import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication @MapperScan("com.hyl.security.mapper") public class SpringSecurityDemoApplication {public static void main(String[] args) {SpringApplication.run(SpringSecurityDemoApplication.class, args);}}
十一、使用Apifox模拟测试。
- 提前准备好数据库。
(1)登录相关测试。
- 登录失败。
- 登录成功。获取到对应的JWT令牌。
(2)访问受保护的接口。
- 直接访问接口:http://localhost:9090/api/test。
- 使用Apifox并携带对应的登录成功后返回的令牌一起请求受保护的接口。
- 另外一种便捷的请求方式。