Spring Security 基础:快速搭建安全认证系统
上周五晚上 10 点半,我正瘫在出租屋里刷 B 站,突然收到 Leader 的微信:“明早站会前搞个基础的登录认证,别整太复杂,但得能跑。” 我盯着消息愣了两秒——这不就是典型的“简单需求”吗?字节人谁不懂,产品经理嘴里的“简单”,往往意味着你得通宵。
我是字节跳动基础架构组的一名后端开发,5 年经验,日常搬砖内容包括但不限于:K8s 调度器魔改、中间件性能优化、以及偶尔被拉去救火。最近还在偷偷卷 AI,想着万一哪天大模型把 CRUD 都干了,我还能靠 prompt engineering 混口饭吃。不过眼下,还是得先搞定这个“简单”的认证系统。
事情起因是团队要给一个内部工具加上用户登录功能。之前这玩意儿直接裸奔,谁拿到 URL 都能进,测试同学上周误删了生产配置,差点让双 11 的流量调度策略崩掉。运维大哥当场黑脸,甩出一句:“再没权限控制,我就把你们服务从网关里摘了。”
于是,轮到我上场了。
为什么选 Spring Security?
其实我们内部有统一的 IAM(Identity and Access Management)平台,但接入成本高、流程长,还得排队等安全团队审核。Leader 说了:“先搞个 MVP,能拦住外人就行,后续再对接统一认证。”
MVP 意味着快、稳、少写代码。Spring Boot + Spring Security 几乎是 Java 后端的默认选项。它抽象了认证(Authentication)和授权(Authorization)的核心逻辑,而且社区文档丰富,GitHub 上一堆 demo,连我这种平时只看源码不写业务的人都能快速上手。
更重要的是——简历上能写。别笑,这很真实。跳槽面试时,如果你说“用 Spring Security 做了 RBAC 权限控制”,至少说明你不是只会调接口的“API Boy”。在如今这行情下,“资源”管控能力几乎是每个中高级岗位的硬性要求。
动手:5 分钟搭个能跑的认证
先理清需求:
- 用户通过用户名/密码登录
- 登录后返回 JWT Token
- 后续请求携带 Token 才能访问受保护的资源
- 管理员和普通用户权限不同
第一步:依赖安排上
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
别小看这两行,去年有个实习生漏加 jjwt,结果线上登录接口 500,报错 ClassNotFoundException: io.jsonwebtoken.Jwts,被测试追着问了三天。
第二步:UserDetails & UserDetailsService
Spring Security 要求你提供一个 UserDetailsService,用来加载用户信息。我建了个简单的 User 实体:
public class User {
private Long id;
private String username;
private String password; // 实际项目务必加密!
private String role; // "ADMIN" or "USER"
}
然后实现 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("User not found");
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities("ROLE_" + user.getRole())
.build();
}
}
注意:这里我把角色转成了 ROLE_ADMIN 格式,因为 Spring Security 默认的 hasRole('ADMIN') 会自动加前缀。不然你会踩坑,比如写 hasRole('ROLE_ADMIN') 反而失效。
第三步:JWT 工具类
为了无状态认证,我们用 JWT。写个工具类生成和解析 Token:
public class JwtUtil {
private String secret = "mySecretKey"; // 生产环境务必用强密钥+配置中心管理!
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24h
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
血泪教训:千万别把 secret 写死在代码里!我们曾因 hardcode 密钥导致安全扫描告警,被安全团队 call 进会议室“喝茶”。
第四步:Security 配置
重头戏来了。继承 WebSecurityConfigurerAdapter(虽然已 deprecated,但在 Spring Boot 2.x 仍是主流),配置过滤器链:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 别再用 MD5 了兄弟!
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(authenticationManagerBean()),
UsernamePasswordAuthenticationFilter.class);
}
}
关键点:
- 关闭 CSRF(因为是 stateless API)
- 设置 session 为 STATELESS
/auth/login公开,/admin/**仅 ADMIN 可访问- 加入自定义的
JwtAuthenticationFilter
第五步:登录接口 & Filter
登录 Controller 很简单:
@PostMapping("/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
} catch (BadCredentialsException e) {
return ResponseEntity.status(401).body("Invalid credentials");
}
}
而 JwtAuthenticationFilter 负责从 Header 中提取 Token,验证并设置 SecurityContext:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// ... 构造注入 jwtUtil, userDetailsService
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
踩过的坑 & 生产建议
密码加密:本地测试用
{noop}能跑,但上生产必须用BCryptPasswordEncoder。我们有一次压测,发现明文密码库被 DBA 发现,差点被当成安全事件上报。Token 刷新机制:上面 demo 没做 refresh token。真实场景建议加上,否则用户每 24 小时就得重新登录,体验极差。
权限粒度:
hasRole只适合粗粒度控制。如果要做数据级权限(比如“只能看自己创建的资源”),得结合@PreAuthorize和自定义表达式。日志与监控:记录失败登录尝试,防暴力破解。我们接入了公司内部的风控系统,连续 5 次失败就封 IP。
资源隔离:不同角色能访问的“资源”必须严格划分。比如
/api/v1/orders/{id},普通用户只能查自己的订单 ID,这块光靠 Spring Security 不够,得在 Service 层二次校验。
效果 & 心得
第二天站会前,我提交了 PR。测试同学跑完冒烟测试,居然一次过。Leader 看了眼代码,点点头:“行,先上线灰度。” 当晚部署后,再也没人能随便进那个内部工具了。
回头想想,Spring Security 虽然配置有点“魔法”,但一旦摸清套路,搭认证系统真的很快。更重要的是,它强迫你思考“谁可以访问什么资源”——这是构建安全系统的基石。
现在我的简历里又多了一行:“基于 Spring Security 实现 JWT 无状态认证,支持 RBAC 权限模型”。虽然听起来平平无奇,但在面试时,只要能讲清楚背后的线程安全(SecurityContext 是 ThreadLocal)、过滤器顺序、以及如何防御常见攻击(比如 Token 泄露),基本就能碾压 80% 的候选人。
最后送大家一句我在代码人生中学到的道理:安全不是功能,而是基础设施。别等到线上被黑了才想起加认证——那时候,你的简历可能已经在 HR 那里被打上了“风险”标签。
(完)
P.S. 本文所有代码已在 GitHub 开源,搜 “bytearch-spring-security-demo” 即可。顺便,招人,字节基础架构组,上海,急缺懂安全又会调优的后端,简历砸过来~

评论 0