用 Spring Security 快速搭建安全认证系统,这波我赌你会踩坑
大家好,我是做了五年后端开发的程序员小陈。今天想和大家分享一下我在实际项目中使用 Spring Security 搭建基础认证系统的心得。这个话题看似基础,但如果你真正上过生产环境,就知道里面藏着不少细节。
背景:为什么我们需要一个安全的认证系统?

我们团队之前接了一个企业内部系统的重构项目,主要功能是给员工提供审批、查看数据报表等功能。客户特别强调——必须保证用户权限清晰,不能有越权访问的情况。
最开始我打算直接手写一套登录逻辑:用户名+密码验证,存 session,然后在每个接口加拦截器判断是否登录。不过领导看了一眼就建议:“你这不如用 Spring Security,能少写不少 bug。”
我当时心里还嘀咕:听说 Spring Security 配置复杂,源码难懂,会不会反倒是给自己找麻烦?但在后来的实践中我发现,如果用得好,它确实能帮你规避很多安全问题,节省大量重复工作。
问题来了:初次尝试踩了哪些坑?


第一次试水是在本地搭了个 demo 环境,结果上来就被几个问题绊住了脚:
坑一:默认的登录页面怎么去掉?
我想用自己的登录接口,不想走 /login 页面跳转那一套,但一开始无论怎么改配置,还是会进到那个烦人的 login 页面。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
}
这个 formLogin() 默认会启用表单登录页面,必须改成
.loginProcessingUrl("/api/login")才行。
坑二:RESTful 接口怎么返回 JSON?
默认情况下,认证成功或失败返回的是 HTML 页面或者重定向,但我希望返回标准的 JSON 格式,例如:
{
"code": 0,
"message": "success",
"data": {}
}
这个需要自定义 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,才能统一格式。
坑三:CSRF 是什么?为什么要关掉?
我们的前端是前后端分离架构,采用 JWT 的方式做认证,不需要 Session。这时候 CSRF 实际上是没有必要的,而且会导致 POST 请求被拒绝。
最终我通过:
http.csrf().disable();
解决了这个问题。
我们是怎么做的?从零搭建 Spring Security 认证系统

下面我就以我们项目的初期阶段为例,讲讲我们是如何快速搭建起一套基于 Spring Security 的认证系统的。
第一步:明确认证流程需求
我们需要满足以下几个核心点:
- 用户名 + 密码登录
- 登录成功返回 token(这里我们用 JWT)
- 后续请求带上 token,由服务端校验
- 支持多角色权限控制(比如普通员工、部门主管、管理员)
第二步:选择技术栈
- Spring Boot 2.7.x(兼容性更好)
- Spring Security 5.7.x
- MyBatis Plus 操作数据库
- JWT:用的 jjwt 库生成 token
- MySQL 存储用户信息与角色关系
第三步:设计数据库结构
用户表(user)
| id | username | password | nickname | deleted |
|---|---|---|---|---|
| 1 | admin | xxx | 管理员 | 0 |
角色表(role)
| id | role_name |
|---|---|
| 1 | ROLE_ADMIN |
| 2 | ROLE_USER |
用户角色关联表(user_role)
| user_id | role_id |
|---|---|
| 1 | 1 |
第四步:关键代码实现
自定义 UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<GrantedAuthority> authorities = new ArrayList<>();
List<String> roleNames = userService.getRolesByUserId(user.getId());
for (String roleName : roleNames) {
authorities.add(new SimpleGrantedAuthority(roleName));
}
return new UserDetailImpl(user, authorities);
}
}
自定义过滤器处理登录逻辑
@Component
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequest loginReq = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginReq.getUsername(), loginReq.getPassword())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
String token = jwtUtils.generateToken(authResult.getName());
response.setHeader("Authorization", "Bearer " + token);
ResponseUtil.writeSuccess(response, Collections.singletonMap("token", token));
}
}
添加 Token 校验的过滤器
@Component
public class JwtVerifyFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.extractUsername(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
配置类 SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private JwtLoginFilter jwtLoginFilter;
@Autowired
private JwtVerifyFilter jwtVerifyFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtVerifyFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAt(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();

return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.build();
}
}
踩过的那些坑,现在回头看都很值
坑一:Security 上下文传递失败?
我们在分布式系统中用了 Feign 进行服务调用,刚开始每次调用其他服务时,SecurityContext 总是为空,导致鉴权失败。
解决方法:在 FeignClient 上加上 configuration = FeignConfig.class,并在 FeignConfig 中注入 RequestInterceptor。
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String authorization = request.getHeader("Authorization");
if (authorization != null) {
requestTemplate.header("Authorization", authorization);
}
}
};
}
坑二:多线程环境下 SecurityContext 丢失?
我们有些业务操作涉及异步执行,比如发送通知邮件,但发现在线程池中获取不到当前用户信息。
解决办法是包装 Runnable:
SecurityContext context = SecurityContextHolder.getContext();
executor.execute(() -> {
SecurityContextHolder.setContext(context);
// do something
});
结果与收益:效率提升明显
整套认证体系上线之后,效果非常不错:
- 登录响应平均时间 < 100ms
- 接口请求鉴权开销几乎可以忽略
- 安全性增强:没有再出现过越权问题
- 权限模型灵活扩展:后续新增角色和权限很容易
更重要的是,我们把这一整套封装成了独立模块,供多个项目复用,极大提高了新项目接入速度。
经验分享:给刚入门的同学几点建议
不要一开始就啃 Spring Security 源码
先动手搭起来,再逐步理解它的机制。否则容易陷入“源码看懂了但不会用”的尴尬。搞清认证和鉴权的区别
Spring Security 更擅长的是认证流程管理,权限控制部分可以根据业务需要自己扩展。关注线程上下文问题
尤其是在异步编程或多线程场景下,SecurityContext 一定要手动传过去。合理关闭不必要的保护机制
比如前后端分离项目完全可以关闭 CSRF,别为了兼容默认设置而强行绕弯路。日志监控很重要
建议记录所有认证失败的请求 IP、时间等信息,方便后续审计和风控。
写在最后:安全不是万能的,但它值得认真对待
做后端开发这么多年,我越来越意识到一点:安全不是锦上添花的功能,而是最基本的保障。
尤其是现在的系统动辄要面对外部攻击、内部数据泄露等问题。Spring Security 虽然学习曲线陡峭,但是一旦掌握熟练,你会发现它是你在后端攻防战中的最佳战友。
希望这篇文章能帮助你在 Spring Security 的学习路上少走点弯路,也欢迎留言交流,我们一起成长 🚀!
如果你对这套方案感兴趣,也可以在我的 GitHub 上看到完整的示例工程(后续会同步开源)。有问题随时欢迎来骚扰!

评论 0