Spring Security 基础:快速搭建安全认证系统
上周五晚上十点半,我还在公司调一个奇怪的 403 错误。窗外国贸三期的灯光已经稀疏,而我的 MacBook 已经烫得可以煎蛋。作为刚背了 30 年房贷的北漂程序员,这种加班场景早已成为生活的一部分——毕竟,新房子的月供可不会因为我搞不定权限系统就自动减免。
入职新公司两个月,我被安排接手一个内部管理后台的重构工作。产品经理上周三甩过来一句话:“下周上线前必须加上登录和权限控制。” 我看了一眼日历,离 deadline 只有四天。行吧,那就 Spring Security 上吧。
其实我对 Spring Security 一直有点敬畏。以前在 GitHub 上看开源项目时,总觉得它的配置又臭又长,XML 和 Java Config 混在一起像一锅乱炖。但这次没得选——公司技术栈是纯 Java + Spring Boot,而且领导明确说:“别整花里胡哨的 OAuth2,先做个基础认证就行。”
被迫营业:为什么选择 Spring Security?
我知道有些团队会用 Shiro,或者干脆手撸一套 JWT。但考虑到我们已经有现成的用户表结构、团队里老哥们对 Spring 生态熟悉,再加上 Spring Security 的社区支持和文档成熟度(虽然有时候文档写得像绕口令),最终还是决定用它。
更重要的是,性能。我们这个后台虽然用户不多(也就几百个运营+BD),但接口响应时间要求很严——领导说“不能让用户觉得卡”,毕竟人家还要去抢下午茶的最后一个蛋挞。Spring Security 默认是基于 Filter Chain 的,只要配置得当,开销其实很小。实测下来,加了完整认证后,P99 延迟只增加了不到 5ms。
初探:从“Hello World”到能跑起来
我新建了一个 Spring Boot 2.7 项目(别问为啥不是 3.x,问就是公司统一版本)。先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动应用,访问任意接口,直接弹出一个丑到爆的登录页,用户名是 user,密码在控制台打印出来一串随机字符串。这显然不能上线——谁家产品敢让用户记一串 UUID 当密码?
于是,我开始自定义认证逻辑。
自定义 UserDetailsService:对接真实数据库
我们的用户信息存在 MySQL 里,表结构大概是这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| username | VARCHAR(50) | 登录名 |
| password | VARCHAR(100) | BCrypt 加密后的密码 |
| enabled | TINYINT | 是否启用(1=是) |
| role | VARCHAR(20) | 角色(admin / operator) |
注意:密码一定不要明文存储! 我们用的是 BCrypt,Spring Security 内置支持,连 encoder 都不用自己写。
接下来实现 UserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 注意:这里返回的是 org.springframework.security.core.userdetails.User
return User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 数据库里已经是 BCrypt 加密后的
.authorities("ROLE_" + user.getRole().toUpperCase())
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
这里有个坑:角色名必须加 ROLE_ 前缀,否则 @PreAuthorize("hasRole('ADMIN')") 会失效。我第一次没加,测试时疯狂 403,差点以为是缓存问题,查了半小时才意识到是命名规范的事。
配置 SecurityFilterChain:性能与安全的平衡
Spring Boot 2.7 开始推荐用 SecurityFilterChain 代替 WebSecurityConfigurerAdapter。我一开始还按老教程写,结果 IDE 直接标红,提示 deprecated。果然,技术债还得自己还。
核心配置如下:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 内部系统,且全是 JSON API,暂时关掉 CSRF
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,用 JWT 或 Token
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login").permitAll()
.requestMatchers("/actuator/**").permitAll() // 健康检查放行
.anyRequest().authenticated()
)
.httpBasic().disable() // 不用 Basic Auth
.formLogin().disable(); // 不用默认表单登录
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
重点说几个性能相关的决策:
- 关闭 CSRF:因为我们是纯 API 后端,前端通过 Axios 发请求,不涉及表单提交,CSRF 防护反而增加无谓开销。
- STATELESS 会话:避免服务器存储 session,减轻内存压力,也方便后续做集群。
- 精确放行路径:比如
/actuator/health必须放行,否则运维监控会报警。
登录接口:生成 Token(伪 JWT)
为了保持简单,我没有立刻上真正的 JWT 库(比如 jjwt),而是用一个临时方案:登录成功后返回一个 UUID 作为 token,存入 Redis,设置过期时间。
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
Authentication authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
if (authenticate.isAuthenticated()) {
String token = UUID.randomUUID().toString();
// 存入 Redis,30 分钟过期
redisTemplate.opsForValue().set("auth:" + token, request.getUsername(), 30, TimeUnit.MINUTES);
return ResponseEntity.ok(Map.of("token", token));
}
} catch (BadCredentialsException e) {
return ResponseEntity.status(401).body("用户名或密码错误");
}
return ResponseEntity.status(401).body("认证失败");
}
}
对应的过滤器负责从 Header 中取 token 并验证:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
String username = (String) redisTemplate.opsForValue().get("auth:" + token);
if (username != null) {
// 构造 Authentication 对象
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
别忘了把这个 filter 加到 Security 配置里:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
// ... 其他配置
}
性能压测:到底慢了多少?
我用 JMeter 模拟了 100 并发用户持续请求一个受保护接口:
| 场景 | 平均响应时间 (ms) | P99 (ms) | CPU 占用 |
|---|---|---|---|
| 无 Security | 12 | 28 | 15% |
| 加 Spring Security(Redis 验证) | 16 | 35 | 18% |
多出来的 4ms 主要花在 Redis 查询上。如果把 token 放进本地缓存(比如 Caffeine),还能再优化。不过目前来看,完全在可接受范围内——毕竟我们不是高并发电商系统,内部工具而已。
踩过的坑 & 经验总结
- 不要用
@EnableGlobalMethodSecurity除非你真的需要方法级安全。它会代理所有 Bean,带来额外性能损耗。我们只用了 URL 级权限控制,够用了。 - 调试时开启 DEBUG 日志:
logging.level.org.springframework.security=DEBUG,能清楚看到每个请求经过哪些 filter、为什么被拒绝。 - 测试账号要覆盖各种角色:有一次我只测了 admin,结果 operator 用户上线后全 403,被测试小姐姐追着问了半小时。
- GitHub 是好东西:遇到问题先搜
spring-security github issues,很多奇怪行为(比如重定向循环)早有人踩过坑。
最后:房贷还在,代码继续
折腾三天,终于在 deadline 前搞定。上线那天,运维小哥跑来问:“你这接口怎么突然要 token 了?” 我笑着说:“安全第一嘛,不然你半夜被叫起来修漏洞,房贷谁还?”
说到底,Spring Security 并不可怕。只要理解它的 Filter Chain 机制,合理配置,完全可以在保证安全的同时兼顾性能。对于像我这样的普通打工人来说,能快速交付、少背锅、按时下班,比什么都重要。
对了,我把这个最小可行的认证模块整理了一下,丢到了 GitHub 上:https://github.com/yourname/spring-security-starter(名字当然是假的,但你可以真去搜类似 repo)。欢迎 Star(虽然我也知道没人会 star 一个基础模板 😂)。
现在,我要去研究前端动画了——毕竟,登录页太丑,产品经理说要加个 loading 动效。唉,程序员的命,都是产品经理给的(狗头保命)。
P.S. 如果你也刚买房、刚入职、被 deadline 追着跑,别慌。一行行代码敲下去,总能搞定。毕竟,房子不会跑,bug 也不会。

评论 0