SpringBoot 安全认证实战(Spring Security + JWT):打造无状态安全接口体系

在前后端分离、微服务架构中,接口安全是核心需求 ------ 需解决用户身份认证、权限控制、接口防篡改、无状态通信等问题。Spring Security 作为一站式安全框架,提供认证、授权、防跨站请求伪造(CSRF)等完整功能,结合 JWT(JSON Web Token)可实现无状态认证,避免 Session 共享带来的分布式问题,是企业级接口安全的首选方案。

本文聚焦 SpringBoot 集成 Spring Security + JWT 的实战落地,从环境搭建、JWT 工具封装、认证授权配置,到用户登录、角色权限控制、接口保护,全程嵌入 Java 代码教学,帮你快速打造安全、可靠、无状态的接口安全体系。

一、核心认知:Spring Security + JWT 核心价值与适用场景

1. 核心优势

  • 无状态认证:JWT 携带用户信息与权限,服务端无需存储会话,适配分布式微服务;
  • 细粒度授权:支持基于角色(RBAC)、资源的权限控制,精准保护接口;
  • 安全可靠:JWT 支持签名加密,防止数据篡改与伪造,Spring Security 提供成熟的安全防护机制;
  • 兼容性强:无缝适配前后端分离架构,支持移动端、PC 端等多终端认证;
  • 可扩展:支持自定义认证逻辑、权限校验规则,适配复杂业务场景。

2. 核心适用场景

  • 前后端分离项目:无状态接口认证,替代传统 Session 认证;
  • 微服务架构:跨服务认证授权,统一安全策略;
  • 多终端应用:PC 端、移动端、小程序共用一套认证体系;
  • 企业级系统:基于角色的权限管理(如管理员、普通用户、访客)。

3. JWT 核心原理

JWT 由三部分组成,通过 Base64 编码与签名连接,整体为一串字符串:

  • 头部(Header):指定签名算法(如 HS256);
  • 载荷(Payload):存储用户 ID、角色、过期时间等非敏感信息;
  • 签名(Signature):基于头部算法、载荷、密钥生成签名,确保数据不被篡改。

二、核心实战一:环境搭建与 JWT 工具封装

1. 引入依赖(Maven)

xml

复制代码
<!-- Spring Security 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 依赖 -->
<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>
<!-- Web 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!-- MyBatis-Plus(用户数据访问,可选) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

2. JWT 工具类封装(生成 Token、解析 Token、验证 Token)

java

运行

复制代码
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class JwtUtils {
    // JWT 密钥(生产环境需放在配置中心,加密存储)
    @Value("${jwt.secret:defaultSecretKey123456}")
    private String secret;

    // Token 过期时间(2小时,单位:毫秒)
    @Value("${jwt.expire:7200000}")
    private Long expire;

    // 生成 JWT Token(携带用户ID、角色信息)
    public String generateToken(Long userId, String username, String role) {
        // 1. 构建载荷信息
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("username", username);
        claims.put("role", role);

        // 2. 生成 Token
        return Jwts.builder()
                .setClaims(claims) // 载荷信息
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expire)) // 过期时间
                .signWith(SignatureAlgorithm.HS256, secret) // 签名算法与密钥
                .compact();
    }

    // 解析 Token,获取载荷信息
    public Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("Token 已过期", e);
            throw new RuntimeException("Token 已过期");
        } catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
            log.error("Token 无效", e);
            throw new RuntimeException("Token 无效");
        }
    }

    // 验证 Token 有效性(是否过期、签名是否正确)
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 从 Token 中获取用户ID
    public Long getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("userId", Long.class);
    }

    // 从 Token 中获取用户角色
    public String getRoleFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("role", String.class);
    }
}

3. 配置文件(application.yml)

yaml

复制代码
# JWT 配置
jwt:
  secret: E7A5F2D8C9B3A1E4F6D2C7B8A9E3F4D5C6B7A8E9F0D1C2B3A4E5F6D7C8B9A0E1 # 生产环境使用复杂密钥
  expire: 7200000 # 2小时过期

# Spring Security 配置(可选,后续在配置类中细化)
spring:
  security:
    user:
      name: admin
      password: 123456 # 临时默认用户,后续替换为数据库用户

# 数据库配置(用户表存储,可选)
  datasource:
    url: jdbc:mysql://localhost:3306/security_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

三、核心实战二:Spring Security 核心配置(认证与授权)

1. 自定义用户详情服务(从数据库获取用户信息)

Spring Security 需通过 UserDetailsService 获取用户信息,用于认证校验,这里结合数据库实现(用户表需包含 id、username、password、role 字段)。

java

运行

复制代码
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.security.entity.User;
import com.example.security.mapper.UserMapper;
import javax.annotation.Resource;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 从数据库查询用户
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 2. 封装用户信息与权限(Spring Security 要求格式)
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword()) // 数据库密码需加密存储(如 BCrypt)
                .roles(user.getRole()) // 角色(如 "ADMIN"、"USER")
                .build();
    }
}

2. 自定义 JWT 认证过滤器(解析 Token 并认证)

过滤器拦截请求,从请求头中获取 Token,解析后完成用户认证,让后续接口能获取当前登录用户信息。

java

运行

复制代码
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.example.security.service.CustomUserDetailsService;
import com.example.security.utils.JwtUtils;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Resource
    private JwtUtils jwtUtils;
    @Resource
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 从请求头获取 Token(格式:Bearer {token})
        String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
        }

        // 2. 解析 Token,获取用户名并认证
        if (token != null && jwtUtils.validateToken(token)) {
            username = jwtUtils.parseToken(token).get("username", String.class);
            // 3. 将用户信息存入 Security 上下文
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                );
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        // 4. 继续执行过滤器链
        filterChain.doFilter(request, response);
    }
}

3. Spring Security 主配置类

配置认证授权规则、密码加密方式、过滤器顺序等核心逻辑。

java

运行

复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.security.filter.JwtAuthenticationFilter;
import javax.annotation.Resource;

@Configuration
@EnableWebSecurity // 开启 Web 安全支持
@EnableMethodSecurity // 开启方法级权限控制(支持 @PreAuthorize 注解)
public class SecurityConfig {
    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    // 密码加密器(BCrypt 加密,不可逆)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 认证管理器(用于用户登录认证)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 核心安全配置(接口授权、会话管理、过滤器顺序)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭 CSRF(前后端分离场景无需 CSRF 防护)
                .csrf(csrf -> csrf.disable())
                // 无状态会话(JWT 无状态认证,不创建 Session)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 接口授权规则
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/**").permitAll() // 登录接口放行
                        .requestMatchers("/api/public/**").permitAll() // 公开接口放行
                        .requestMatchers("/api/admin/**").hasRole("ADMIN") // 管理员接口仅 ADMIN 角色可访问
                        .anyRequest().authenticated() // 其他接口需认证
                )
                // 添加 JWT 过滤器(在用户名密码过滤器之前执行)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

四、核心实战三:用户登录与接口权限控制

1. 登录接口实现(生成 Token 返回给前端)

java

运行

复制代码
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.security.entity.LoginDTO;
import com.example.security.utils.JwtUtils;
import javax.annotation.Resource;

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private JwtUtils jwtUtils;

    // 用户登录接口
    @PostMapping("/login")
    public String login(@RequestBody LoginDTO loginDTO) {
        // 1. 执行认证(用户名密码校验)
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword())
        );

        // 2. 认证成功,生成 Token
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String username = userDetails.getUsername();
        String role = userDetails.getAuthorities().iterator().next().getAuthority().replace("ROLE_", "");
        // 从数据库查询用户ID(实际开发中可优化,避免重复查询)
        Long userId = 1L; // 示例,实际需从用户信息中获取
        String token = jwtUtils.generateToken(userId, username, role);

        // 3. 返回 Token 给前端
        return "Bearer " + token;
    }
}

2. 方法级权限控制(@PreAuthorize 注解)

除了配置类中的接口路径授权,还可通过注解实现更细粒度的权限控制。

java

运行

复制代码
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/user")
public class UserController {
    // 仅 USER 角色可访问
    @PreAuthorize("hasRole('USER')")
    @GetMapping("/info")
    public String getUserInfo() {
        return "普通用户信息";
    }

    // 仅 ADMIN 角色可访问
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/list")
    public String getUserList() {
        return "用户列表(仅管理员可见)";
    }

    // 多个角色可访问
    @PreAuthorize("hasAnyRole('ADMIN', 'USER')")
    @GetMapping("/profile")
    public String getProfile() {
        return "个人资料";
    }
}

3. 获取当前登录用户信息

java

运行

复制代码
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/public")
public class PublicController {
    // 获取当前登录用户信息
    @GetMapping("/current-user")
    public String getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        String role = authentication.getAuthorities().iterator().next().getAuthority();
        return "当前登录用户:" + username + ",角色:" + role;
    }
}

五、避坑指南

坑点 1:登录接口认证失败,提示 "用户不存在"

表现:输入正确用户名密码,仍提示用户不存在;✅ 解决方案:检查 CustomUserDetailsService 中数据库查询逻辑是否正确,确保用户名匹配规则一致,数据库密码已通过 BCryptPasswordEncoder 加密存储(避免明文密码无法匹配)。

坑点 2:JWT Token 解析失败,提示 "签名无效"

表现:前端携带 Token 请求接口,后端提示 Token 无效;✅ 解决方案:确保生成 Token 与解析 Token 使用的密钥(secret)一致,Token 格式正确(需包含 "Bearer" 前缀),避免 Token 被篡改或截断。

坑点 3:方法级权限注解(@PreAuthorize)失效

表现:添加注解后,无对应角色仍可访问接口;✅ 解决方案:确保配置类添加 @EnableMethodSecurity 注解,注解中的角色名称需与 UserDetails 中封装的角色一致(区分大小写)。

坑点 4:Security 上下文获取不到用户信息

表现:接口中通过 SecurityContextHolder 无法获取当前登录用户;✅ 解决方案:检查 JWT 过滤器是否正确执行,Token 是否有效且已解析,过滤器是否在 UsernamePasswordAuthenticationFilter 之前添加。

六、终极总结:接口安全的核心是「认证可靠 + 授权精准」

Spring Security + JWT 实战的核心,是通过 JWT 实现无状态认证,摆脱 Session 依赖,适配分布式场景;通过 Spring Security 实现细粒度授权,确保接口仅被合法用户访问。企业级开发中,需平衡「安全性」与「易用性」,同时规避常见安全风险。

核心原则总结:

  1. 密钥安全优先:JWT 密钥需复杂且加密存储,生产环境避免硬编码,通过配置中心管理;
  2. 权限粒度适配业务:根据业务需求选择路径级或方法级授权,避免过度授权或授权不足;
  3. Token 生命周期可控:合理设置 Token 过期时间,可实现 Token 刷新机制,避免频繁登录;
  4. 安全防护全面:结合 HTTPS 防止 Token 传输被窃取,关闭不必要的安全防护(如 CSRF)适配前后端分离场景。
相关推荐
羑悻的小杀马特2 小时前
不做“孤岛”做“中枢”:拆解金仓时序库,看国产基础软件如何玩转“多模融合”
数据库·人工智能
Z_W_H_2 小时前
MyBatis-Plus 详细学习文档
学习·mybatis
bbq粉刷匠2 小时前
MySQL - 基础增删查改
数据库·mysql
rannn_1112 小时前
【Javaweb学习|Day7】事务管理、文件上传
后端·学习·javaweb
Zfox_2 小时前
【Docker#1】技术架构演进之路
后端·docker·容器·架构
l1t2 小时前
DeepSeek总结的SQLite 数据库的版本更新历史摘要
数据库·人工智能·sqlite
一个天蝎座 白勺 程序猿2 小时前
Apache IoTDB(13):数据处理的双刃剑——FILL空值填充与LIMIT/SLIMIT分页查询实战指南
数据库·sql·ai·apache·时序数据库·iotdb
一步一个脚印2 小时前
Oracle LONG类型与CLOB类型的比较与转换
数据库·oracle
小园子的小菜2 小时前
Spring事务失效9大场景(Java面试高频)
java·spring·面试