Spring Security上手踩坑实录:一个考公程序员的自救指南
上周五晚上十一点,我盯着屏幕上刺眼的403 Forbidden错误,差点把机械键盘砸了。这不是第一次被权限问题折磨——但这次真的不一样。领导临时拍板,要把我们内部管理系统开放给合作方使用,要求“下周上线”,还补了一句:“安全方面你看着搞,别出事就行。”
我?一个主业写业务逻辑、副业刷行测申论的Java后端,在上海合租屋里一边改bug一边背常识判断的打工人,现在要硬刚认证授权体系?
没办法,为了不耽误周末去图书馆刷题的时间,我只能速通Spring Security。还好有Claude在旁边当外挂(感谢GFW没完全封死),不然真得通宵。
说起来,我们团队之前的安全方案挺“野路子”:自己用JWT写了个拦截器,用户信息塞进ThreadLocal,权限校验靠if-else硬编码。双11期间差点因为token过期逻辑漏洞被渗透测试打穿——运维老哥当时看我的眼神,仿佛在看一个定时炸弹。
痛定思痛,技术债总得还。这次需求正好是个契机。可选方案其实不少:
- Shiro:轻量,上手快,但社区活跃度一般,文档老旧;
- 自研JWT+拦截器:熟悉但脆弱,扩展性差;
- Spring Security:重量级选手,生态强大,但学习曲线陡峭。
考虑到项目已经重度依赖Spring Boot,而且未来可能接入OAuth2、LDAP等企业级认证方式,最终还是咬牙选了Spring Security。毕竟——万一考上公务员,这段经历也能写进简历里“主导安全架构升级”嘛(笑)。
从零搭建:别被默认配置吓到
很多人一看到Spring Security就退缩,因为它默认开启CSRF、Session管理、表单登录……一堆功能一股脑全开,本地跑起来连接口都调不通。我当时就懵了:我只是想加个登录接口,怎么连Swagger都403了?
关键在于先禁用不需要的功能,快速跑通主流程。我的起步配置长这样:
@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/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic().disable()
.formLogin().disable();
return http.build();
}
}
注意几个关键点:
STATELESS会话策略:我们是纯API服务,不需要服务器维护Session;- 放行认证接口和Swagger:不然开发体验直接负分;
- 禁用CSRF和表单登录:RESTful API用不到这些。
这时候启动项目,你会发现除了/api/auth/**,其他接口都返回401。很好,第一步成功了——至少不是403了!
认证流程:从UsernamePasswordAuthenticationToken说起
Spring Security的核心抽象是Authentication对象。当你调用/login接口时,需要手动构造一个未认证的UsernamePasswordAuthenticationToken,交给AuthenticationManager去验证:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword())
);
// 验证通过后,生成JWT返回
String token = jwtUtil.generateToken(authentication);
return ResponseEntity.ok(new AuthResponse(token));
}
而AuthenticationManager背后干活的是UserDetailsService。你需要实现它,告诉框架“用户数据从哪来”:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepo.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 注意:数据库存的是BCrypt加密后的密码
.authorities("ROLE_USER") // 简化处理,实际应查角色表
.build();
}
}
这里有个坑:密码必须是BCrypt加密的!如果你数据库里存的是明文或者MD5,登录永远失败。Spring Security默认用DelegatingPasswordEncoder,会自动识别{bcrypt}...前缀。所以建用户时记得:
@Autowired
private PasswordEncoder passwordEncoder;
// 注册时
user.setPassword(passwordEncoder.encode(rawPassword));
我当时就忘了这茬,调试半小时才发现日志里默默报了Bad credentials——产品经理路过问“进度如何”,我只能强颜欢笑:“快了快了”。
权限控制:方法级注解才是生产力
接口级别的权限(比如/admin/**需要ADMIN角色)用authorizeHttpRequests()就能搞定。但更细粒度的控制,比如“只有订单创建者才能取消订单”,就得靠方法级安全注解了。
首先在配置类上加@EnableMethodSecurity(Spring Boot 3.x之后替代了旧的@EnableGlobalMethodSecurity):
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig { /* ... */ }
然后在Service方法上直接加注解:
@Service
public class OrderService {
@PreAuthorize("@orderService.isOwner(#orderId, principal)")
public void cancelOrder(Long orderId) {
// 取消逻辑
}
public boolean isOwner(Long orderId, Authentication auth) {
String currentUsername = auth.getName();
return orderRepository.findById(orderId)
.map(order -> order.getCreator().equals(currentUsername))
.orElse(false);
}
}
@PreAuthorize支持SpEL表达式,可以调用Bean方法、访问principal(当前用户)、甚至写复杂逻辑。这种灵活性比硬编码if-else优雅多了,也更容易单元测试。
不过要注意:方法级安全默认是基于代理的,如果同一个类里方法A调用带注解的方法B,权限检查不会生效!这是Spring AOP的经典陷阱。解决办法要么拆Service,要么用AopContext.currentProxy()——但后者很丑,建议重构。
生产环境避坑指南
1. JWT过期刷新机制
不要只发一个access token!一定要配refresh token,否则用户每小时重新登录,前端同事会提刀来找你。我们的做法是:
- access token有效期15分钟;
- refresh token有效期7天;
- 刷新时校验refresh token合法性,并颁发新的access token。
2. 密码策略别偷懒
上线前务必确认:
- 密码存储用BCrypt(强度10以上);
- 登录失败次数限制(防暴力破解);
- 敏感操作(如改密)需二次验证。
我们曾因没做失败锁定,被安全扫描工具标为高危漏洞。运维直接把报告甩到群里:“兄弟,这能上线?”
3. 日志与监控
在AuthenticationFailureHandler和AuthenticationSuccessHandler里埋点,记录登录行为。结合ELK,能快速发现异常IP爆破。
性能与架构考量
Spring Security本身性能开销极低——核心是过滤器链,每个请求多几毫秒。但你的实现方式会影响性能:
UserDetailsService.loadUserByUsername()被频繁调用,务必加缓存(比如Caffeine或Redis);- 权限计算避免N+1查询,
isOwner这类方法最好一次SQL查完; - JWT解析别每次读公钥,提前加载到内存。
另外,别把权限逻辑耦合到业务代码里。我们后来抽出了一个PermissionService,统一处理资源归属、角色校验,Controller层只调用checkCanEdit(userId, resourceId),清爽多了。
结语:安全不是功能,是底线
折腾两周后,系统顺利上线。合作方反馈“接口很规范”,领导夸“考虑周全”。其实我心里清楚:离真正的企业级安全还差得远——没做审计日志、没集成SSO、RBAC模型也简化了。但至少,我不再是那个随便写个拦截器就敢上线的愣头青了。
作为一个白天写Java、晚上刷申论的考公人,这次经历让我明白:技术深度和体制内追求的“稳妥”并不冲突。安全这件事,宁可多花三天,也不能留一分钟隐患。
对了,最近在刷《公共基础知识》,看到“网络安全法”章节时,莫名觉得亲切——毕竟,我也算是为网络空间清朗贡献过一行代码的人了(笑)。
如果你也在上海租房、加班、备考,不妨试试Spring Security。它可能不会帮你上岸,但至少能让你在岸上站得更稳一点。

评论 0