Spring Security 从零搭到上线,我熬了三个通宵
凌晨两点,公司只剩我和运维老张。咖啡机早就罢工了,我只能靠红牛续命。这已经是本周第三次加班到这个点了——产品那边临时加了个需求:下周一上线新后台系统,必须带完整的权限控制。而我们的后端还是裸奔状态,连个登录都没有。
作为一个在这家电商公司干了三年多的老油条,我对这种“周一上线”的 deadline 早就免疫了。但这次有点不一样:技术栈强制用 Spring Boot,安全模块指定用 Spring Security。说实话,我之前主要搞 Python 后端(Django + FastAPI 那一套玩得飞起),Java 虽然会写,但 Spring Security 这玩意儿文档又臭又长,配置项比 Vim 的 .vimrc 还复杂。
不过,既然要跳槽前最后冲一波 KPI,那就干吧。
为什么不用现成的?因为产品经理说“要定制”
一开始我想偷懒,直接套个 Keycloak 或 Auth0。结果 PM 一句“我们要自己的用户体系,而且权限粒度要到按钮级别”直接把我打回原形。行吧,自己造轮子。
Spring Security 其实挺强大的,但上手门槛高得离谱。光是 WebSecurityConfigurerAdapter 这个类(虽然现在 deprecated 了)就让我翻了半小时源码。更别说 OAuth2、JWT、RBAC 这些概念堆在一起,像极了产品经理画的原型图——看起来很美,实现起来想砸键盘。
不过好在我最近在学 Rust,对“显式优于隐式”这套哲学理解更深了。Spring Security 虽然啰嗦,但每一步都是可控的。只要理清流程,其实没那么可怕。
核心思路:认证 + 授权 + 资源保护
我把整个安全体系拆成三块:
- 认证(Authentication):你是谁?用户名密码 or Token?
- 授权(Authorization):你能干啥?角色 or 权限?
- 资源保护(Resource Protection):哪些接口需要保护?哪些公开?
这三块对应到代码里,就是配置类 + UserDetailsService + 权限注解。
先说数据库设计。我们没用 Spring Data JPA 那套自动建表,而是手动建了三张表:
users:存用户基本信息roles:角色表(admin, editor, viewer)user_roles:关联表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN DEFAULT true
);
CREATE TABLE roles (
id BIGINT PRIMARY KEY,
name VARCHAR(20) NOT NULL -- e.g., ROLE_ADMIN
);
CREATE INSERT INTO roles (id, name) VALUES (1, 'ROLE_ADMIN'), (2, 'ROLE_EDITOR');
注:密码字段存的是 BCrypt 加密后的字符串,别学某些人明文存密码,那真的会被开除。
实战:一行配置都不能少
我新建了一个 SecurityConfig.java,继承 WebSecurityCustomizer(新版推荐方式):
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离项目,关掉 CSRF
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter();
}
}
关键点:
- 关闭 CSRF:因为我们是纯 API 服务,前端用 Axios 发请求,不走表单提交。
- Session 策略设为 STATELESS:无状态,全靠 JWT。
/api/auth/**公开,其他全部拦截。- 自定义
JwtAuthFilter插在认证过滤器前面。
这里踩了个大坑:一开始忘了 .and() 链式调用,结果权限规则没生效,测试同学直接提了个 P0 Bug:“未登录能删商品?!”。吓得我赶紧回滚,还好没上生产。
用户认证:从数据库查,不是硬编码
很多人 demo 里直接写死用户名密码,比如:
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("123456")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
这在生产环境等于自杀。我们当然要接真实数据库。
于是我写了 CustomUserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity 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()) // 已经是 BCrypt 加密
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList()))
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
注意:password 字段必须是 BCrypt 格式。生成方法很简单:
String encoded = new BCryptPasswordEncoder().encode("raw_password");
建议在用户注册或重置密码时调用。
资源级权限:@PreAuthorize 是真香
产品要求“编辑只能改自己的文章”,这就要方法级别的权限控制了。
Spring Security 提供了 @PreAuthorize,配合 SpEL 表达式,非常灵活:
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
@PreAuthorize("@articleService.canEdit(principal.name, #id)")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable Long id) {
articleService.deleteById(id);
return ResponseEntity.ok().build();
}
}
对应的 articleService.canEdit() 方法:
public boolean canEdit(String username, Long articleId) {
Article article = articleRepository.findById(articleId).orElse(null);
return article != null && article.getAuthor().equals(username);
}
这里用了 @ 引用 Bean,principal.name 拿当前用户名。比硬写 hasRole('EDITOR') 灵活多了。
不过要注意:开启 @PreAuthorize 需要在配置类加 @EnableMethodSecurity(新版)或 @EnableGlobalMethodSecurity(prePostEnabled = true)(旧版)。
性能与运维:别让安全拖垮系统
上线前压测发现,每次请求都要查数据库拿用户权限,QPS 直接掉一半。
解决方案:缓存 UserDetails。
我在 CustomUserDetailsService 里加了 Redis 缓存:
@Override
public UserDetails loadUserByUsername(String username) {
String cacheKey = "user_detail:" + username;
UserDetails cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
// ...查 DB...
redisTemplate.opsForValue().set(cacheKey, userDetails, Duration.ofMinutes(30));
return userDetails;
}
缓存 30 分钟,用户改权限后手动清理即可。线上 QPS 回升到 98%。
另外,日志也加了关键信息:
log.info("User {} accessed {} with roles {}", username, requestURI, authorities);
方便审计和排查问题。运维老张说:“终于不用半夜打电话问我‘为啥某人没权限’了”。
一点综合思考:Python vs Java 安全生态
作为长期混迹 Python 圈的人,不得不说,Spring Security 虽然重,但胜在完备。Django 的 auth 系统简单好用,但要做细粒度权限、多租户、OAuth2 集成,就得自己拼轮子。而 Spring Security 把这些都封装好了,只是学习曲线陡了点。
如果你的团队有 Java 背景,且系统复杂度高,Spring Security 是值得投入的。但如果只是做个内部小工具,Python + FastAPI + PyJWT 可能更快出活。
最后
折腾三天,系统终于上线了。虽然代码还有优化空间(比如权限缓存策略、JWT 刷新机制),但至少扛住了双 11 预热流量。
现在我一边改 Bug,一边刷 Rust 的 async book。毕竟,跳槽简历上不能只有“精通加班”啊。
共勉。

评论 0