Spring Security 权限认证实战指南
Spring Security 是 Java 后端安全认证与授权的事实标准,也是 CSDN Java 板块的高频话题。前面讲过 Spring 核心 IoC/AOP,这篇专门讲安全:认证流程、过滤器链、JWT 集成、方法级权限、密码加密、OAuth2、常见坑,全程带 Spring Boot 代码。
一、Spring Security 是什么
1.1 定位
Spring Security 提供**认证(你是谁)和授权(你能干什么)**两大能力:
- 认证(Authentication):验证用户名密码、Token、OAuth 登录;
- 授权(Authorization):角色/权限控制(哪些接口谁能访问);
- 防护:CSRF、CORS、Session 固定、点击劫持。
1.2 为什么用它
- 业界标准(Spring 官方出品),生态完整;
- 过滤器链机制,扩展性强;
- 声明式安全配置,少写代码。
二、快速开始
2.1 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
加入依赖后默认行为 :所有接口都要登录,默认用户 user,密码在启动日志里(随机生成)。
2.2 基础配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 前后端分离关 CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录注册放行
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated() // 其余都要登录
)
.formLogin(form -> form.disable()) // 禁用表单登录
.httpBasic(basic -> basic.disable()); // 禁用 Basic
return http.build();
}
}
2.3 内存用户(测试用)
java
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder().encode("123456"))
.roles("ADMIN")
.build();
UserDetails user = User.withUsername("user")
.password(passwordEncoder().encode("123456"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
三、认证流程(过滤器链)
3.1 核心过滤器链
请求
→ SecurityContextPersistenceFilter(恢复上下文)
→ UsernamePasswordAuthenticationFilter(表单登录)
→ BasicAuthenticationFilter(Basic 认证)
→ BearerTokenAuthenticationFilter(JWT/OAuth2 Token)
→ AnonymousAuthenticationFilter(匿名)
→ ExceptionTranslationFilter(异常转 401/403)
→ AuthorizationFilter(授权判断)
→ 业务接口
记住 :请求先过认证过滤器,再过授权过滤器。
3.2 认证核心组件
| 组件 | 作用 |
|---|---|
| Authentication | 认证信息(未认证/已认证) |
| AuthenticationManager | 认证入口 |
| AuthenticationProvider | 具体认证逻辑(查库、校验密码) |
| UserDetailsService | 按用户名查用户 |
| SecurityContext | 当前登录上下文 |
| SecurityContextHolder | 保存上下文的容器 |
3.3 从数据库认证(生产标准)
java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword()) // 存的是 BCrypt 密文
.roles(user.getRole()) // "ADMIN" / "USER"
.build();
}
}
四、JWT 集成(前后端分离标准方案)
4.1 为什么用 JWT
- 无状态:服务器不存会话,适合分布式/微服务;
- 自包含:Token 里带用户信息和过期时间;
- 跨域友好:放在 Authorization 头。
4.2 JWT 结构
Header(算法).Payload(claims).Signature(签名)
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.签名
- Header:算法 HS256;
- Payload:sub(用户名)、iat(签发时间)、exp(过期时间)、角色;
- Signature:Header + Payload + 密钥签名------防篡改。
4.3 登录接口生成 Token
java
@Service
public class AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
public String login(LoginDTO dto) {
// 1. 认证(校验用户名密码)
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
dto.getUsername(), dto.getPassword()));
// 2. 认证成功 → 生成 token
UserDetails user = (UserDetails) auth.getPrincipal();
return jwtUtil.generateToken(user.getUsername());
}
}
java
// JwtUtil(简化版,生产用 jjwt 库)
@Component
public class JwtUtil {
private static final SecretKey KEY =
Keys.hmacShaKeyFor("your-secret-key-please-change-it-32bytes".getBytes());
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.claim("role", "ADMIN")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1h
.signWith(KEY)
.compact();
}
public String parseUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(KEY)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
4.4 JWT 过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
String username = jwtUtil.parseUsername(token);
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
var auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// token 无效:不设置认证,走匿名
}
}
chain.doFilter(request, response);
}
}
4.5 注册过滤器
java
// 在 SecurityConfig 里加
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
五、方法级权限
5.1 开启
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级权限
public class SecurityConfig { }
5.2 注解使用
java
@RestController
public class OrderController {
@GetMapping("/orders")
@PreAuthorize("hasRole('USER')") // 需要 USER 角色
public List<Order> myOrders() { ... }
@DeleteMapping("/orders/{id}")
@PreAuthorize("hasRole('ADMIN')") // 需要 ADMIN 角色
public void deleteOrder(@PathVariable Long id) { ... }
@GetMapping("/profile")
@PreAuthorize("hasAuthority('user:read')") // 按权限点
public Profile profile() { ... }
}
5.3 常用表达式
| 表达式 | 含义 |
|---|---|
| hasRole('ADMIN') | 有 ADMIN 角色 |
| hasAnyRole('ADMIN','USER') | 任一角色 |
| hasAuthority('user:read') | 有某权限点 |
| #id == authentication.principal.id | SpEL 比对参数 |
| isAuthenticated() | 已登录 |
| permitAll() / denyAll() | 全放行/全拒绝 |
5.4 拿到当前用户
java
@GetMapping("/me")
public User me() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return userService.findByUsername(auth.getName());
}
// 或参数注入
@GetMapping("/me2")
public User me2(@AuthenticationPrincipal UserDetails user) {
return userService.findByUsername(user.getUsername());
}
六、密码安全
6.1 BCrypt(默认推荐)
java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 自动加盐,不可逆
}
// 注册时加密存储
user.setPassword(passwordEncoder.encode(rawPassword));
// 登录时校验
boolean ok = passwordEncoder.matches(rawPassword, storedHash);
6.2 为什么不用 MD5/SHA
- MD5/SHA 是"摘要"不是"密码哈希"------无盐可查彩虹表;
- BCrypt 内置随机盐 + 计算慢(防暴力);
- 现代替代:scrypt、Argon2。
七、OAuth2 / 第三方登录(可选)
7.1 概念
OAuth2 让用户"授权"第三方应用访问其资源,而不交出密码。
用户 → 授权服务器(微信/GitHub)→ 授权码
→ 应用服务器用授权码 + client_secret 换 token
→ 用 token 调资源接口拿用户信息
7.2 Spring Boot 集成
yaml
spring:
security:
oauth2:
client:
registration:
github:
client-id: xxx
client-secret: xxx
java
http.oauth2Login(oauth -> oauth
.defaultSuccessUrl("/", true));
企业内常用:OAuth2 + 微信/钉钉扫码登录(前面有钉钉插件能力)。
八、常见坑速查
| 现象 | 原因 | 解决 |
|---|---|---|
| 加了依赖全站要登录 | 默认安全策略 | 配置 permitAll |
| 密码错误但明明对 | 密码不是 BCrypt | 用 encoder.matches |
| 登录后 403 | 角色前缀问题 | hasRole 自动加 ROLE_ |
| JWT 过滤器不执行 | 没注册 | addFilterBefore |
| 自定义接口 404 | 放行了但没权限 | 检查 matcher 顺序 |
| 改配置不生效 | 缓存 | 重启/清缓存 |
| 前后端分离 403 | CSRF | 关 csrf 或配白名单 |
| 方法权限不生效 | 没开 @EnableMethodSecurity | 开启注解 |
本章小结
Spring Security 的心智模型:过滤器链做认证,AuthorizationFilter 做授权,SecurityContext 存当前用户 。前后端分离项目的标准组合 = JWT 过滤器 + BCrypt 密码 + 方法级 @PreAuthorize。记住角色有 ROLE_ 前缀、密码必须 BCrypt、Token 必须有过期时间这三个要点,就能避免 90% 的坑。
下一篇讲 FastAPI------Python 后端开发的高效选择。
(全文完)