Spring Security 基础:快速搭建安全认证系统,一个北漂程序员的实战记录
上周五晚上十点半,我坐在工位上盯着屏幕上那条 403 Forbidden 的报错,心里直冒火。房贷刚还完这个月的,老板又催着上线新功能——用户权限模块得在下周一前搞定。偏偏产品经理昨天还跑来说:“能不能支持微信扫码登录?顺便加个角色分级?” 我差点把键盘砸了。
作为一个早起型选手(每天8点准时坐到工位,不然通勤地铁挤到怀疑人生),我已经连续三天边刷 LeetCode 边改权限逻辑了。跳槽面试官最爱问“你用过哪些安全框架”,结果我连 Spring Security 都没正经搭过…… 被逼无奈,只能硬着头皮啃文档、扒源码,好歹赶在周末前把基础认证搞定了。今天这篇就当是复盘笔记,也给同样被权限问题折磨的兄弟们省点时间。
起因:不是我想学,是需求追着我跑
事情要从公司最近做的内部管理系统说起。我们是个小厂,团队不到20人,后端就仨Java开发(包括我)。之前为了赶双11大促,权限这块直接用最原始的方式:每个接口手动 check userId + role。代码丑得像泡面桶堆成的塔,测试一测就崩,运维天天在群里@我:“又有人越权访问了!”
上周产品提了个需求:接入第三方登录 + 多角色细粒度控制。我第一反应是“这不就是 Spring Security 的主场吗?” 但说实话,以前光听说过,没真用过。看网上教程一堆 WebSecurityConfigurerAdapter(现在都废弃了!),配置复杂得像在解魔方。更气人的是,隔壁组用 Go 写的微服务,鉴权直接用中间件几行搞定,而我还在纠结 AuthenticationManager 到底怎么注入……
真实场景吐槽:Go 组的同事甚至用爬虫抓我们系统的 API 文档,说“你们 Java 的权限模型太黑盒了,不如我们 Go 的清晰”。我当场反呛:“你们那是没遇到复杂 RBAC!” —— 其实心里慌得一批。
开干:从零搭建,别怕踩坑
第一步:别信过时教程!
Spring Security 5.7+ 已经移除了 WebSecurityConfigurerAdapter,但 80% 的中文博客还在教这个。我一开始照着抄,结果启动直接报错:
Consider defining a bean of type 'org.springframework.security.authentication.AuthenticationManager'
查了一晚上 Stack Overflow 才知道,现在要用 SecurityFilterChain 的方式配置。血泪教训:永远看官方最新文档,别信 CSDN 三年前的帖子。
第二步:建表设计,别偷懒
权限系统的核心是数据模型。我参考了 RBAC(Role-Based Access Control)标准,建了四张表:
| 表名 | 说明 |
|---|---|
sys_user |
用户基本信息(含密码哈希) |
sys_role |
角色表(如 ADMIN, USER) |
sys_permission |
权限点(如 user:read, order:create) |
user_role, role_permission |
两张关联表 |
开发心得:千万别把权限字符串直接存在用户表里!后期改权限要哭死。我们之前就这么干过,改个权限得全量 update 用户表,DBA 直接找我喝茶。
密码存储必须用 BCryptPasswordEncoder,别学某些老项目用 MD5(对,说的就是我司三年前的祖传代码)。
第三步:核心配置,一行都不能少
新建 SecurityConfig.java,这才是现代 Spring Security 的正确打开方式:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离可关掉,但生产环境慎用!
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
关键点解释:
STATELESS:因为是前后端分离,用 JWT 无状态认证addFilterBefore:自定义 JWT 过滤器放在默认用户名密码过滤器之前permitAll():登录和公开接口放行
第四步:自定义 JWT 认证过滤器
这才是重头戏。我写了个 JwtAuthenticationFilter,核心逻辑:
- 从 Header 取
Authorization: Bearer <token> - 解析 token,验证签名和过期时间
- 如果有效,构造
UsernamePasswordAuthenticationToken放入 SecurityContext
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
// 从数据库加载用户详情(含权限)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
踩坑记录:最开始我忘了
setDetails(),导致某些安全上下文信息丢失,单元测试全挂。后来翻源码才发现WebAuthenticationDetails会存客户端 IP 和 session ID,有些审计日志依赖这个。
性能与生产环境考虑
数据库查询优化
每次请求都要查用户+角色+权限?那 DB 不就炸了?我的方案:
- 缓存用户权限:登录成功后,把用户权限列表塞进 Redis,key 为
auth:permissions:{userId} - JWT 携带最小信息:token payload 只存
userId和exp,权限信息走缓存 - 懒加载:首次访问受保护接口时才查权限,后续走缓存
// UserServiceImpl.java
public List<String> getPermissions(Long userId) {
String cacheKey = "auth:permissions:" + userId;
List<String> perms = redisTemplate.opsForList().range(cacheKey, 0, -1);
if (perms == null || perms.isEmpty()) {
perms = permissionMapper.selectByUserId(userId);
redisTemplate.opsForList().leftPushAll(cacheKey, perms);
redisTemplate.expire(cacheKey, 30, TimeUnit.MINUTES); // 缓存30分钟
}
return perms;
}
接口设计规范
和前端约定好:
- 登录成功返回
{ token: "xxx", userInfo: { id, name, roles } } - 所有受保护接口,前端必须在 Header 带
Authorization: Bearer <token> - 权限不足时,后端统一返回
403 { code: 40301, msg: "权限不足" }
这样前端才能做统一拦截处理,不用每个页面单独判断。
和 Go 爬虫组的“技术交流”
说回开头那个 Go 组的同事。其实他们最近也在做权限系统,但他们用 Go 写了个轻量级中间件:
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !validateToken(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
确实简洁!但当我问他:“如果要实现‘部门经理只能看本部门数据’这种动态权限呢?” 他愣住了。Spring Security 的 @PreAuthorize("hasPermission(#orderId, 'ORDER', 'READ')") 这种表达式级别的控制,Go 中间件就得自己造轮子。
开发心得:框架不是银弹,但复杂业务场景下,Spring Security 的扩展性真的香。当然,如果是简单 API 网关鉴权,Go 确实更快更轻。
最后:跳槽、房贷与技术成长
折腾完这套权限系统,我不仅按时交付了需求(老板终于没在周会上点名批评),还顺手整理了一份 Spring Security 面试八股文。最近面试了几家大厂,聊到安全模块时底气足多了——毕竟亲手踩过坑的人,和只会背概念的人,聊起来完全不是一个 level。
虽然每天早起挤地铁、晚上回家还得刷题的日子很苦,但看着账户里慢慢增长的技术积累,感觉房贷的压力也没那么窒息了。程序员这行,说到底还是靠解决问题的能力吃饭。Spring Security 看似复杂,拆开来看也就是:认证(你是谁) + 授权(你能干啥) + 安全上下文(记住你是谁)。
下次再有人问我“Java 权限怎么搞”,我就能甩出这篇实战记录,而不是尴尬地说“我用拦截器手写的”。
附:常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 登录成功但访问接口 403 | 权限未加载到 SecurityContext | 检查 UserDetails.getAuthorities() 是否返回非空 |
| JWT 过期后仍能访问 | 过滤器未校验 token 有效性 | 在 JwtAuthenticationFilter 中调用 jwtUtil.validateToken() |
| 自定义 UserDetailsService 不生效 | 未注入到 AuthenticationProvider | 确保 DaoAuthenticationProvider 设置了 userDetailsService |
| 跨域请求被拦截 | CSRF 未关闭或 CORS 配置缺失 | 添加 .cors().and().csrf().disable() |
友情提示:生产环境千万别关 CSRF!除非你确定是纯 API 服务且有其他防护措施。
写完这篇已经是凌晨一点,明天还要早起改 Bug。但至少,今晚能睡个踏实觉了——毕竟,权限系统没崩,房贷还能继续还。

评论 0