从零搭建认证系统:Spring Security真的比Shiro香吗?
大家好,我是小K,两个月前刚加入这家上市公司的技术中台团队。说实话,入职第一天就被拉进一个“紧急重构”项目——把老掉牙的登录模块干掉,换成现代安全架构。产品经理上周五下班前甩过来一句话:“下个月上线,不能出问题。”我盯着那行字,心里一万只草泥马奔腾而过。
不过吐槽归吐槽,活儿还得干。作为一个对前端动画情有独钟(可惜现在天天写后端)的码农,我其实一直对安全这块有点发怵。以前在小公司用过 Apache Shiro,觉得挺顺手;但新东家是正经上市公司,合规、审计、多租户……一套套下来,光听运维大哥念叨“等保三级”我就头皮发麻。
所以这次领导拍板:上 Spring Security。理由很直接——生态完善、社区活跃、和 Spring Boot 天然集成。但问题是,市面上关于它的教程要么太浅(Hello World 级别),要么太深(源码级剖析),中间缺个“打工人速成指南”。正好借这个机会,把自己踩过的坑、做的技术选型对比、以及为什么最终放弃其他方案的原因写下来,也算给后来人铺点路。
顺便说一句,最近在刷招聘网站,发现不少岗位要求“熟悉 Spring Security”,甚至有些区块链项目也招 Java 安全工程师(虽然我怀疑他们是不是只是想蹭热点)。可见这玩意儿,真不是可有可无的玩具,而是求职市场的硬通货。
为啥不用 Shiro?也不是 Python 的 FastAPI?
先说结论:不是 Shiro 不好,而是它在复杂企业场景下显得“力不从心”。
我们内部做过一轮技术选型评估,主要对比了三个方向:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Spring Security | 与 Spring 生态无缝集成,OAuth2/JWT 支持完善,权限模型灵活,社区强大 | 学习曲线陡峭,配置稍显复杂 | 中大型企业、微服务架构、需对接第三方认证(如微信、钉钉) |
| Apache Shiro | API 简洁,上手快,轻量 | 社区活跃度下降,微服务支持弱,OAuth2 需额外扩展 | 中小型单体应用、内部工具系统 |
| Python (FastAPI + Authlib) | 开发效率高,异步友好,适合快速原型 | 公司技术栈以 Java 为主,运维体系不兼容,缺乏成熟的企业级审计能力 | AI/数据分析平台、实验性项目 |
看到最后那个 Python 选项,可能有人会问:你们公司不是搞区块链相关业务吗?怎么不用 Python 写智能合约配套服务?
哈哈,这里得澄清一下。我们确实有区块链实验室,但他们主攻底层共识算法和跨链协议,后端服务还是 Java 扛大旗。至于 Python,更多用于数据清洗和链上分析脚本——安全认证这种核心模块,谁敢轻易换语言?运维大哥第一个提刀上门。
而且,FastAPI 虽然香,但在我们这种强合规环境下,连日志格式都要符合 ISO 标准,更别说权限审计了。Spring Security 的 @PreAuthorize 和方法级安全,配合 AOP 做操作留痕,简直是为审计而生。
所以,尽管我内心还惦记着用 Python 写个 Flask 小玩具,现实还是把我按回了 Java 的工位上。
快速搭建:三步走战略
既然定了 Spring Security,那就开干。我们的目标很明确:一周内跑通账号密码登录 + JWT 令牌 + 接口权限控制。别笑,这在我们这儿已经算“敏捷”了——上次有个需求改个按钮颜色,PR 走了三天审批。
第一步:基础依赖与结构设计
首先,在 pom.xml 加上核心依赖(注意版本对齐!):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</td>
</tr>
<tr>
<td><code>spring-security-oauth2-resource-server</code></td>
<td>用于解析 JWT 令牌</td>
</tr>
<tr>
<td><code>jjwt-api</code> / <code>jjwt-impl</code></td>
<td>生成和解析 JWT(别用旧版,坑多)</td>
</tr>
</tbody>
</table>
数据库设计上,我们沿用了经典的 RBAC 模型:
- <code>sys_user</code>:用户表(含 username, password, enabled)
- <code>sys_role</code>:角色表
- <code>sys_permission</code>:权限表(如 <code>user:read</code>, <code>order:create</code>)
- 中间表关联用户-角色、角色-权限
> 💡 **血泪教训**:千万别把权限字符串直接存用户表!后期加角色、改权限会疯掉。我们之前有个老系统就这么干的,每次产品说“加个导出功能”,DBA 就要批量 update 上万条记录……
### 第二步:自定义 UserDetailsService
Spring Security 默认用内存用户,显然不能上生产。我们实现了自己的 `UserDetailsService`:
```java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 从 DB 加载权限列表,转成 GrantedAuthority
List<GrantedAuthority> authorities = buildAuthorities(user.getRoles());
return org.springframework.security.core.userdetails.User
.builder()
.username(user.getUsername())
.password(user.getPassword()) // 注意:必须是 BCrypt 加密后的
.authorities(authorities)
.accountExpired(!user.isEnabled())
.build();
}
private List<GrantedAuthority> buildAuthorities(List<Role> roles) {
Set<String> permissions = new HashSet<>();
for (Role role : roles) {
permissions.addAll(role.getPermissions().stream()
.map(Permission::getCode)
.collect(Collectors.toSet()));
}
return permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
这里有个细节:密码必须用 BCrypt 加密。别偷懒用 MD5,现在连 CTF 比赛都不考 MD5 碰撞了好吗!
第三步:JWT 认证过滤器
Spring Security 默认用 Session,但我们是前后端分离 + 微服务,必须上无状态 JWT。于是自定义一个过滤器:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractTokenFromHeader(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
private String extractTokenFromHeader(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
然后在 SecurityConfig 里注册它:
@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("/auth/login").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
⚠️ 线上事故预警:千万别忘了
.csrf().disable()!我们测试环境一开始没关,前端 POST 登录直接 403,还以为是 Nginx 配置错了,查了两小时……
权限控制:从 URL 到方法
搞定认证只是第一步,真正的难点在授权。
我们采用 双层权限控制:
- URL 级:粗粒度拦截(如
/api/admin/**只允许 ADMIN 角色) - 方法级:细粒度控制(如
@PreAuthorize("hasPermission('order','delete')"))
URL 配置很简单:
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
但方法级才是王道。比如删除订单接口:
@DeleteMapping("/orders/{id}")
@PreAuthorize("@permissionService.hasPermission(authentication, 'order', 'delete')")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.delete(id);
return ResponseEntity.noContent().build();
}
这里的 @permissionService 是我们自定义的 Bean,会根据当前用户、资源类型、操作类型动态判断。好处是:权限逻辑和业务代码解耦,产品哪天说“运营也能删订单”,改一行配置就行,不用动代码。
性能与监控:别让安全拖垮系统
安全模块最容易成为性能瓶颈。我们做了几件事:
- 缓存 UserDetails:用户登录信息缓存 30 分钟,避免每次请求都查 DB。
- JWT 本地校验:公钥放在 configmap,不走远程验证。
- 关键操作埋点:登录失败、权限拒绝等事件上报到 ELK,方便审计。
运维同事还专门配了 Grafana 面板,监控:
- 每分钟认证请求数
- JWT 过期率
- 权限拒绝次数(异常飙升可能意味着攻击)
上周就靠这个发现了个脚本小子在暴力破解测试账号,IP 直接被防火墙 ban 了。
写在最后:安全不是功能,是底线
折腾两周后,新认证系统终于上线。虽然过程中被产品经理催了八百遍,被测试提了三十多个“边界 case”,但看到登录成功那一刻返回的 200 OK,心里还是有点小骄傲。
回头想想,Spring Security 确实比 Shiro “重”,但它的严谨和扩展性,在企业级场景下是值得的。就像我们 CTO 说的:“你可以没有 fancy 的功能,但不能没有安全的底线。”
顺便,如果你也在看新工作,不妨把 Spring Security 当成跳槽加分项。现在连做区块链钱包后台的公司都在招懂 OAuth2 的 Java 工程师——毕竟,再炫酷的 DApp,登录不上也是白搭。
好了,今天就水到这里。我去改下一个需求了,听说又要对接钉钉扫码登录……(叹气)
P.S. 如果你也在用 Spring Security,欢迎留言交流踩坑经验。要是能顺便安利点前端动画库,那就更好了 😄

评论 0