Spring Security基础:快速搭建安全认证系统

沉默的架构师
2025-06-14 09:44
阅读 260

用Spring Security快速搭建安全认证系统:我的一次实战经历

去年年底,我所在的团队接手了一个新的项目,目标是为公司内部的一套业务管理平台搭建一个统一的用户认证和权限管理系统。这套平台涉及多个部门的协作模块,用户群体包括管理员、普通员工以及部分合作第三方,因此我们面对的不仅是传统的登录功能,还包括多角色权限控制、OAuth2集成等较为复杂的场景。

最开始的时候,团队内部曾经讨论过几个技术选型方案:比如使用Shiro或者直接基于JWT手写一个认证框架。但最终,我还是力挺了 Spring Security —— 虽然它一开始的学习曲线稍微陡峭了一些,但在企业级系统中,它的灵活性、安全性以及生态支持都极具优势。

这篇文章我会结合那次项目的具体背景,分享一下我们是怎么通过Spring Security在两周之内搭建起一套完善的安全认证体系的,并谈谈过程中踩过的坑、学到的经验。


项目背景与挑战

我们的项目是一个典型的后端服务,前后端分离架构,前端采用Vue,后端使用Spring Boot + MyBatis。主要的功能模块包括:

  • 用户登录/注册
  • 基于角色的访问控制(RBAC)
  • 支持第三方 OAuth2 登录(未来可能接入钉钉/企业微信)
  • 日志审计:记录每次成功或失败的登录尝试

最核心的需求是 统一身份认证。在此之前,各个业务模块有各自独立的用户体系,导致权限难以统一管理,用户体验差,运维复杂度高。

我们在初期遇到了几个关键问题:

  1. 认证流程设计混乱:不同接口需要不同的鉴权方式,有些接口允许游客访问,有些则需要特定角色。
  2. 权限粒度过粗:仅靠Role控制权限不够灵活,希望引入类似Permission细粒度控制。
  3. Token生成与验证机制不明确:原本考虑完全基于Session,但考虑到后期要扩展移动端,最终决定采用无状态的JWT方案。
  4. 对Spring Security不熟悉:团队成员之前基本没怎么深入使用过Security,上手需要时间,项目进度又紧。

技术选型对比与最终决策

为了满足这些需求,我们做了初步的技术评估,对比了几种主流认证方案:

框架 特点 适用场景
Shiro 学习成本低,配置简洁 简单的小型项目或SSM架构项目
Spring Security 功能强大,适合企业级应用,支持OAuth2/JWT 中大型系统,需高度定制化安全策略
自研基于JWT 控制粒度大,但开发维护成本高 定制化极强且有足够人力投入
Keycloak/CAS 已有的统一认证中心方案 大型企业,已有成熟IDaaS体系

考虑到我们的系统后续很可能作为其他系统的身份提供者(IdP),并且我们需要快速上线,同时希望尽可能利用现有生态减少重复造轮子,最终选择了 Spring Security + JWT + OAuth2 Client模式 的组合。

API接口文档-2

我们没有一开始就上OAuth2授权服务器那一套,而是先实现了基于JWT的Basic认证流程,预留好对接OAuth2客户端的接口。


整体架构设计思路

整体认证流程大致如下:

  1. 用户提交用户名密码到/auth/login接口;
  2. 后端校验用户信息,签发JWT Token;
  3. 客户端将Token保存在localStorage中;
  4. 后续请求自动携带Authorization头(Bearer方式);
  5. Spring Security拦截请求,解析Token并设置当前用户上下文;
  6. 权限控制层(如@PreAuthorize)进行逻辑判断。

其中,我们重点关注以下几个模块的设计:

数据库设计

我们采用RBAC模型,主要表结构如下:

  • sys_user: 用户表,存储账号信息
  • sys_role: 角色表,定义各类角色
  • sys_permission: 权限表,定义每个具体的权限项(如“订单列表”、“新增员工”)
  • sys_user_role, sys_role_permission: 多对多关系表

这样的设计使得权限可以动态配置,避免硬编码。

接口设计考量

在安全设计上,有几个关键点需要注意:

  • 登录接口必须加密传输(HTTPS),防止明文密码泄露;
  • 返回的Token应包含有效期、用户信息、权限信息等;
  • 所有敏感接口都应启用权限校验;
  • 错误处理要统一,比如返回401(未授权)、403(权限不足)等标准HTTP状态码。

快速搭建步骤详解

下面我会带你一步步走一遍基础安全模块的搭建过程。

Step 1: 引入依赖

<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>

我们选用JJWT这个库来处理JWT生成与解析。

Step 2: 自定义JWT工具类

我们封装了一个简单的JwtUtils类,用于生成和解析Token:

public class JwtUtils {
    private static final String SECRET = "your-secret-key";
    private static final long EXPIRATION = 864_000_000; // 十天

    public static String generateToken(String username, Collection<? extends GrantedAuthority> authorities) {
        return Jwts.builder()
                .setSubject(username)
                .claim("authorities", authorities.stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toList()))
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    public static String parseToken(String token) {
        return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getSubject();
    }
}

Step 3: 实现UserDetailsService

实现Spring Security中的核心接口:

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserService userService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        List<SimpleGrantedAuthority> authorities = user.getRoles().stream()
            .flatMap(role -> role.getPermissions().stream())
            .distinct()
            .map(p -> new SimpleGrantedAuthority(p.getCode()))
            .collect(Collectors.toList());
        
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

这里我们把用户的权限(permission code)以authority的方式注入到Spring Security上下文中,以便后续使用@PreAuthorize注解做权限判断。

Step 4: 编写FilterChain配置

创建SecurityConfig类,配置过滤链路:

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationStrategy.STATELESS)
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .build();
    }
}

上面我们关闭了CSRF保护(因为用的是JWT),禁用了Session,并添加了一个自定义的Filter去拦截Token。

Step 5: 自定义JWT Filter

public class JwtAuthenticationFilter extends OncePerRequestFilter {

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


![系统架构设计图-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061409/7c236390-eff6-4cd8-af3c-30f5e2788948.jpg)


    // 提取Token逻辑省略...
    // 验证及获取权限逻辑也省略...
}

这一步最关键的是如何把JWT中的信息绑定到Spring Security上下文中,这样后面的@PreAuthorize才能生效。


开发过程中踩过的几个坑

虽然整个流程看起来很顺畅,但在实际开发中还是遇到了不少问题:

坑一:Spring Security默认拒绝OPTIONS预检请求

这个问题出现在前端发起跨域请求时。OPTIONS请求会被Spring Security拦截并返回403。

解决方案:

在SecurityConfig里加一句:

http.cors().configurationSource(corsConfigurationSource());

然后定义一个CORS配置Bean即可解决。

坑二:权限表达式中的SpEL异常难定位

Spring Security的权限表达式非常强大,但也容易出错。例如我们曾经误用了#request.header.userId这种变量,结果一直报错。

建议:

  • 使用@EnableGlobalMethodSecurity(prePostEnabled = true)开启方法级别的权限控制;
  • 在Controller中使用@PreAuthorize("hasAuthority('perm_code')")这种方式控制接口权限;
  • 权限名尽量保持一致,便于调试;

坑三:JWT的签名算法混淆

刚开始时,我们用了HMAC256,后来想换成RSA非对称加密,结果在解析的时候一直报签名不匹配。

经验总结:

  • 使用对称加密(HS256)简单方便,适合测试环境;
  • 生产环境下建议使用RS256,私钥由认证服务掌握,资源服务只存公钥;
  • 密钥的管理和分发也要注意安全。

上线后的效果与收益

经过两周时间,我们顺利交付了第一版安全认证系统。后续我们也陆续接入了OAuth2客户端(如钉钉扫码登录),并通过Spring Security整合到了统一认证流程中。

上线后的效果体现在几个方面:

  1. 统一了用户管理体系:所有子系统的用户权限都在一个地方管理;
  2. 提升了权限控制精度:不再只是基于Role,而是细化到了具体Action;
  3. 开发效率提升:有了Spring Security的基础模板,新业务模块只需复制粘贴+改一点配置即可;
  4. 性能表现良好:在压测环境中,QPS稳定在5k以上,JWT解析对性能影响几乎可以忽略。

值得一提的是,我们将Security日志单独收集,通过ELK分析登录成功率、异常行为等指标,对安全运营也有很大帮助。


给读者的一些经验和建议

最后,我想给正在学习或准备使用Spring Security的同学几点建议:

  1. 不要被文档吓退:官方文档确实厚,但实际项目中只需要掌握常用配置即可;
  2. 动手是最好的学习方式:从一个简单的登录功能开始,逐步加上权限控制、JWT、OAuth2等模块;
  3. 学会看日志:Spring Security的日志非常详细,遇到问题时打开DEBUG级别能快速定位;
  4. 结合实际业务设计权限模型:权限不是越细越好,而要符合你的业务场景;
  5. 注意安全细节:比如Token的有效期、加密方式、防暴力破解策略等,别让漏洞成为系统短板。

如果你还在用Session,不妨试试JWT + Spring Security这套组合拳,特别是在分布式系统中,你会发现它真的能帮你少掉很多头发😄。


写在最后

Spring Security并不是最难的技术,但它确实是一道门槛。很多人止步于那堆配置类和FilterChain,但只要真正沉下心去做一个完整的项目,你会发现它其实是非常强大的,而且一旦搭好了基础框架,后续开发会快得飞起。

我也经历过那个阶段,从最初看到Security的配置文件就头皮发麻,到现在可以熟练地根据需求定制Filter、实现多租户认证……这些成长都来自一个个真实的项目打磨。

希望这篇真实分享对你有所帮助,也希望你能在自己的Spring Security项目中少走弯路,写出更优雅的安全代码。

评论 0

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