Appearance
第61课:Spring Security
🎯 学习目标
- 理解认证、授权、过滤器链、SecurityContext、UserDetailsService 的关系。
- 掌握 Spring Security 6 风格的
SecurityFilterChain配置。 - 能实现基于表单、HTTP Basic 或 JWT 的登录认证流程。
- 能使用密码加密、方法级权限和角色/权限模型。
- 能识别明文密码、CSRF 误用、JWT 密钥硬编码、权限边界混乱等风险。
📖 一、认证与授权
两个概念必须区分:
text
认证 Authentication:你是谁?
授权 Authorization:你能做什么?示例:
text
用户登录成功,系统确认他是 userId=1001,这是认证。
用户访问 /admin/users,系统判断他是否有 ADMIN 权限,这是授权。Spring Security 的核心是过滤器链。请求进入 Controller 前,会先经过多个安全过滤器。
📖 二、基本依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>加入依赖后,Spring Boot 默认保护所有接口,并生成临时密码。这是为了安全默认值。
📖 三、SecurityFilterChain
Spring Security 6 推荐配置方式:
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}REST API 如果使用 Token,通常关闭 CSRF;传统表单 + Cookie Session 应认真评估 CSRF 防护。
📖 四、UserDetailsService
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPasswordHash())
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getCode()))
.toList())
.accountLocked(user.isLocked())
.disabled(!user.isEnabled())
.build();
}
}密码必须存哈希:
java
String hash = passwordEncoder.encode(rawPassword);
boolean matches = passwordEncoder.matches(rawPassword, hash);不要存明文密码,也不要自己设计加密算法。
📖 五、JWT 认证流程
JWT 常用于前后端分离:
text
1. 用户提交用户名密码。
2. 后端验证成功后生成 JWT。
3. 前端后续请求带 Authorization: Bearer <token>。
4. 后端过滤器解析 token,构建 Authentication。
5. Controller 或方法权限基于 Authentication 判断。过滤器伪代码:
java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtService.validate(token)) {
Authentication authentication = jwtService.toAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}注册到过滤器链:
java
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);JWT 密钥、过期时间、刷新机制、黑名单、退出登录都需要设计。
📖 六、方法级权限
开启:
java
@EnableMethodSecurity使用:
java
@PreAuthorize("hasRole('ADMIN')")
public List<UserResponse> listAllUsers() {
return userService.findAll();
}java
@PreAuthorize("#userId == authentication.principal.userId or hasRole('ADMIN')")
public UserResponse getProfile(Long userId) {
return userService.getProfile(userId);
}方法级权限适合保护 Service 层敏感操作,不要只依赖前端隐藏按钮。
📖 七、角色与权限模型
常见模型:
text
User -> Role -> Permission示例:
text
USER
ROLE_ADMIN
user:create
user:delete
ROLE_AUDITOR
audit:readSpring Security 中:
text
hasRole('ADMIN') 实际匹配 ROLE_ADMIN
hasAuthority('user:delete') 精确匹配权限字符串角色适合粗粒度,权限适合细粒度。
📖 八、统一认证失败响应
REST API 不应该跳转登录页,而应该返回 JSON。
java
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"请先登录\"}");
}
}403 访问拒绝也应统一处理:
java
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":\"FORBIDDEN\",\"message\":\"无权限访问\"}");
}
}⚠️ 九、常见陷阱
1. 明文存密码
必须使用 BCrypt、Argon2 等成熟算法。不要 MD5 加盐后就自认为安全。
2. JWT 密钥硬编码
密钥应放环境变量、配置中心或密钥系统,并定期轮换。
3. 误关 CSRF
纯 Token REST API 通常可以关闭 CSRF;Cookie Session 表单应用不能无脑关闭。
4. 只做前端权限
前端隐藏按钮不是安全措施。后端必须校验权限。
5. 角色和权限混乱
ROLE_ADMIN 和 user:delete 应有清晰层级,不要随意混用。
🆚 十、Java vs C 对比
| 维度 | C Web 服务 | Spring Security |
|---|---|---|
| 认证 | 手写中间件/回调 | FilterChain 标准化 |
| 密码处理 | 手动调用加密库 | PasswordEncoder |
| 上下文 | 手动传递用户信息 | SecurityContext |
| 方法权限 | 手写判断 | @PreAuthorize |
| 扩展 | 函数链 | Filter、Provider、Handler |
Spring Security 很强,但学习成本也高,因为它把认证授权做成了一套完整框架。
💡 十一、最佳实践
- 密码只存哈希,不存明文。
- REST API 使用统一 401/403 JSON 响应。
- Token 密钥不硬编码。
- 权限必须在后端校验。
- 角色用于粗粒度,权限用于细粒度。
- 登录、刷新、退出、Token 失效策略要一起设计。
- 管理后台和普通 API 可以拆分不同 SecurityFilterChain。
- 安全配置写集成测试,避免误放开接口。
🔍 十二、自测问题
text
认证和授权有什么区别?
SecurityFilterChain 在请求流程中做什么?
UserDetailsService 负责什么?
PasswordEncoder 为什么不能省?
hasRole 和 hasAuthority 有什么区别?
JWT 退出登录为什么比 Session 复杂?
CSRF 什么情况下需要开启?
401 和 403 有什么区别?🧭 十三、安全设计清单
一个 REST API 项目至少检查:
text
登录接口是否限流?
密码是否使用 BCrypt 或 Argon2?
Token 是否有合理过期时间?
Refresh Token 是否可吊销?
退出登录是否能让 Token 失效?
管理接口是否只允许管理员访问?
方法级权限是否覆盖关键业务?
401/403 是否返回统一 JSON?
安全日志是否记录登录失败和越权访问?
生产密钥是否不在代码仓库中?安全不是最后上线前补一个配置,而是接口设计的一部分。
🧪 十四、实战案例:获取当前用户
业务代码经常需要当前登录用户:
java
public class SecurityUtils {
public static String currentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new BusinessException("UNAUTHORIZED", "请先登录");
}
return authentication.getName();
}
}更好的方式是封装自己的 Principal:
java
public record LoginUser(Long userId, String username, Set<String> permissions) {
}这样业务层可以使用稳定的用户上下文,而不是到处解析框架对象。
📌 十五、学习建议
建议分三步练习:
text
先做 HTTP Basic,理解过滤器链。
再做表单登录,理解 Session 和 CSRF。
最后做 JWT,理解无状态认证和 Token 失效问题。如果一开始直接复制 JWT 代码,很容易只会粘贴配置,不理解认证授权模型。
📚 十六、常见状态码
| 状态码 | 含义 | 场景 |
|---|---|---|
| 401 | 未认证 | 未登录、Token 无效、Token 过期 |
| 403 | 已认证但无权限 | 普通用户访问管理员接口 |
| 429 | 请求过多 | 登录限流、防爆破 |
| 500 | 系统异常 | 安全逻辑之外的未知错误 |
认证失败和授权失败必须区分。否则前端无法判断是跳登录页,还是显示“无权限”。
📌 十七、权限模型建议
权限字符串建议稳定、可读:
text
user:create
user:read
user:update
user:delete
order:refund
admin:dashboard:view角色只是权限集合,不要把所有判断都硬编码成角色。
权限设计应尽量稳定,因为权限字符串一旦被前端、网关、审计系统或脚本引用,随意改名会造成联动故障。
权限变更也应该进入发布记录。
🎓 小结
Spring Security 是 Java 后端安全的核心框架。它的重点不是背配置,而是理解过滤器链、认证对象、权限模型和失败处理。安全设计必须保守,任何“为了方便暂时放开”的配置都可能成为真实漏洞。