认证(Authentication)vs 授权(Authorization)
这两个概念经常被混用,但它们解决的是完全不同的问题:
认证(Authentication)— 你是谁?
- 验证用户身份的过程
- 典型方式:用户名+密码、OAuth2、证书
- 结果:确认身份,颁发凭证(Token)
- Spring Security 对应:AuthenticationManager
授权(Authorization)— 你能做什么?
- 验证用户是否有权访问资源
- 基于角色(RBAC)或权限(ABAC)
- 结果:允许或拒绝访问
- Spring Security 对应:AccessDecisionManager
核心概念名词解释
SecurityFilterChain
Spring Security 5.x+ 的核心配置方式,替代了旧版 WebSecurityConfigurerAdapter。通过定义 SecurityFilterChain Bean 来配置哪些 URL 需要认证、使用什么认证方式、CORS/CSRF 等安全策略。
UserDetailsService
Spring Security 提供的接口,只有一个方法 loadUserByUsername(String username)。实现此接口来告诉 Spring Security 如何从数据库加载用户信息(用户名、密码哈希、角色列表)。
PasswordEncoder
密码编码器接口。BCryptPasswordEncoder 是最常用的实现,使用 bcrypt 算法对密码进行单向哈希,每次哈希时加入随机盐值,使得相同密码的哈希结果不同,有效防御彩虹表攻击。
JWT(JSON Web Token)
一种紧凑的、自包含的令牌格式,用于在各方之间安全传递信息。服务端签发 JWT 后不需要存储 Session,每次请求只需验证签名即可,实现无状态认证(Stateless Authentication)。
Claims(声明)
JWT Payload 中的键值对,包含用户信息。标准 Claims:sub(主题/用户ID)、iat(签发时间)、exp(过期时间)。自定义 Claims 可以包含 roles、email 等业务信息。
BCrypt
一种密码哈希算法,专为密码存储设计。其工作因子(cost factor)可调整,使得随着硬件性能提升,可以通过增加工作因子来保持暴力破解的计算成本。Spring Security 默认使用 cost=10。
RBAC(基于角色的访问控制)
Role-Based Access Control,将权限分配给角色,再将角色分配给用户。如 ROLE_ADMIN 拥有所有权限,ROLE_USER 只能访问自己的数据。Spring Security 通过 @PreAuthorize 等注解实现。
JWT 认证完整流程
┌─────────────────────────────────────────────────────────────────┐
│ JWT 无状态认证流程 │
└─────────────────────────────────────────────────────────────────┘
【登录阶段】
客户端
│ POST /auth/login { username, password }
▼
Spring Security(UsernamePasswordAuthenticationFilter)
│ loadUserByUsername → 从数据库加载用户
│ BCrypt.verify(inputPassword, storedHash)
▼
认证成功 → JwtService.generateToken(user)
│
│ 返回 { accessToken: "eyJ...", refreshToken: "eyJ..." }
▼
客户端存储 Token(localStorage 或 HttpOnly Cookie)
【请求阶段】
客户端
│ GET /api/users Header: Authorization: Bearer eyJ...
▼
JwtAuthenticationFilter(自定义 OncePerRequestFilter)
│ 1. 提取 Header 中的 Token
│ 2. JwtService.validateToken(token)
│ ├── 验证签名(防篡改)
│ ├── 检查过期时间
│ └── 解析 Claims
│ 3. 将用户信息放入 SecurityContext
▼
Controller 正常处理请求
│
▼
响应数据
JWT 结构解析
JWT 由三个 Base64URL 编码的部分组成,用 . 分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header(算法+类型)
.
eyJzdWIiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJST0xFX1VTRVIiXSwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9
← Payload(用户信息+过期时间)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
← Signature(HMAC-SHA256 签名)
Payload 解码后:{"sub":"123","email":"user@example.com","roles":["ROLE_USER"],"iat":1700000000,"exp":1700003600}
完整实现代码
Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
JwtService — JWT 工具类
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:3600}") // 默认 1 小时
private long expiration;
private SecretKey getKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getKey())
.compact();
}
public Claims extractClaims(String token) {
return Jwts.parser().verifyWith(getKey()).build()
.parseSignedClaims(token).getPayload();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractClaims(token).getSubject();
return username.equals(userDetails.getUsername())
&& !extractClaims(token).getExpiration().before(new Date());
}
}
SecurityFilterChain 配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启 @PreAuthorize 等方法级安全
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // JWT 无状态,禁用 CSRF
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/actuator/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // cost factor 默认 10
}
@Bean
public AuthenticationManager authManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}
// 自定义 JWT 过滤器
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
final String authHeader = req.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(req, res);
return;
}
final String jwt = authHeader.substring(7);
final String username = jwtService.extractClaims(jwt).getSubject();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(req, res);
}
}
方法级权限控制
@RestController
@RequestMapping("/admin")
public class AdminController {
// 只有 ADMIN 角色可以访问
@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public List<UserResponseDTO> listAllUsers() { ... }
// 管理员或本人才能查看
@GetMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public UserResponseDTO getUser(@PathVariable Long id) { ... }
}
Danger
密码必须使用 BCrypt 等不可逆哈希算法存储,绝对不能明文存储,也不能使用 MD5/SHA1 等普通哈希(易受彩虹表攻击)。BCryptPasswordEncoder 会在每次哈希时自动加随机盐值,即使两个用户密码相同,存储的哈希值也不同。
Warning
JWT Secret 必须足够长(至少 256 位/32字节),且不能硬编码在代码中。应通过环境变量注入:
JWT_SECRET=your-256-bit-secret-here,在 application.yml 中用 ${JWT_SECRET} 引用。