在前后端分离、微服务架构中,接口安全是核心需求 ------ 需解决用户身份认证、权限控制、接口防篡改、无状态通信等问题。Spring Security 作为一站式安全框架,提供认证、授权、防跨站请求伪造(CSRF)等完整功能,结合 JWT(JSON Web Token)可实现无状态认证,避免 Session 共享带来的分布式问题,是企业级接口安全的首选方案。
本文聚焦 SpringBoot 集成 Spring Security + JWT 的实战落地,从环境搭建、JWT 工具封装、认证授权配置,到用户登录、角色权限控制、接口保护,全程嵌入 Java 代码教学,帮你快速打造安全、可靠、无状态的接口安全体系。
一、核心认知:Spring Security + JWT 核心价值与适用场景
1. 核心优势
- 无状态认证:JWT 携带用户信息与权限,服务端无需存储会话,适配分布式微服务;
- 细粒度授权:支持基于角色(RBAC)、资源的权限控制,精准保护接口;
- 安全可靠:JWT 支持签名加密,防止数据篡改与伪造,Spring Security 提供成熟的安全防护机制;
- 兼容性强:无缝适配前后端分离架构,支持移动端、PC 端等多终端认证;
- 可扩展:支持自定义认证逻辑、权限校验规则,适配复杂业务场景。
2. 核心适用场景
- 前后端分离项目:无状态接口认证,替代传统 Session 认证;
- 微服务架构:跨服务认证授权,统一安全策略;
- 多终端应用:PC 端、移动端、小程序共用一套认证体系;
- 企业级系统:基于角色的权限管理(如管理员、普通用户、访客)。
3. JWT 核心原理
JWT 由三部分组成,通过 Base64 编码与签名连接,整体为一串字符串:
- 头部(Header):指定签名算法(如 HS256);
- 载荷(Payload):存储用户 ID、角色、过期时间等非敏感信息;
- 签名(Signature):基于头部算法、载荷、密钥生成签名,确保数据不被篡改。
二、核心实战一:环境搭建与 JWT 工具封装
1. 引入依赖(Maven)
xml
<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MyBatis-Plus(用户数据访问,可选) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2. JWT 工具类封装(生成 Token、解析 Token、验证 Token)
java
运行
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class JwtUtils {
// JWT 密钥(生产环境需放在配置中心,加密存储)
@Value("${jwt.secret:defaultSecretKey123456}")
private String secret;
// Token 过期时间(2小时,单位:毫秒)
@Value("${jwt.expire:7200000}")
private Long expire;
// 生成 JWT Token(携带用户ID、角色信息)
public String generateToken(Long userId, String username, String role) {
// 1. 构建载荷信息
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
// 2. 生成 Token
return Jwts.builder()
.setClaims(claims) // 载荷信息
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + expire)) // 过期时间
.signWith(SignatureAlgorithm.HS256, secret) // 签名算法与密钥
.compact();
}
// 解析 Token,获取载荷信息
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.error("Token 已过期", e);
throw new RuntimeException("Token 已过期");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
log.error("Token 无效", e);
throw new RuntimeException("Token 无效");
}
}
// 验证 Token 有效性(是否过期、签名是否正确)
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// 从 Token 中获取用户ID
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
// 从 Token 中获取用户角色
public String getRoleFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("role", String.class);
}
}
3. 配置文件(application.yml)
yaml
# JWT 配置
jwt:
secret: E7A5F2D8C9B3A1E4F6D2C7B8A9E3F4D5C6B7A8E9F0D1C2B3A4E5F6D7C8B9A0E1 # 生产环境使用复杂密钥
expire: 7200000 # 2小时过期
# Spring Security 配置(可选,后续在配置类中细化)
spring:
security:
user:
name: admin
password: 123456 # 临时默认用户,后续替换为数据库用户
# 数据库配置(用户表存储,可选)
datasource:
url: jdbc:mysql://localhost:3306/security_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
三、核心实战二:Spring Security 核心配置(认证与授权)
1. 自定义用户详情服务(从数据库获取用户信息)
Spring Security 需通过 UserDetailsService 获取用户信息,用于认证校验,这里结合数据库实现(用户表需包含 id、username、password、role 字段)。
java
运行
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.security.entity.User;
import com.example.security.mapper.UserMapper;
import javax.annotation.Resource;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 封装用户信息与权限(Spring Security 要求格式)
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword()) // 数据库密码需加密存储(如 BCrypt)
.roles(user.getRole()) // 角色(如 "ADMIN"、"USER")
.build();
}
}
2. 自定义 JWT 认证过滤器(解析 Token 并认证)
过滤器拦截请求,从请求头中获取 Token,解析后完成用户认证,让后续接口能获取当前登录用户信息。
java
运行
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.example.security.service.CustomUserDetailsService;
import com.example.security.utils.JwtUtils;
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;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private JwtUtils jwtUtils;
@Resource
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头获取 Token(格式:Bearer {token})
String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
// 2. 解析 Token,获取用户名并认证
if (token != null && jwtUtils.validateToken(token)) {
username = jwtUtils.parseToken(token).get("username", String.class);
// 3. 将用户信息存入 Security 上下文
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
// 4. 继续执行过滤器链
filterChain.doFilter(request, response);
}
}
3. Spring Security 主配置类
配置认证授权规则、密码加密方式、过滤器顺序等核心逻辑。
java
运行
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.method.configuration.EnableMethodSecurity;
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;
import com.example.security.filter.JwtAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity // 开启 Web 安全支持
@EnableMethodSecurity // 开启方法级权限控制(支持 @PreAuthorize 注解)
public class SecurityConfig {
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
// 密码加密器(BCrypt 加密,不可逆)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 认证管理器(用于用户登录认证)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
// 核心安全配置(接口授权、会话管理、过滤器顺序)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭 CSRF(前后端分离场景无需 CSRF 防护)
.csrf(csrf -> csrf.disable())
// 无状态会话(JWT 无状态认证,不创建 Session)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 接口授权规则
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录接口放行
.requestMatchers("/api/public/**").permitAll() // 公开接口放行
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 管理员接口仅 ADMIN 角色可访问
.anyRequest().authenticated() // 其他接口需认证
)
// 添加 JWT 过滤器(在用户名密码过滤器之前执行)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
四、核心实战三:用户登录与接口权限控制
1. 登录接口实现(生成 Token 返回给前端)
java
运行
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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;
import com.example.security.entity.LoginDTO;
import com.example.security.utils.JwtUtils;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private JwtUtils jwtUtils;
// 用户登录接口
@PostMapping("/login")
public String login(@RequestBody LoginDTO loginDTO) {
// 1. 执行认证(用户名密码校验)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword())
);
// 2. 认证成功,生成 Token
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
String role = userDetails.getAuthorities().iterator().next().getAuthority().replace("ROLE_", "");
// 从数据库查询用户ID(实际开发中可优化,避免重复查询)
Long userId = 1L; // 示例,实际需从用户信息中获取
String token = jwtUtils.generateToken(userId, username, role);
// 3. 返回 Token 给前端
return "Bearer " + token;
}
}
2. 方法级权限控制(@PreAuthorize 注解)
除了配置类中的接口路径授权,还可通过注解实现更细粒度的权限控制。
java
运行
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
// 仅 USER 角色可访问
@PreAuthorize("hasRole('USER')")
@GetMapping("/info")
public String getUserInfo() {
return "普通用户信息";
}
// 仅 ADMIN 角色可访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/list")
public String getUserList() {
return "用户列表(仅管理员可见)";
}
// 多个角色可访问
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
@GetMapping("/profile")
public String getProfile() {
return "个人资料";
}
}
3. 获取当前登录用户信息
java
运行
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/public")
public class PublicController {
// 获取当前登录用户信息
@GetMapping("/current-user")
public String getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
String role = authentication.getAuthorities().iterator().next().getAuthority();
return "当前登录用户:" + username + ",角色:" + role;
}
}
五、避坑指南
坑点 1:登录接口认证失败,提示 "用户不存在"
表现:输入正确用户名密码,仍提示用户不存在;✅ 解决方案:检查 CustomUserDetailsService 中数据库查询逻辑是否正确,确保用户名匹配规则一致,数据库密码已通过 BCryptPasswordEncoder 加密存储(避免明文密码无法匹配)。
坑点 2:JWT Token 解析失败,提示 "签名无效"
表现:前端携带 Token 请求接口,后端提示 Token 无效;✅ 解决方案:确保生成 Token 与解析 Token 使用的密钥(secret)一致,Token 格式正确(需包含 "Bearer" 前缀),避免 Token 被篡改或截断。
坑点 3:方法级权限注解(@PreAuthorize)失效
表现:添加注解后,无对应角色仍可访问接口;✅ 解决方案:确保配置类添加 @EnableMethodSecurity 注解,注解中的角色名称需与 UserDetails 中封装的角色一致(区分大小写)。
坑点 4:Security 上下文获取不到用户信息
表现:接口中通过 SecurityContextHolder 无法获取当前登录用户;✅ 解决方案:检查 JWT 过滤器是否正确执行,Token 是否有效且已解析,过滤器是否在 UsernamePasswordAuthenticationFilter 之前添加。
六、终极总结:接口安全的核心是「认证可靠 + 授权精准」
Spring Security + JWT 实战的核心,是通过 JWT 实现无状态认证,摆脱 Session 依赖,适配分布式场景;通过 Spring Security 实现细粒度授权,确保接口仅被合法用户访问。企业级开发中,需平衡「安全性」与「易用性」,同时规避常见安全风险。
核心原则总结:
- 密钥安全优先:JWT 密钥需复杂且加密存储,生产环境避免硬编码,通过配置中心管理;
- 权限粒度适配业务:根据业务需求选择路径级或方法级授权,避免过度授权或授权不足;
- Token 生命周期可控:合理设置 Token 过期时间,可实现 Token 刷新机制,避免频繁登录;
- 安全防护全面:结合 HTTPS 防止 Token 传输被窃取,关闭不必要的安全防护(如 CSRF)适配前后端分离场景。