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

LogicBuilder
2025-06-14 22:30
阅读 351

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

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


引言:从一次重构说起

大概是一年多前,我参与了一个公司内部的后台管理系统的重构项目。原来的老系统是用 PHP 写的,架构老旧、安全性差得一塌糊涂,连用户权限都只能靠前端判断,后端几乎没有做任何校验。更别说登录安全、会话管理这些现代 Web 应用的标准操作了。老板对这套系统非常不满,要求我们在 3 个月内完成全部重构,并且“必须做到真正的安全认证”。

接到任务的时候,我心里其实挺没底的。虽然之前在其他项目里用过一些简单的认证逻辑,比如基于 token 的 JWT 验证,但真正系统性地落地一个安全体系,我还是第一次。

那段时间我查了很多资料,也踩了不少坑。直到最后,我们用了 Spring Security 搭建起了一套完整的、满足业务需求的安全认证机制。这篇文章就是想结合那次实战经验,讲讲我是怎么一步步搭建出这个系统的。


项目背景与挑战

新系统是典型的 Java 后台管理平台,使用 Spring Boot + Vue 的前后端分离结构。主要功能包括:

  • 用户管理(创建、编辑、删除)
  • 角色权限控制
  • 接口访问控制
  • 日志审计
  • 登录认证和退出机制

安全方面的需求有几个关键点:

  1. 所有接口必须做身份认证和权限控制;
  2. 支持用户名密码 + 短信验证码双重登录;
  3. 用户登录失败次数超过一定限制要封号;
  4. 登录 session 要有过期机制,支持主动登出;
  5. 权限需要细粒度控制(RBAC 模型);

当时我们评估了多个方案,最终决定用 Spring Security 作为整个安全框架的核心。


技术选型与实现思路

为什么选择 Spring Security?

虽然 Shiro 也是一个很流行的 Java 安全框架,但从以下几个角度考虑,我们还是选择了 Spring Security:

  • 更加成熟稳定,社区活跃,文档丰富;
  • 对 RESTful 接口天然支持良好;
  • 可高度定制,扩展性强;
  • 提供丰富的开箱即用功能(如登录限制、CSRF 防护、OAuth2 集成等);
  • 与 Spring Boot 整合极为顺畅;

总体设计思路

我们的整体认证流程大致如下:

用户登录请求 → 自定义 AuthenticationProvider 处理认证 → 成功后生成 Token 或设置 Session →
→ 请求携带凭证(Header/Bearer/Session) → FilterChainProxy 过滤链验证权限 →
→ 访问目标资源或返回 401/403

整个过程涉及几个核心组件:

  • UserDetailsService:用于加载用户信息;
  • PasswordEncoder:密码加密解密;
  • AuthenticationManager:负责认证流程;
  • SecurityFilterChain:过滤器链配置;
  • AccessDecisionManager:权限决策;
  • LogoutHandlerLogoutSuccessHandler:注销处理;
  • ExceptionTranslationFilter:处理异常情况(如未认证/无权限访问);

实战代码实践

接下来我会结合具体代码片段来说明如何搭建这样一个系统。

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>

我们这里用到了 JWT,所以额外引入了 JJWT 相关的依赖。

2. 定义用户实体与权限模型

我们的用户表设计大致如下:

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 TABLE sys_role (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  role_name VARCHAR(50) NOT NULL
);

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

-- 权限粒度到菜单级别
CREATE TABLE sys_menu (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  menu_name VARCHAR(50),
  permission VARCHAR(100)  -- 如 system:user:read
);

这样我们就可以通过 RBAC 模型进行权限控制。

3. 配置 Security 核心类

我们定义了自己的 UserDetailsService 实现类:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    private UserService userService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(), 
            user.getPassword(), 
            authorities
        );
    }
}

注意这里的权限格式要符合 Spring Security 的标准格式,如 ROLE_ADMIN 或自定义的权限字符串。

4. 自定义认证逻辑:支持多类型登录

我们实现了自己的 AuthenticationProvider

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserService userService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String loginType = (String) authentication.getDetails();
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        // 可以根据 loginType 判断是手机号登录还是普通账号登录
        // 在这里调用不同服务去校验验证码或者密码
        // 示例中假设只走用户名密码方式
        User user = userService.loadUserByUsername(username);
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }

        List<GrantedAuthority> authorities = new ArrayList<>();
        user.getRoles().forEach(r -> authorities.add(new SimpleGrantedAuthority(r.getName())));
        
        return new UsernamePasswordAuthenticationToken(username, password, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

同时注册进 Security 中:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authenticationProvider(customAuthenticationProvider); // 注册自己的 provider
    
    http.formLogin(formLogin -> formLogin.disable())
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationToken.class)
        .authorizeHttpRequests(auth -> auth.anyEndpoint().authenticated());
    
    return http.build();
}

5. JWT 校验过滤器

负载均衡配置-2

我们实现了一个轻量级的 JWT 校验 Filter:

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

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

踩过的坑和解决方法

问题 1:跨域请求被拦截导致无法登录

由于我们用的是前后端分离架构,登录请求从 Vue 前端发送过来,一开始总被 Spring Security 拦截。调试半天发现是因为 CSRF 保护默认开启。

解决方案:
如果你是 REST API 架构,而且用了 Token 或者 Session 方式,可以直接关闭 CSRF。

.csrf(csrf -> csrf.disable())

当然如果你要用 Cookie + Session 的传统方式登录,那就不能关闭 CSRF,而是要在前端每次请求带上 _csrf token。

问题 2:登录成功不跳转或返回 JSON 错误

Spring Security 默认是面向浏览器页面的,登录成功会重定向。但我们这里是纯后端接口,需要返回 JSON。

解决方案:
自定义 AuthenticationSuccessHandlerAuthenticationFailureHandler

.successHandler((request, response, authentication) -> {
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write("{\"code\":200,\"message\":\"登录成功\"}");
})
.failureHandler((request, response, ex) -> {
    response.setContentType("application/json;charset=UTF-8");
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.getWriter().write("{\"code\":401,\"message\":\"" + ex.getMessage() + "\"}");
});

微服务架构示意图-1

问题 3:权限表达式写错导致 403

一开始我们在 controller 上写了类似 @PreAuthorize("hasAuthority('system:user:list')"),结果总是提示 403。

排查原因:

  1. 用户实际加载的权限不是字符串 "system:user:list",而是被自动加上了 ROLE_ 前缀;
  2. hasAuthority()hasRole() 的区别没有搞清楚;

修正方法:

  • 明确使用 hasAuthority("system:user:list")
  • 或者统一用 hasAnyAuthority(...)
  • 如果你非要用 Role 的方式,确保传入的权限是带 ROLE_ 前缀的字符串。

实施效果与收获

在项目上线后,我们这套安全体系表现得非常稳定。以下是我们总结的一些成果:

  • 系统安全性得到了大幅提高,所有接口都必须通过认证;
  • 权限粒度细化到菜单级别,管理员可以灵活配置;
  • 登录失败限制机制有效防止了暴力破解;
  • 使用 JWT 代替了传统的 Session,更适合分布式部署;
  • 日志清晰记录了用户的登录行为和访问路径;
  • 新增用户时自动继承角色权限,减少配置成本;

开发过程中我也更加深刻地理解了 Spring Security 的运作机制,尤其是:

  • FilterChain 是怎样一步一步处理请求;
  • 认证流程中的责任链是如何串联起来的;
  • 方法级别的权限控制是怎么落地的;

经验分享:给后端同学的一些建议

如果你也在准备搭建一个类似的认证系统,我可以给你几点建议:

✅ 把 Spring Security 当作一个可插拔的模块来看待

它不是“黑盒”,也不是“万能钥匙”。你可以按需替换每一个组件。比如:

  • 替换掉默认的 PasswordEncoder 使用你自己的算法;
  • 替换掉登录失败处理器自己写逻辑;
  • 修改默认的 Filter 顺序,加入你的 Token 校验逻辑;

这正是 Spring Security 的强大之处。

✅ 认真处理好权限模型的设计

很多系统之所以后面改不动,都是因为一开始权限模型没设计好。建议从三个维度考虑:

  • 用户维度:是否启用、是否锁定、登录历史;
  • 角色维度:角色之间是否有继承关系;
  • 权限维度:权限划分是否合理,是否存在冗余。

✅ 提前规划好日志与审计功能

不要等到出了问题才想着补审计日志。Spring Security 提供了很好的监听机制,你可以轻松实现:

  • 用户登录/退出事件监听;
  • 权限变更追踪;
  • 接口访问频率统计;
  • 敏感接口访问记录等。

✅ 关注性能与并发问题

特别是在高并发场景下,频繁查询数据库可能会成为瓶颈。你可以:

  • 缓存权限信息(如 Redis);
  • 使用异步的方式记录审计日志;
  • 分布式锁控制登录频率限制;
  • 使用本地缓存避免重复查询。

结语:安全从来不是可选项

写到这里,我想起一句话:“系统安全就像穿衣服,平时可能觉得麻烦,关键时刻却至关重要。”

在那个项目之后,我越来越意识到:安全从来不是锦上添花的功能,而是一个产品最基础的生命线。

Spring Security 不完美,但它给了我们构建坚实安全体系的良好起点。只要你肯花时间去理解它的设计理念和实现机制,就一定能打造出既安全又高效的服务。

希望这篇文章能帮助你少踩点坑,更快地上手 Spring Security,建立起属于你自己的安全防线。如果有什么疑问,欢迎留言交流。技术路上一起成长 🙌


(全文约 3272 字)

评论 0

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