Spring Security实战:从零搭建安全认证系统

注解魔法师
2025-06-22 11:33
阅读 576

开始的契机

开始的契机

去年我接手了一个中小型企业的 SaaS 项目,主要目标是重构他们原来的平台后端服务。这个平台涉及用户管理、权限划分、数据隔离等典型场景。由于原有系统安全性堪忧,客户提出了一个硬性要求:所有接口必须经过统一的权限控制,防止越权操作和信息泄露

说实话,刚接到这个需求的时候,我还是有点发怵的。虽然Spring生态我已经用得挺熟,但Security这一块在之前的项目里主要是“配个默认登录页”那种程度。这次不同了,要真真正正从头搭起一套完整、可扩展、性能又不差的认证授权体系。

今天就来聊聊我是怎么一步步把这个“安全防线”搭建起来的,以及过程中踩过的坑和一些经验教训。


真实挑战来了

真实挑战来了

我们当时的系统背景是基于 Spring Boot + MyBatis 的微服务架构,前端是 Vue.js,前后分离。业务上需要支持:

  • 用户注册 / 登录
  • 基于角色的权限控制(RBAC)
  • RESTful 接口访问控制(例如 /api/user/** 只有管理员能访问)
  • 登录状态维护(会话过期、并发登录限制等)
  • 安全审计(谁在什么时间执行了哪些操作)

当时我们团队遇到的第一个问题就是:“现在有很多安全框架和组件,该如何选型并快速落地?

比如:

  • 用Shiro还是Spring Security?
  • 是用Session还是JWT?
  • 如何把认证和授权模块设计得既灵活又解耦?

我们最终选择了 Spring Security + JWT + 自定义权限注解 的方案。原因有几个:

  1. 团队对 Spring Boot 比较熟悉,迁移成本低;
  2. Security 功能完备且社区活跃,文档丰富;
  3. JWT 无状态特性适合未来的微服务拆分。

我的设计思路与实现过程

第一阶段:基础认证功能落地

首先我把整个认证流程划分为几个核心步骤:

登录 -> 认证 -> 生成Token -> 返回客户端 -> 后续请求携带Token -> 校验有效性 -> 放行/拦截

核心依赖引入:

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

登录接口 & Token发放

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthService authService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        String token = authService.authenticate(request.getUsername(), request.getPassword());
        return ResponseEntity.ok().header("Authorization", "Bearer " + token).build();
    }
}

authService.authenticate() 内部调用了 AuthenticationManager.authenticate() 方法进行用户名密码验证,并通过自定义 UserDetailsService 查询数据库中的用户信息。

缓存策略对比-2

JWT工具类封装

public class JwtUtils {
    private static final String SECRET_KEY = "your_secret_key_here";
    private static final long EXPIRATION = 86400000; // 24小时

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

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

实现 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("用户不存在");
        }

        return User.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRoles().toArray(new String[0]))
                .build();
    }
}

这时候,基础的认证流程就跑通了。


第二阶段:权限控制

Spring Security 提供了非常强大的方法级权限控制功能,比如:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<User> getAllUsers() {
    return userService.findAll();
}

为了启用这个功能,别忘了加上配置:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/auth/login").permitAll()
            .anyRequest().authenticated();
    }
}

这里有个关键点,我们在过滤器链中添加了一个自定义的 JWT 过滤器,在每次请求到达 controller 前校验token是否合法,并填充 SecurityContextHolder,让后续逻辑能识别出当前用户身份。


第三阶段:日志与审计

为了满足客户的审计需求,我还设计了一张简单的安全日志表:

CREATE TABLE security_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(64),
    action VARCHAR(255), -- 如“访问了/users接口”
    ip_address VARCHAR(128),
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);

然后使用 AOP 技术自动记录安全事件:

@Aspect
@Component
public class SecurityAuditAspect {

    @Autowired
    private SecurityLogService logService;

    @AfterReturning("execution(* com.example.controller.*.*(..)) && @annotation(secured)")
    public void auditAccess(JoinPoint joinPoint, Secured secured) {
        String methodName = joinPoint.getSignature().getName();
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        String ip = getCurrentRequestIpAddress();

        logService.saveLog(username, "访问了方法:" + methodName, ip);
    }

    private String getCurrentRequestIpAddress() {
        // 从RequestContextHolder获取HttpServletRequest
        ServletRequestAttributes attributes = 
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return attributes != null ? attributes.getRequest().getRemoteAddr() : "";
    }
}

这样就能记录到谁在何时做了哪些敏感操作。


踩过的坑与解决之道

坑1:跨域(CORS)导致Token无法传递

现象: 浏览器控制台报错 “No 'Access-Control-Allow-Origin' header present”,并且 Authorization Header 没有被正确携带。

解决方法:

在 Security 配置中显式开启 CORS 支持,并在网关层做好域名白名单设置。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.cors().and()
        // 其他配置...
}

同时配置 CORS 映射:

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
    configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
    configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
    configuration.setExposedHeaders(Collections.singletonList("Authorization"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

坑2:JWT签名不过期?

现象: 用户修改密码后,原来的token仍然有效!

解决方案:

  1. 设置合理的过期时间(通常不超过24小时);
  2. 使用 Redis 缓存黑名单机制,当用户登出或修改密码时,把对应token加入黑名单,并在每次请求时检查是否存在黑名单中;
  3. 在签发token时加入用户版本号(如用户每次修改密码,增加 version 字段),token中也包含该字段,验证时比对。

坑3:权限表达式配置错误导致接口全开放

这个问题差点酿成线上事故。

原本写的路径匹配规则是这样的:

.antMatchers("/api/user/**").hasRole("USER")

结果发现其他角色也能访问。后来才明白,hasRole("USER") 相当于自动加了 "ROLE_" 前缀,而我们的角色名是直接保存为 USER、ADMIN 的。应该改成:

.antMatchers("/api/user/**").hasAuthority("USER")
// 或者
.hasRole("USER") → 需要角色名是 ROLE_USER 才生效

这是一个非常容易混淆的细节,建议尽量使用 hasAuthority(),或者规范你的角色命名方式。


最终效果与收获

缓存策略对比-1

上线几个月后,整体运行稳定。安全方面没有出现过明显的漏洞,审计日志也帮助我们查到了几次异常访问行为。这套安全体系支撑了我们后续多个子系统的接入。

最让我欣慰的是:

  • 登录流程流畅
  • 权限控制粒度细
  • 日志清晰可追溯
  • 新人上手快,因为结构清晰、组件解耦做得不错

而且,由于采用了 JWT 和无状态设计,未来向微服务架构迁移也变得轻松不少。


给读者的一些建议

如果你正在构建自己的认证授权系统,以下几点是我的经验总结:

✅ 优先考虑系统演进方向

如果未来计划使用微服务,那一定不要使用 Session。JWT 是目前比较主流的做法,它天然适合分布式部署。

✅ 数据库设计很重要

权限模型不能太简单粗暴。用户、角色、权限、菜单这四张表的关联关系,直接影响到你能否方便地做权限配置。可以参考 RBAC 模型,合理设计中间表。

✅ 把 Security 拆成独立模块

如果你的项目比较大,可以把 Security 模块抽出来作为一个公共组件,比如 auth-center,其他服务通过 OpenFeign 或 RestTemplate 调用完成认证。

✅ 多留监控点

包括但不限于:

  • 登录次数限制(防暴力破解)
  • 异地登录通知(提升用户体验)
  • 黑名单机制
  • 审计日志保留期限
  • 敏感操作二次确认(如删除数据)

✅ 保持学习更新意识

现在市面上已经有更高级的安全协议,比如 OAuth2、OpenID Connect,甚至 CAS、OAuth2 Resource Server 等标准协议的集成方案也越来越成熟。Spring Security 对这些都有良好的支持,值得花时间去研究。


结语

写这篇文章的过程,也是我在回顾过去一年成长的过程。记得刚开始接触Spring Security时,连基本的过滤器链都看得晕乎乎的。如今,已经能够根据实际业务需求定制化开发。

我希望通过分享我的经历,可以帮助到同样走在后端安全路上的朋友。真正的安全不是一句“配个拦截器”那么简单,它是一个长期投入的过程,是对业务、技术和人性的综合理解。

最后,感谢你耐心读到这里。如果你有任何疑问或者想要更详细的代码结构,欢迎留言交流,我会尽快回复。

共勉!

评论 0

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