从零开始,用 Spring Security 快速搭建安全认证系统

郭娟_码农
2025-06-28 20:16
阅读 653

引言:一次小需求引发的“大工程”

引言:一次小需求引发的“大工程”

我之前在一家做 SaaS 的公司负责后端开发,有一次产品提了一个需求:给用户后台加上登录功能,并且根据角色控制不同页面的访问权限。听起来挺简单吧?但当时我们的项目已经上线了一段时间,很多模块都已经成型了,直接加一个登录模块不仅要考虑兼容现有接口,还得保证不破坏原有的功能。

一开始我打算自己写个简单的登录逻辑,结果越想越复杂:密码怎么加密?Token 怎么管理?有没有现成的框架可以快速实现?这时候我想起了以前学过的 Spring Security,于是决定试试看能不能用它来快速搭起一套完整的安全认证系统。

这篇文章就是基于这次实战经验写的,希望能帮你少走点弯路。


问题描述:我们需要什么?

问题描述:我们需要什么?

我们当时的系统是一个前后端分离的 REST API 系统,前端使用 Vue,后端是 Spring Boot + MyBatis,数据库用的是 MySQL。

主要诉求如下:

  1. 用户登录后能获取 Token(比如 JWT)。
  2. 每次请求都需要带上这个 Token 进行身份验证。
  3. 不同角色的用户可以访问不同的接口(权限控制)。
  4. 登录失败要限制尝试次数,防止暴力破解。
  5. 已有的接口不能因为引入安全机制被破坏掉。

这些问题看起来好像都能靠写一堆拦截器搞定,但实际上一动手就发现要考虑的东西特别多,特别是权限这块,还要和数据库配合设计角色权限表。


解决方案:为什么选择 Spring Security?

Spring Security 虽然学习曲线略陡,但一旦掌握,就能覆盖绝大多数企业级系统的安全需求。我当时之所以选择它,主要是因为它有以下优势:

  • 支持多种认证方式:表单登录、JWT、OAuth2、LDAP 等等。
  • 权限控制非常灵活,支持方法级别的注解(如 @PreAuthorize)。
  • 有丰富的扩展点,可以定制各种行为,比如登录失败处理、自定义过滤器等。
  • 社区活跃,文档完善,遇到问题基本上都能找到答案。

更重要的是,它可以很好地整合进已有的 Spring Boot 项目中,不需要你把整个项目重构一遍。


实践过程:一步步搭建认证体系

1. 添加依赖

先引入 Spring Security 和 JWT 相关的依赖(我们最后决定采用 JWT 做无状态认证):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>

2. 数据库设计:用户与角色管理

我们原本的用户表只有基础信息字段,现在需要添加角色、权限相关的内容,最终设计如下:

users 表:

字段名 类型 说明
id bigint 主键
username varchar 用户名
password varchar 密码(BCrypt 加密)
enabled tinyint 是否启用账户

roles 表:

字段名 类型 说明
id bigint 主键
name varchar 角色名称(如 ROLE_ADMIN)

负载均衡配置-2

user_roles 表:

字段名 类型 说明
user_id bigint 用户ID
role_id bigint 角色ID

有了这些表结构之后,就可以通过 Spring Data JPA 或 MyBatis 查询出用户的角色用于权限判断。

3. 核心配置:SecurityConfig

这是我最花时间调试的地方,尤其是对已有接口的影响控制。下面是简化后的核心配置类:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private JwtFilter jwtFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/api/auth/login").permitAll()
            .anyRequest().authenticated();


![缓存策略对比-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062820/b17c2553-d7d5-49f6-b527-bafaff9b69d7.jpg)


        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

这段代码做了几件事:

  • 关闭 CSRF 防护(因为我们用 Token 认证)
  • 设置会话为无状态(Stateless)
  • 添加一个 JWT 的前置过滤器
  • 开启 @PreAuthorize 注解支持
  • /login 接口放行,其他接口都需认证

4. 实现 JWT 登录流程

我们实现了几个关键类:

  • JwtUtil: 生成和解析 JWT Token
  • AuthController: 提供登录接口
  • JwtFilter: 拦截请求,校验 Token 合法性
  • UserDetailsServiceImpl: 根据用户名加载用户和角色信息

JwtFilter 为例,它的作用是在请求进入 Controller 之前进行 Token 校验,并设置当前登录用户的上下文:

public class JwtFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
        throws ServletException, IOException {
        String token = getToken(request);
        if (token != null && JwtUtils.validateToken(token)) {
            String username = JwtUtils.getUsernameFromToken(token);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                username, null, getAuthorities(username));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

踩坑经验:那些让人崩溃的小细节

说真的,Spring Security 官方文档虽然不错,但有些地方还是太抽象,真正开发时踩了不少坑:

坑一:静态资源访问问题

一开始我们把 /static/** 映射的路径也拦住了,结果前端页面访问不了 CSS 和 JS 文件。后来才意识到要在 configure 方法里放行静态资源:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/static/**", "/api/auth/login");
}

坑二:跨域问题(CORS)

前后端分离场景下,跨域是个常见问题。解决办法有两种:

  1. 在 Spring Security 中单独配置允许的来源(推荐):
http.cors(withDefaults())

同时提供一个 CorsConfigurationSource Bean。

  1. 或者在网关层统一处理 CORS(更优雅)。

坑三:异常处理没有统一入口

Spring Security 默认会跳转到 /login 页面或者返回 HTML,但我们是 REST API,必须让它返回 JSON 格式的错误信息。可以通过自定义 AccessDeniedHandlerAuthenticationEntryPoint 实现:

@Bean
public AccessDeniedHandler accessDeniedHandler() {
    return (request, response, ex) -> {
        response.sendError(HttpStatus.FORBIDDEN.value(), "Forbidden");
    };
}

效果总结:稳定又高效的安全体系

这套安全体系上线后,我们团队反馈非常好:

  • 登录认证响应速度很快,几乎不影响原有接口性能。
  • 所有接口都可以通过 @PreAuthorize("hasRole('ADMIN')") 控制权限,开发效率大幅提升。
  • 用户权限变更实时生效,无需重启服务。
  • 安全防护做得更规范了,审计时也没有暴露明显的漏洞。

而且随着后续接入 OAuth2 和双因素认证,这套架构也具备良好的扩展能力。


经验分享:给初学者的一些建议

如果你正准备上手 Spring Security,这里是我的几点建议:

  1. 不要怕它难,它其实比你想象的好理解 —— 先跑起来一个最简的例子,再逐步增加功能。
  2. 关注版本差异,官方文档有时更新滞后 —— GitHub 和社区问答网站有时候更有帮助。
  3. 一定要做测试! 使用 Postman 或 curl 测试每个接口的安全限制是否生效。
  4. 权限模型设计要提前规划好 —— 千万别临时改数据库表结构,否则后期麻烦。
  5. 结合日志排查问题更快捷 —— Spring Security 有很多 debug 日志输出开关,善用它们可以节省大量时间。

结语:安全从来不是附加项

写完这篇回顾性的文章,我也深刻体会到,在一个系统中,“安全”其实从一开始就应该是架构的一部分,而不是事后补上的功能。Spring Security 可以帮助我们在早期就把安全体系构建起来,也能让后续的维护变得更轻松。

希望这篇真实经历能对正在学习或准备使用 Spring Security 的你有所帮助。如果你在使用过程中遇到任何问题,欢迎留言交流,我们一起成长 🌱

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝