Spring Security基础:快速搭建安全认证系统
上周五晚上九点半,我坐在新公司的工位上,盯着屏幕上产品经理刚发来的PRD文档,心里默默问候了他祖宗十八代。这已经是我入职这家“高速成长型”创业公司两个月以来,第三次临时加需求了——这次是要在两周内给一个内部管理后台加上完整的用户登录和权限控制。
作为一个在快手干了六年的老架构师,从零到一搭过消息队列、实时推荐、风控系统……但说实话,每次碰上权限这块儿,我还是会条件反射地挠头。不是因为难,而是因为太容易翻车。去年双11前夜,我们组就因为一个权限绕过漏洞被安全团队拉去“喝茶”,差点没赶上大促上线。
所以这次,我决定不折腾了,直接上 Spring Security —— 虽然它配置起来像在解谜题,但一旦跑通,稳如老狗。
为什么是 Spring Security?
先说清楚,我不是它的脑残粉。早年我也试过自己手撸 Token 验证、中间件校验,结果呢?代码越写越多,Bug 越改越乱,最后连我自己都看不懂哪段逻辑管登录、哪段管鉴权。
后来读《Spring实战》(对,就是那本被无数人当枕头的神书)里关于 Security 的章节,才恍然大悟:人家早就把轮子造好了,你非要去削木头,图啥?
而且我们这个项目是个典型的 B 端管理产品,用户角色清晰(管理员、运营、审核员),接口权限粒度到方法级别就行。这种场景,Spring Security + JWT 的组合拳打出来,又快又稳。
💡 小插曲:我其实重度依赖 ChatGPT 辅助开发(别笑,现在谁不用?),但上次让它生成 SecurityConfig,结果把
antMatchers写成了anyMatchers,本地跑得好好的,一上测试环境直接 403 拒绝全家桶。从此我只敢让它写 CRUD,核心安全逻辑必须自己过一遍。
快速搭建:三步走战略
咱们的目标很明确:让用户能登录,登录后能访问自己的接口,不能越权。
第一步:依赖 & 基础实体设计
先往 pom.xml 里塞点料:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
数据库方面,简单搞两张表:
user表:存用户名、加密密码、状态role表:存角色(比如ADMIN,OPERATOR)- 中间表
user_role:多对多关联
📌 注意:密码千万别明文存!用
BCryptPasswordEncoder加密,这是 Spring Security 自带的,一行代码搞定:@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
第二步:自定义 UserDetailsService
Security 默认是从内存或 JDBC 查用户,但我们得对接自己的用户体系:
@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("用户不存在");
}
// 这里可以把角色也查出来,转成 GrantedAuthority 列表
List<GrantedAuthority> authorities = buildAuthorities(user.getRoles());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities
);
}
private List<GrantedAuthority> buildAuthorities(List<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
}
这段代码的关键在于:把你的业务用户模型,转换成 Spring Security 认的 UserDetails 对象。别小看这个转换,很多新手在这卡半天——比如忘了加 ROLE_ 前缀,结果权限死活不生效。
第三步:配置 SecurityFilterChain(重点!)
以前用 WebSecurityConfigurerAdapter,现在 Spring Boot 2.7+ 推荐用新的函数式配置方式。我一开始也懵,后来发现其实更清晰:
@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("/auth/login").permitAll() // 登录接口放行
.requestMatchers("/actuator/**").permitAll() // 健康检查
.anyRequest().authenticated() // 其他都要认证
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
这里有几个坑要提醒:
csrf().disable():如果你是纯 API 服务(比如 Vue/React 前端调后端),可以关。但如果还有 Thymeleaf 页面,别乱关!STATELESS:表示不创建 Session,靠 JWT Token 认证,适合分布式部署。addFilterBefore:把自己的 JWT 校验过滤器插到 Security 的认证流程前面。
JWT 认证过滤器怎么写?
这个过滤器负责从请求头里拿 Token,解析,然后塞进 SecurityContext:
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.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);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
⚠️ 真实事故复盘:有一次线上用户反馈“登录后还是跳登录页”,查了半天发现前端传的 Header 是
authorization(小写),而我们后端用的是getHeader("Authorization")。HTTP Header 是大小写不敏感的,但某些代理(比如 Nginx 配置不当)会丢掉小写 header!最后统一约定前端传Authorization: Bearer xxx,后端用@RequestHeader("Authorization")接收,问题解决。
权限控制:方法级注解真香
光有登录不够,还得控制谁能调哪个接口。比如:
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.listAll();
}
@PreAuthorize("hasRole('OPERATOR') or hasRole('ADMIN')")
@PostMapping("/audit")
public Result audit(@RequestBody AuditRequest req) {
return auditService.process(req);
}
}
记得在启动类上加 @EnableMethodSecurity(旧版是 @EnableGlobalMethodSecurity(prePostEnabled = true))。
这种方式写起来爽,但性能有代价——每个方法调用都会触发权限检查。如果你的接口 QPS 很高(比如每秒几千次),建议在 Controller 层做粗粒度拦截,而不是在 Service 方法上加。
最后一点心得
搭建完这套系统,总共花了我两天时间(包括写单元测试)。第二天下午 demo 给产品经理看,他眼睛一亮:“这么快?我以为至少要一周。” 我心里苦笑:六年踩过的坑,换来的就是少加班两小时。
Spring Security 确实学习曲线陡峭,文档又臭又长。但只要你理解它的核心思想——认证(Authentication)和授权(Authorization)分离,通过 Filter Chain 插拔组件——后面就顺了。
顺便说一句,如果你想系统学,除了官方文档,我强烈推荐《Spring Security in Action》这本书(比《Spring实战》讲得深)。不过别指望看一遍就会,最好的学习方式永远是:接个需求,硬着头皮干。
现在,我的管理后台已经稳稳跑在生产环境了。虽然运维昨天半夜打电话说“CPU 突然飙高”,但查了一圈发现是另一个服务的问题……Spring Security 这块,至今零故障。
这就够了。毕竟,在互联网公司,能不背锅的代码,就是好代码。

评论 0