Spring Security 基础:快速搭建安全认证系统
昨晚十一点半,窗外的上海下着小雨,我还在公司附近的出租屋里敲代码。咖啡已经凉了第三杯,但效率奇高——也不知道是不是因为明天就是项目交付 deadline,还是单纯地“深夜才是程序员真正的黄金时间”。作为一名 211 软件工程研二的学生,我在实验室接了个横向项目,说是“科研”,其实干的活跟一线后端开发差不多:写接口、调性能、怼测试、陪产品经理画饼……
最近被安排搞一个内部管理后台的权限控制模块,领导轻飘飘一句:“用 Spring Security 吧,成熟稳定。”
我表面点头称是,心里却在翻白眼:Spring Security 这玩意儿配置复杂得堪比考研政治,文档又臭又长,连官方 demo 都像是从 2008 年穿越过来的。更离谱的是,团队里居然没人真正用过它做过生产级项目——大家都是“听说过,没用过,但感觉很牛”。
于是,上周五晚上,我一边啃着泡面,一边硬着头皮开干。目标很明确:三天内,搭出一个能跑、能登录、能鉴权的基础认证系统。不求花里胡哨,但求稳如老狗。
为啥非得是 Spring Security?
其实一开始我想上 Shiro,轻量、简单、中文文档友好。但组长一句话把我打回现实:“Shiro 社区快凉了,新项目别碰。Security 是 Spring 官方亲儿子,集成 OAuth2、JWT、多租户都方便,以后扩展也省事。”
行吧,亲儿子就亲儿子。但问题是,Security 的“约定大于配置”风格真的劝退。你要是不按它的套路来,分分钟给你报 AccessDeniedException,还附带一行灵魂拷问:
"Authentication object cannot be null"
那一刻我真的想砸电脑。
不过吐槽归吐槽,Security 的优势确实没法忽视:
- 深度集成 Spring Boot,自动装配省心
- 权限模型灵活(GrantedAuthority、Role、Expression)
- 支持方法级注解(
@PreAuthorize真香) - 内置 CSRF、Session 固定攻击防护等安全机制
对于我们这种既要赶工期又要保安全的小团队来说,稳定 > 创新。虽然我喜欢折腾新技术(比如前阵子偷偷在本地跑了个 Quarkus 项目),但上线的东西,还是得用“祖传配方”。
第一步:别一上来就搞 JWT,先让表单登录跑起来
很多教程一上来就教你怎么集成 JWT + Redis + OAuth2,搞得像在造火箭。但对我们这个内部系统来说,用户就几十个管理员,根本不需要无状态!直接用最传统的基于 Session 的表单登录,反而更简单、调试更直观。
于是,我新建了一个 Spring Boot 3.2 项目(对,我们已经升级到 Jakarta EE 9 了,别再用 javax.* 了兄弟们),然后只加了两个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
启动!访问任意接口,果然弹出了那个经典的丑到爆的登录页。用户名是 user,密码在控制台打印了一长串 UUID。这显然不能交差。
接下来,我自定义了用户体系。
用户表设计:别偷懒,密码必须加密!
数据库里建了张 sys_user 表(MySQL 8.0):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| username | VARCHAR(50) | 唯一,用于登录 |
| password | VARCHAR(100) | BCrypt 加密后存储 |
| role | VARCHAR(20) | 如 ADMIN, OPERATOR |
| enabled | TINYINT | 账号是否启用 |
重点来了:千万别存明文密码!我见过实习生把密码明文存进去,还理直气壮说“反正内网”。结果某天运维误操作把数据库 dump 文件发到了群里……那场面,至今难忘。
在代码里,我用 BCryptPasswordEncoder 处理密码:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
注册时加密,登录时框架自动比对,完全不用手动处理。Spring Security 在这点上做得非常体贴。
自定义 UserDetailsService:让系统认识你的用户
Security 默认从内存加载用户,我们需要对接数据库。于是实现 UserDetailsService 接口:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 注意:role 字段要加 ROLE_ 前缀,Security 才认
List<GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList("ROLE_" + user.getRole());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true, // accountNonExpired, credentialsNonExpired, accountNonLocked
authorities
);
}
}
这里有个坑:角色必须带 ROLE_ 前缀!否则你在 @PreAuthorize("hasRole('ADMIN')") 时会失效。我一开始没加,调试了两个小时,最后发现日志里权限列表是空的……血泪教训。
配置 SecurityFilterChain:别再用 WebSecurityConfigurerAdapter 了!
Spring Security 5.7+ 已经废弃了 WebSecurityConfigurerAdapter,改用函数式配置。新的写法更清爽:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.csrf(csrf -> csrf.disable()); // 内部系统,暂时关掉 CSRF(生产慎用!)
return http.build();
}
}
注意几个细节:
hasRole("ADMIN")会自动匹配ROLE_ADMIN- 我关掉了 CSRF,仅限内部管理系统!对外系统千万别这么干
- 登录成功后跳转到
/dashboard,失败则留在登录页
前端用 Thymeleaf 写了个极简登录页,表单字段名必须是 username 和 password,否则 Security 不认。这也是“约定”的一部分,改字段名就得重写 UsernamePasswordAuthenticationFilter,没必要。
方法级权限:@PreAuthorize 真香警告
表单登录跑通后,产品经理突然提了个需求:“普通操作员只能看自己创建的数据,管理员能看全部。”
这时候 URL 级权限就不够用了,得上 方法级鉴权。
首先在主类加个注解:
@EnableMethodSecurity(prePostEnabled = true)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后在 Service 方法上加注解:
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or #creatorId == authentication.principal.username")
public List<Order> getOrdersByCreator(String creatorId) {
// 查询逻辑
}
}
这里的 authentication.principal.username 就是当前登录用户名,可以直接和参数比较。动态权限校验就这么简单!
不过要注意:方法级权限默认只对 Spring Bean 生效,且必须通过代理调用(不能 this.method())。另外,性能开销略大,高频接口慎用。
踩过的坑 & 生产建议
1. Session 管理别忽视
我们用的是默认的内存 Session,但多实例部署时会出问题。后来加了 Spring Session + Redis:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
一行配置搞定分布式 Session,Security 自动接管。但要注意 Redis 连接池配置,别让 Session 把 Redis 打挂了。
2. 日志太吵?关掉 DEBUG
Security 默认日志级别是 INFO,但登录失败会疯狂刷日志。建议在 application.yml 里调整:
logging:
level:
org.springframework.security: WARN
3. 密码策略别太弱
虽然 BCrypt 很安全,但如果用户密码是 123456,照样被撞库。我们后来加了自定义 PasswordEncoder 包装层,强制密码长度 ≥8,包含大小写+数字。
4. 测试账号别硬编码
开发时经常需要不同角色账号测试。我写了个 DevUserInitializer,启动时自动创建测试用户(仅 dev profile):
@Profile("dev")
@Component
public class DevUserInitializer {
// 创建 admin / operator 账号
}
上线时用 prod profile,自动失效,安全又方便。
效果如何?Deadline 前两小时搞定!
周一上午十点,我把 PR 提交了。测试同学跑完冒烟测试,说“登录、权限、登出都正常”。组长看了一眼代码,点点头:“结构清晰,注释到位,可以 merge。”
那一刻,泡面的余味还在嘴里,但心里踏实了。虽然只是个基础认证系统,但安全无小事。以前总觉得“反正没人黑我们”,直到听说隔壁组因为没做权限校验,导致测试数据被删光……
写在最后:代码人生,安全先行
在实验室做项目这一年,我越来越意识到:炫技不如稳字当头。Spring Security 可能不是最酷的,但它像一把老式但可靠的锁——不显眼,却能守住底线。
深夜 coding 的时候,我常想:我们写的每一行代码,最终都会变成某个系统的一部分。可能是银行交易,可能是医疗记录,也可能是我正在做的这个不起眼的后台。安全不是功能,而是责任。
所以,哪怕只是个学生项目,我也坚持把密码加密、权限校验、日志脱敏这些事做到位。因为我知道,未来的某一天,这些习惯会让我成为一个值得信赖的工程师。
共勉。
P.S. 下周准备接入 OAuth2.0 对接公司统一认证中心,估计又要掉头发。如果有踩坑经验,欢迎评论区交流!
P.P.S. 别信“三天速成安全架构”,真正的安全,藏在每一个细节里。

评论 0