Spring Security实战:一个后端工程师的认证系统搭建手记

Django老掌柜
2025-06-27 10:40
阅读 547

开篇:为什么我要写这篇文章?

开篇:为什么我要写这篇文章?

作为一名有五年工作经验的后端工程师,我参与过多个中小型项目的开发与维护工作。其中一个让我印象最深的项目,是去年为一家金融公司重构其后台系统的用户权限模块。当时我们决定使用Spring Security来实现安全认证机制。

说实话,那时候我对Spring Security的理解还停留在“听说过”的阶段。在真正上手之后才发现,这个框架虽然功能强大、扩展性强,但它的学习曲线也并不平缓。尤其在面对实际业务场景时,很多细节处理不当就可能带来严重的安全隐患或性能问题。

所以我希望借这篇文章,把我这段时间的踩坑经历、解决方案和心得总结分享出来,帮助像我当初一样刚入门的同学少走一些弯路。


问题描述:认证系统搭建前的混乱局面

问题描述:认证系统搭建前的混乱局面

我们的项目最初是一个遗留系统,原有的用户认证部分完全是手动拼接出来的逻辑:

  • 登录接口直接用明文校验用户名密码
  • 权限控制基本靠前端判断(你没看错)
  • 用户角色信息存在session里,没有做持久化存储
  • 没有任何日志记录和失败尝试次数限制
  • 前后端混杂,接口权限难以统一管理

这种做法在系统用户量还小的时候勉强能应付,但随着产品逐渐上线推广,暴露的问题越来越多。尤其是在一次安全扫描中,我们被指出了几个高危漏洞,包括:

  1. 密码未加密存储
  2. 存在暴力破解风险
  3. 缺乏访问审计能力
  4. 接口没有严格的权限隔离

这让我们不得不正视系统的安全性问题,于是启动了用Spring Security重构认证授权体系的计划。


解决方案:引入Spring Security进行整体改造

解决方案:引入Spring Security进行整体改造

我们的目标很明确:在不影响现有系统稳定性的前提下,逐步引入Spring Security,提升系统安全性。以下是我们在改造过程中采用的一些关键策略:

✅ 技术选型与架构设计

  • 使用 JWT 替代 Session 实现无状态认证
  • 结合 Redis 管理令牌的吊销和有效期
  • 数据库层面增加了 用户角色权限表结构
  • 使用 Spring Data JPA 做数据持久层
  • 接口鉴权统一通过 Spring Security Filter Chain 控制

🧩 核心流程

登录 → 生成 JWT → Redis 缓存 → 后续请求带 Token → Filter 解析验证 → 授权访问对应资源


代码实践:如何快速构建认证系统

代码实践:如何快速构建认证系统

下面我会从实际工程角度出发,列出几个核心配置类和关键代码片段,并附上一些必要的说明。

🧱 表结构设计

CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN DEFAULT TRUE,
    create_time DATETIME
);

CREATE TABLE sys_role (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL
);

CREATE TABLE sys_user_role (
    user_id BIGINT NOT NULL,
    role_id INT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES sys_user(id),
    FOREIGN KEY (role_id) REFERENCES sys_role(id),
    PRIMARY KEY (user_id, role_id)
);

提示:建议将用户敏感字段(如密码)单独存储在一个独立数据库中,以提高数据安全性。


🔐 用户实体与权限集成

@Entity
@Table(name = "sys_user")
public class User implements UserDetails {
    @Id
    private Long id;
    private String username;
    private String password;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "sys_user_role",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> authorities;

    // 忽略其他getter/setter
}

这里的关键点是 UserDetails 接口的实现,以及多对多关系映射到Spring Security所需的 GrantedAuthority 集合。


⚙️ 配置类核心设置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

        return http.build();
    }

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

这段代码定义了整个安全过滤链的基础结构,包括:

  • 禁用 CSRF
  • 设置为无状态模式
  • 加入 JWT 自定义过滤器
  • 配置 URL 访问权限规则

📡 JWT 工具类实现(简化版)

@Component
public class JwtTokenProvider {

    private final String JWT_SECRET = "your-secret-key";

    public String generateToken(Authentication authentication) {
        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
        return Jwts.builder()
            .setSubject(userPrincipal.getUsername())
            .setExpiration(new Date(System.currentTimeMillis() + 86400000))
            .signWith(SignatureAlgorithm.HS512, JWT_SECRET)
            .compact();
    }

    public String getUsernameFromToken(String token) {
        return Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token);
            return true;
        } catch (JwtException ex) {
            // log error
        }
        return false;
    }
}

微服务架构示意图-1


👤 认证处理器

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = getTokenFromRequest(request);
        
        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsernameFromToken(token);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest req) {
        String bearerToken = req.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

系统架构设计图-2


踩坑经验:那些年我和Spring Security一起犯过的错

❌ 初期忽略的角色加载问题

刚开始我们以为只要返回了 UserDetails 就可以自动拿到对应的权限列表,结果发现接口一直报 403 Forbidden,后来排查才意识到是因为:

如果你的角色信息不是直接通过内存配置的,而是从数据库查询来的,那么一定要确保 UserDetailsgetAuthorities() 返回值正确!

这个问题花了一个上午定位,教训是:不要忽视Spring Security内部的权限传递流程。


❌ 忘记关闭 Session 并导致 JWT 无效

我们在使用 JWT 之前忘记禁用 Session,结果有些接口仍然会在响应头中出现 Set-Cookie,导致客户端误以为是有状态服务。

解决方式是加上如下配置:

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

❌ 多 Filter 配置顺序错误

Spring Security 的过滤器链顺序非常关键。比如我们在添加 JWT 过滤器时放错了位置,导致某些请求根本没有经过 JWT 的解析,从而出现匿名访问问题。

正确的做法是:

.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

这样能保证在执行默认的用户名密码认证之前,先完成自定义的身份识别。


❌ 忽视注销机制导致 Token 可能长期有效

一开始我们只做了 Token 的颁发,却没有处理注销机制。后果就是一旦 Token 泄露,它在整个生命周期内都可正常使用。

后来通过 Redis 缓存 Token 的黑名单(吊销列表),配合拦截器提前检测 Token 是否合法,实现了更完善的注销逻辑。


效果总结:改造后的收益

经过几周的重构后,我们的认证系统发生了明显变化:

维度 改造前 改造后
安全性 极低,存在高危漏洞 满足 OWASP 基本标准
性能 登录过程慢,依赖 session 锁 全部无状态,吞吐量上升约 30%
扩展性 新增权限困难 角色权限统一管理,新增角色只需 DB 操作
日志追踪 几乎无审计日志 登录登出/失败尝试均有完整记录

不仅如此,我们还借此机会完成了以下几个改进:

  • 引入登录失败锁定机制(支持动态阈值)
  • 增加密码复杂度限制
  • 为不同角色分配不同的 API 接口权限
  • 实现基于 IP 的白名单访问控制(用于管理员操作)

这些优化让整个系统的安全性有了质的飞跃。


经验分享:给正在路上的你的一些建议

作为一个经历过这一套流程的老兵,我想给刚接触Spring Security或者打算重构认证系统的同学几点建议:

🔑 1. 安全意识比框架更重要

掌握Spring Security不是最终目的,重要的是理解安全背后的基本原则:

  • 最小权限原则
  • 输入验证原则
  • 明确身份与权限分离
  • 日志不可缺位
  • 注销机制必须完善

框架只是工具,安全才是核心。


💡 2. 不要急着追求完美架构

特别是对于已经有大量业务逻辑的项目来说,重构认证模块应该循序渐进:

  • 先从登录/登出开始接入 Spring Security
  • 再逐步替换原有硬编码的权限逻辑
  • 最后统一接口的权限注解管理

每一步都要经过充分测试和灰度发布。


🛠️ 3. 善于利用调试工具

  • 使用 Postman 测试接口的权限行为
  • 查看 Security 的 Debug 输出,确认过滤链是否触发正确
  • 利用 Spring Boot Actuator 查看实时的权限状态

记住:Spring Security 默认是“拒绝一切”,所以一开始可能会觉得处处受限,但这是好事。


🧪 4. 编写单元测试验证权限逻辑

尤其是涉及 RBAC(基于角色的访问控制)的部分,可以通过 MockMvc 编写模拟请求测试不同角色的访问结果:

@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void testAdminAccessOnlyApi() throws Exception {
    mockMvc.perform(get("/api/admin"))
           .andExpect(status().isOk());
}

这种方式可以有效防止因权限修改带来的回归风险。


🔄 5. 和前端协作好 Token 的管理机制

前后端在处理 Token 生命周期时需要高度同步,比如:

  • Token 有效期与刷新时机
  • 登录超时跳转逻辑
  • Token 自动刷新机制
  • 多 tab 页面间共享 Token 的处理

否则容易出现权限混乱的情况。


结语:安全永无止境,进步不止于此

回头来看,那次Spring Security的重构不仅仅是一个技术升级,更像是我们团队安全意识集体觉醒的过程。通过这次改造,我们不仅解决了眼前的隐患,也建立了一套可持续发展的权限治理体系。

现在再回过头来看,如果你问我 Spring Security 到底难不难?我觉得答案取决于你是想用它跑通个 Demo,还是真正让它成为保障系统安全的防线。

框架易学,安全不易。

希望你在学习 Spring Security 的过程中,也能建立起对系统安全更深的理解。别怕踩坑,每一次折腾其实都是成长的机会。

最后,送大家一句话:

“一个好的程序员不仅要写出别人看不懂的代码,更要写出别人不敢轻易攻击的系统。”

共勉 😊


如果你也在实践中遇到了类似问题,欢迎留言讨论。

评论 0

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