Spring Security基础:快速搭建安全认证系统
大家好,我是小张,在老家县城远程办公的“小镇做题家”。白天在一家中小厂当后端开发,晚上刷 LeetCode 准备跳槽简历,周末偶尔研究点底层原理——比如 JVM 的 GC 算法、Netty 的 Reactor 模型,或者 Redis 的持久化机制。最近被领导安排了一个“紧急又不急”的任务:给公司内部管理系统加上一套靠谱的身份认证和权限控制。说白了,就是用 Spring Security 搞个安全认证系统。
坦白讲,之前我对 Spring Security 的理解还停留在“加个 @EnableWebSecurity 就完事了”的水平。但这次不行,因为产品经理上周五下午 5:47 在群里甩过来一句话:“老张,咱们的管理后台现在谁都能进,太危险了,得加登录和角色权限。”然后第二天就去三亚团建了。我看着钉钉消息,心里一万个草泥马奔腾而过——这不就是典型的“需求提得快,人跑得更快”吗?
更离谱的是,测试同学昨天还报了个线上 bug:某个接口没做鉴权,实习生误删了生产数据(还好有 binlog 回滚)。运维大哥一边骂一边帮我们恢复数据,临走前丢下一句:“你们再不做权限控制,我就把你们服务 IP 加到黑名单。”行吧,看来这事儿真的不能再拖了。
于是,我咬咬牙,关掉 B 站上的《Go 语言并发编程实战》(是的,我也在偷偷学 Go,毕竟现在简历上不写点 Go 好像都不好意思投大厂),打开 VSCode,插件栏里 Spring Boot Extension Pack、Lombok、GitLens 全都亮着,深吸一口气,开始啃 Spring Security 的官方文档。
为什么不用自己手写 Token 鉴权?
其实一开始我想偷懒——直接搞个拦截器,校验 JWT Token,查数据库看用户有没有权限,完事。这种“土法炼钢”我在上家公司就干过,代码简单,逻辑清晰,调试也方便。但这次项目要上线到客户私有化部署环境,安全审计要求很高,自己造轮子风险太大。万一哪天被人绕过 Token 直接调接口,锅还是我的。
而且,Spring Security 不是花架子。它背后有一套完整的过滤器链(Filter Chain) 架构,从 UsernamePasswordAuthenticationFilter 到 ExceptionTranslationFilter,每个环节都经过工业级验证。更重要的是,它和 Spring 生态无缝集成——比如方法级权限 @PreAuthorize("hasRole('ADMIN')"),简直不要太香。
所以,别再想着“我手写一个更轻量”了。安全这事,交给专业框架,省心又省命。
快速搭建:三步走战略
第一步:引入依赖,配置基础结构
新建一个 Spring Boot 项目(我用的 3.2.0),pom.xml 加上:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>pring-boot-starter-web</artifactId>
</dependency>
注意:Spring Boot 3.x 默认用的是 Jakarta EE 9+,包名从 javax.* 变成了 jakarta.*,如果你是从旧项目升级,可能会遇到兼容问题。我上周就踩了这个坑,启动直接报 ClassNotFoundException,差点以为是我电脑中病毒了。
然后,创建一个最简单的 SecurityConfig:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
}
这时候启动项目,访问任意接口(比如 /api/user/list),会被自动重定向到 /login 页面——Spring Security 默认会生成一个丑到爆的登录页。别慌,这是正常现象,说明安全框架生效了。
开发心得:很多人第一次看到这个默认登录页都会懵,以为配置错了。其实不是,它只是 Spring Security 的“兜底行为”。你可以通过
.formLogin().loginPage("/your-login")自定义,也可以直接禁用表单登录,改用 JSON 认证(后面会讲)。
第二步:自定义用户认证逻辑
默认情况下,Spring Security 会用内存中的用户(用户名 user,密码随机生成在日志里)。这显然不能用于生产。
我们需要实现 UserDetailsService 接口,从数据库加载用户:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 注意:这里应该是 BCrypt 加密后的
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
同时,记得配置密码编码器:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
重点来了:千万别存明文密码!我见过太多小公司为了“调试方便”直接存明文,结果某天数据库泄露,全员账号被盗。BCrypt 是 Spring Security 官方推荐的,加盐、慢哈希、防彩虹表攻击,一行代码搞定安全底线。
第三步:改成前后端分离的 JSON 登录
我们公司的前端是 Vue + Axios,根本不需要 Spring Security 的表单跳转。所以得改成 JSON 认证。
思路是:前端 POST /auth/login,携带 {username, password},后端验证成功后返回 JWT Token。
为此,我们要自定义一个 AuthenticationFilter,并替换掉默认的 UsernamePasswordAuthenticationFilter。
先写个登录控制器:
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 生成 JWT Token(这里简化,实际用 JJWT 库)
String token = JwtUtil.generateToken(auth.getName());
return ResponseEntity.ok(Map.of("token", token));
} catch (BadCredentialsException e) {
return ResponseEntity.status(401).body("Invalid credentials");
}
}
}
然后在 SecurityConfig 中关闭表单登录,放行 /auth/login:
http
.csrf(csrf -> csrf.disable()) // 前后端分离通常关 CSRF
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
最后,写一个 JwtFilter 从 Header 中解析 Token 并设置 SecurityContext:
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
String token = extractToken(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());
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
这样,整个认证流程就跑通了:前端拿 Token,每次请求带 Authorization: Bearer <token>,后端验证后自动填充用户上下文。
权限控制:从 URL 到方法级
光有登录还不够,还得控制谁能看到什么。
Spring Security 支持两种粒度:
URL 级别:在
SecurityConfig里配.requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")方法级别:用注解
@PreAuthorize("hasRole('ADMIN')") public void deleteUser(Long id) { ... }
后者需要开启全局方法安全:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {}
生产建议:URL 级别适合粗粒度控制(比如整个模块),方法级别适合细粒度(比如“只有本人能修改自己的资料”)。两者结合使用最稳。
另外,权限字段设计也得讲究。我们数据库里用户表有个 roles 字段,存的是 ["ROLE_ADMIN", "ROLE_USER"] 这样的 JSON 数组。Spring Security 会自动去掉前缀 ROLE_ 做匹配,所以 hasRole('ADMIN') 实际匹配的是 ROLE_ADMIN。
血泪教训:千万别把权限字符串硬编码在代码里!我们之前有个同事写了
if (role.equals("管理员")),结果测试环境角色叫“超级管理员”,直接权限失效。后来统一改成枚举 + 数据库配置,才避免这种低级错误。
性能与运维考量
虽然 Spring Security 功能强大,但也有性能陷阱。
每次请求都查数据库?
不会!UserDetails默认会被缓存到SecurityContext中,一次认证,全程有效(只要你不手动清理)。但如果用了 JWT 且是无状态服务,那每次都要解析 Token 并查用户信息。这时候可以考虑:- Token 里直接嵌入角色信息(减少 DB 查询)
- 用 Redis 缓存用户权限(TTL 和 Token 过期时间一致)
日志监控怎么做?
我们在AuthenticationSuccessHandler和AuthenticationFailureHandler里加了日志埋点,记录登录成功/失败的 IP、时间、用户名。配合 ELK,能快速发现暴力破解行为。如何应对 Token 泄露?
虽然 JWT 无法主动失效,但我们加了“黑名单”机制:用户登出时,把 Token 存入 Redis,设置 TTL 为剩余有效期。每次请求先查黑名单,存在则拒绝。
跳槽视角:为什么简历要写 Spring Security?
说实话,很多初级开发者觉得“会用 Spring Boot 就行了,安全有中间件挡着”。但面试官恰恰喜欢问 Security 的底层原理——比如:
- 过滤器链的执行顺序是怎样的?
AuthenticationManager和ProviderManager的关系?- 如何自定义
AccessDecisionManager实现动态权限?
我在刷面经时发现,懂 Security 的人,往往对 Spring AOP、代理模式、责任链模式也理解更深。这正是大厂看重的“技术纵深”。
而且,现在微服务架构下,OAuth2、SSO、RBAC 都是标配。哪怕你主语言是 Go(没错,我又在学 Go 了),理解这些概念也能让你在跨语言协作时不掉队。
写在最后
折腾了三天,终于把这套安全系统上线了。测试同学跑完回归测试,给了我一个大拇指;运维大哥默默把我从“高危开发者名单”里划掉了;产品经理在群里发了个红包,虽然只有 8 块钱……
但最开心的是,我再也不用担心实习生误删数据了。
回头看看,Spring Security 其实没那么可怕。只要你愿意沉下心读文档、看源码(强烈建议 debug 跟一下 FilterChainProxy 的调用栈),就能把它变成你的护城河,而不是拦路虎。
对了,如果你也在县城远程办公,边工作边刷题,准备跳槽——欢迎留言交流。也许下次我们就能在杭州或深圳的某家大厂工位上,一起吐槽新公司的产品经理呢。
共勉。
附:常用配置对比表
| 配置项 | 表单登录(传统) | JSON + JWT(前后端分离) |
|---|---|---|
| Session | 有状态(Cookie) | 无状态(Header Token) |
| CSRF 防护 | 默认开启 | 通常关闭 |
| 登录入口 | /login 页面 |
/auth/login API |
| 权限传递 | Session 存储 | Token 携带 |
| 适合场景 | 后台管理系统 | 移动 App / SPA 前端 |
关键依赖版本参考
Spring Boot: 3.2.0
Spring Security: 6.1.5
JJWT: 0.11.5
记住:安全不是功能,而是基础设施。别等事故发生了才想起它。

评论 0