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

Prompt造梦师
2025-12-13 09:03
阅读 668

上周五晚上9点半,我正一边听着 Radiohead 的《Creep》循环播放,一边盯着屏幕上那个该死的 403 Forbidden 错误发呆。这是我在新公司入职后的第二个月,刚被分配到一个内部管理后台的重构项目——前端用 Vue3,后端是 Spring Boot,看起来人畜无害。结果,产品经理轻飘飘一句:“咱们这个系统得加个登录和权限控制哈”,直接把我送进了安全框架的深水区。

更离谱的是,第二天就是 Sprint 评审会,而我的本地连个 /login 都打不开。那一刻我真的想把 MacBook 直接从窗户扔出去——可惜办公室在25楼,物业不允许高空抛物。

但吐槽归吐槽,活儿还得干。作为一个 DevOps 工程师(虽然现在被迫写业务代码),我深知“安全不是功能,而是底线”。更何况,再过两周就是季度技术分享会,我还打算拿这个项目去装个逼。于是,我咬咬牙,打开了 Spring Security 的官方文档……然后又关了。说实话,那玩意儿比《量子力学导论》还劝退。

不过别慌!经过三天两夜的踩坑、Google、Stack Overflow 和无数次重启应用,我终于搞定了一个极简但生产可用的安全认证系统。今天就来手把手带你搭一套,顺便聊聊我在过程中踩过的雷——说不定哪天你面试就被问到这些“陷阱题”。


为什么选 Spring Security?Python 不香吗?

先说清楚:我们后端是 Java 技术栈,Spring Boot 是主框架。虽然我个人最近沉迷 Rust(真香警告⚠️),也写过不少 Python 脚本做运维自动化,但在这个项目里,用 Django 或 FastAPI 显然不合适——团队没人维护,CI/CD 流水线也不支持。

Spring Security 虽然配置复杂,但它和 Spring Boot 天然集成,社区资源丰富,而且——最重要的是——它能扛住生产流量。去年双11期间,隔壁组用自研鉴权中间件崩了三次,最后还是靠 Spring Security 救场。血的教训啊!

另外,现在很多大厂的 Java 后端面试题里都会问:“你怎么实现 RBAC 权限控制?”、“JWT 和 Session 的区别是什么?”——如果你连 Spring Security 的基本流程都说不清,简历可能直接进碎纸机。

所以,别再幻想用一行 Flask 代码搞定安全了。现实很骨感。


快速上手:三步搭建基础认证

废话不多说,直接上干货。我们的目标是:

  • 用户通过用户名/密码登录
  • 登录成功后返回 JWT Token
  • 后续请求携带 Token 访问受保护接口
  • 支持基于角色的权限控制(比如 ADMIN 才能删用户)

第一步:依赖引入

<!-- pom.xml -->
<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

💡 小贴士:别用太老的 jjwt 版本,否则会有兼容性问题。我就因为用了 0.9.x,在生成 token 时莫名其妙报 SignatureException,debug 到凌晨两点才发现是版本锅。

第二步:用户实体与服务

我们用最简单的内存用户(实际项目请对接数据库):

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 实际项目这里查 DB,比如 UserRepository.findByUsername(username)
        if ("admin".equals(username)) {
            return User.builder()
                .username("admin")
                .password(passwordEncoder().encode("123456")) // 别用明文!
                .roles("ADMIN")
                .build();
        } else if ("user".equals(username)) {
            return User.builder()
                .username("user")
                .password(passwordEncoder().encode("123456"))
                .roles("USER")
                .build();
        }
        throw new UsernameNotFoundException("用户不存在");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 强烈建议用 BCrypt
    }
}

🚨 安全提醒:永远不要存储明文密码!BCrypt 是目前最推荐的加密方式,自带 salt,防彩虹表攻击。

第三步:核心配置 —— SecurityConfig

这才是重头戏。Spring Security 的精髓在于 Filter Chain。我们需要关闭默认的表单登录,启用 JSON 登录,并集成 JWT 校验。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 前后端分离项目通常禁用 CSRF
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
            .and()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/auth/login").permitAll()
                .requestMatchers("/actuator/**").permitAll() // 健康检查放行
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // 只有 ADMIN 能访问
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) 
            throws Exception {
        return config.getAuthenticationManager();
    }
}

注意几个关键点:

  • STATELESS:告诉 Spring 不要创建 Session,符合 JWT 无状态特性
  • addFilterBefore:在标准认证过滤器前插入我们的 JWT 拦截器
  • hasRole("ADMIN"):自动加上 ROLE_ 前缀,所以数据库里存的是 ADMIN,但 Spring 会识别为 ROLE_ADMIN

JWT 过滤器怎么写?

这是最容易出错的地方。很多人直接 copy 网上的代码,结果 token 过期了还不刷新,或者解析失败直接 500。

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain chain) throws ServletException, IOException {
        
        String token = extractTokenFromHeader(request);
        if (token != null && JwtUtil.validateToken(token)) {
            String username = JwtUtil.getUsernameFromToken(token);
            UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken auth = 
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }

    private String extractTokenFromHeader(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

配套的 JwtUtil 工具类负责生成和解析 token:

@Component
public class JwtUtil {
    private static final String SECRET_KEY = "mySecretKeyThatIsLongEnoughForHS512"; // 生产环境请用 KMS 管理!
    private static final long EXPIRATION_TIME = 86400000; // 24小时

    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
            .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
            .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false; // token 过期或无效
        }
    }

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

🔒 运维视角:SECRET_KEY 绝对不能硬编码在代码里! 我们公司用 HashiCorp Vault + CI/CD 注入,上线前自动替换。你也可以用环境变量,但别提交到 GitHub!


登录接口 & 测试

最后写个登录 Controller:

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

    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(), request.getPassword())
            );
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(401).body("用户名或密码错误");
        }

        UserDetails userDetails = customUserDetailsService.loadUserByUsername(request.getUsername());
        String token = jwtUtil.generateToken(userDetails);
        return ResponseEntity.ok(new JwtResponse(token));
    }
}

用 curl 测试一下:

curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

拿到 token 后,访问受保护接口:

curl -H "Authorization: Bearer <your_token>" http://localhost:8080/api/admin/users

如果一切顺利,你应该能看到数据;如果是普通用户 token,访问 /admin 接口会返回 403


常见面试题 & 坑点总结

问题 正确姿势
密码怎么加密? 必须用 BCryptPasswordEncoder,别用 MD5/SHA1
Token 存哪里? 前端存在 HttpOnly Cookie 最安全,其次才是 localStorage
如何登出? JWT 无法真正“失效”,解决方案:服务端维护黑名单 or 缩短有效期
权限粒度怎么控制? @PreAuthorize("hasRole('ADMIN')") 注解方法级权限
性能瓶颈在哪? 每次请求都查 DB?加 Redis 缓存 UserDetails!

特别提醒:别把 Spring Security 当黑盒用。我见过太多人只会 copy-paste 配置,结果线上被绕过权限。理解 Filter Chain 的执行顺序,比背八股文重要一百倍。


写在最后

搞定这套系统后,我终于能在周五下班前合上电脑。虽然过程痛苦,但收获巨大——不仅搞定了项目需求,还顺手整理了一份 GitHub Gist(别担心,没泄露公司代码 😅),准备下周技术分享用。

说真的,作为 DevOps,我原本以为自己只用管 CI/CD 和监控告警。但现实是,现代 DevOps 必须懂应用层安全。你部署的每一个服务,都可能是黑客的入口。Spring Security 虽然陡峭,但一旦掌握,就是你职业护城河的一部分。

对了,如果你也在学 Rust,欢迎一起交流!我刚用 Actix 写了个玩具版的 Auth 服务,性能吊打 JVM(开玩笑的,别当真)。毕竟,在程序员的世界里,语言之争永远不休,但——能跑就行

下次见!

评论 0

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