Spring Security 基础:从“连登录都不会”到搭出安全认证系统,我的野路子实战记
大家好,我是小陈,一个刚毕业半年的大专生,坐标杭州未来科技城。去年秋招靠自学前端 + 一点后端基础,在一家中型电商公司混到了全栈开发岗(其实是前端为主,但老板看我会点 Java 就顺手丢后端活过来)。团队里除了我这个“野鸡出身”的,其他全是科班硕士,每次开会我都缩在角落假装自己是背景板。
不过最近可把我牛逼坏了——上周五晚上,我居然独立用 Spring Security 搭了一套完整的用户认证系统,上线后稳如老狗,连测试妹子都说“这次没崩”。要知道三个月前,我连 AuthenticationManager 是干啥的都不知道,还把 JWT 和 Session 混为一谈,被同事笑称“安全盲”。
今天这篇不是那种高大上的架构师指南,就是一个普通大专应届生的踩坑实录。如果你也像我一样,学历平平、基础一般,但又想在阿里网易扎堆的杭州活下去,那这篇可能对你有点用。毕竟,代码人生,从来不是靠学历写的,而是靠一行行 debug 出来的。
为啥突然搞 Spring Security?因为老板说“双11前必须上登录”
事情得从上个月说起。我们有个内部运营后台,原本是裸奔状态——没登录、没权限、谁拿到 URL 都能改数据。产品经理(对,就是那个总说“这个需求很简单”的 P0)突然拍脑袋:“双11前必须加登录认证,还要分角色!”
我当场就懵了。之前做过的项目顶多是用 localStorage 存个 token,前端拦一下路由就完事。现在要真刀真枪搞后端认证?我连数据库里的用户表都没建过!
硬着头皮接了活,第一反应是翻书。不是电子书,是实体书——《Spring Security 实战》(人民邮电出版社那本)。别笑,虽然现在人都刷视频学技术,但我发现,有些底层逻辑,只有书才能讲清楚。比如为什么 Spring Security 默认用 DelegatingFilterProxy 而不是直接注册 Filter?书里一页图解就让我豁然开朗。
第一步:别一上来就搞 OAuth2,先搞定最简单的表单登录
很多教程一上来就讲 JWT、OAuth2、RBAC,搞得人头晕。我一开始也犯这毛病,结果第一天就在 UserDetailsService 和 PasswordEncoder 的注入上卡了三小时。
后来我悟了:先跑通最原始的表单登录,再谈高级功能。
1. 引入依赖(别漏了 Web)
<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>
注意:必须加 web!不然启动都起不来,报错 No qualifying bean of type 'AuthenticationManager' —— 我当时以为是配置错了,其实是因为没引入 Web 环境,Security 自动配置压根没触发。
2. 写个 UserDetailsService(重点!)
这是认证的核心。你需要告诉 Spring Security:“用户信息去哪查”。
@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("用户不存在");
}
// 注意:这里返回的是 org.springframework.security.core.userdetails.User
return User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 必须是加密后的!
.roles("USER") // 或从数据库读取
.build();
}
}
血泪教训:密码一定要加密存储!我第一次测试用明文密码,结果登录死活不成功。后来才发现 Spring Security 默认用 BCryptPasswordEncoder,而我数据库存的是 123456……运维大哥差点把我电脑砸了。
3. 配置 Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
这段配置的意思很直白:
/login页面和静态资源放行- 其他所有请求都要登录
- 登录页是
/login,成功后跳/dashboard - 登出后跳回登录页
跑起来后,你会发现 Spring Security 自动生成了一个丑到爆的登录页。别慌,这是正常现象。先让功能跑通,UI 后面再换——这是我从产品经理那学来的“敏捷哲学”(其实就是拖时间)。
从表单登录到 JWT:为了前后端分离,我被迫升级
我们的运营后台是 Vue 写的 SPA,根本不需要表单提交。所以第二天,我就被要求改成 Token 认证。
这时候,很多人会直接上 JwtAuthenticationFilter,但千万别急!先理清流程:
- 用户 POST
/login,带 username/password - 后端验证,成功则生成 JWT 返回
- 前端存 Token,后续请求带
Authorization: Bearer <token> - 后端用 Filter 解析 Token,还原 Authentication
关键改造点
- 禁用 Session:
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - 自定义登录接口:不再用
formLogin(),而是写一个/loginController - 加一个 JwtFilter:放在
UsernamePasswordAuthenticationFilter之前
// JwtFilter 示例(简化版)
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.validate(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
然后在 SecurityConfig 里注册:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
踩坑现场:
- 忘了
@Component注解,Filter 没注入,导致所有请求 403 - Token 过期时间设成 10 年,被安全审计组骂成狗
SecurityContextHolder没清理,测试时串用户(线上事故预警!)
数据库设计 & 接口规范:别只顾着写代码
很多教程只讲 Security 配置,但实际工作中,数据库和接口才是命门。
用户表设计(简化版)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| username | VARCHAR(50) | 唯一 |
| password | VARCHAR(100) | BCrypt 加密后 |
| role | VARCHAR(20) | 如 "ADMIN", "OPERATOR" |
| enabled | TINYINT | 是否启用(用于封号) |
注意:
- 密码字段长度至少 60(BCrypt 输出约 60 字符)
- 加
enabled字段,比物理删除更安全 - 角色建议用字符串而非 ID,避免联表查询(性能考虑)
接口设计
POST /api/login
{
"username": "chen",
"password": "123456"
}
→
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.xxxxx",
"expiresIn": 3600
}
}
别返回密码!别返回明文! 曾经有个实习生把整个 User 对象返回,包括加密后的密码,被运维拉去喝茶。
生产环境那些事儿:日志、监控、防爆破
你以为跑通就完了?Too young.
1. 登录失败次数限制
暴力破解怎么办?Spring Security 本身不提供,但可以结合 Redis:
// 登录时
String key = "login_fail:" + username;
Long count = redisTemplate.opsForValue().increment(key);
if (count > 5) {
throw new RuntimeException("尝试次数过多,请10分钟后重试");
}
redisTemplate.expire(key, 10, TimeUnit.MINUTES);
2. 记录登录日志
谁在什么时候从哪登录,必须留痕:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
try {
// 认证...
logService.recordLogin(username, getClientIP(request), "SUCCESS");
return ok(token);
} catch (Exception e) {
logService.recordLogin(username, getClientIP(request), "FAILED: " + e.getMessage());
throw e;
}
}
3. 定期轮换密钥(JWT)
JWT 的 secret 如果泄露就全完了。我们团队的做法是:
- Secret 存在 Vault(或配置中心)
- 每月自动轮换
- 支持多版本 secret 验证(过渡期)
总结:大专生也能搞安全,关键是要“钻”
回头看这一个月,从连 Authentication 和 Authorization 都分不清,到现在能独立设计认证模块,我最大的体会是:
不要怕底层,越怕越弱。
Spring Security 看似复杂,拆开就是几个核心组件:
UserDetailsService→ 用户从哪来PasswordEncoder→ 密码怎么校验Filter→ 请求怎么拦截GrantedAuthority→ 权限怎么判断
搞懂这些,其他都是排列组合。
另外,多看源码。我花了一晚上 debug UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法,终于明白表单参数为什么默认是 username 和 password。这种理解,是看一百篇博客都换不来的。
最后,分享一句我在《代码大全》里看到的话(对,我又看书了):
“安全不是功能,而是属性。”
我们写代码,不能只想着“跑起来就行”,还得想“会不会被人黑”。尤其是在杭州这种互联网重镇,阿里网易对安全的要求近乎变态——听说隔壁厂因为一个 XSS 漏洞,年终奖直接砍半。
所以啊,兄弟们,别觉得自己学历低就干不了后端。代码人生,从来不由起点决定,而由你愿意钻多深决定。
作者:小陈
身份:杭州某电商公司全栈开发(大专应届)
爱好:周末逛 GitHub 看开源项目源码,偶尔给 Spring Boot 提 PR(虽然都被拒了)
近况:正在啃《Spring Security in Action》,目标是年底前能面阿里 P6
如果你也在自学路上挣扎,欢迎留言交流。别怕问“蠢问题”——我问过的问题,可能比你想象的更蠢 😅

评论 0