Spring Security 认证授权实战(JWT 版):从基础配置到权限精细化控制

在分布式系统中,认证授权是保障系统安全的核心防线 ------ 负责验证用户身份合法性、控制资源访问权限,防止未授权访问、数据泄露等安全问题。Spring Security 作为 Spring 生态的安全框架,提供了完整的认证授权机制,支持表单登录、OAuth2、JWT 等多种认证方式,灵活适配单体、分布式系统。

本文聚焦 Spring Security 结合 JWT(无状态认证)的实战落地,从基础环境搭建、自定义认证逻辑、JWT 集成,到接口权限精细化控制,全程嵌入完整代码教学,帮你彻底掌握 Spring Security 实战技巧,构建安全可靠的企业级系统。

一、核心认知:认证授权与 JWT 核心原理

1. 认证与授权的区别

  • 认证(Authentication):验证用户身份是否合法(如账号密码正确、令牌有效),核心是 "你是谁";
  • 授权(Authorization):验证合法用户是否有权访问某资源(如普通用户不能访问管理员接口),核心是 "你能做什么"。

2. JWT 无状态认证优势

JWT(JSON Web Token)是一种轻量级令牌,通过数字签名保证安全性,核心优势是「无状态」------ 服务器无需存储令牌信息,仅通过密钥验证令牌有效性,完美适配分布式系统(避免 Session 共享问题),认证流程如下:

  1. 用户登录成功,服务器生成 JWT 令牌返回给客户端;
  2. 客户端后续请求携带 JWT 令牌(放在请求头);
  3. 服务器拦截请求,验证令牌有效性,有效则放行并解析用户信息,无效则拒绝访问。

二、核心实战一:Spring Security 基础环境搭建(完整代码)

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>

2. 配置文件(application.yml)

yaml

复制代码
# JWT 配置
jwt:
  secret: your-secret-key-123456 # 密钥(生产环境需加密存储,长度≥256位)
  expire: 7200000 # 令牌有效期(2小时,单位:毫秒)
  refresh-expire: 86400000 # 刷新令牌有效期(24小时)

# Spring Security 配置(可选,核心配置在Java类中)
spring:
  security:
    user:
      name: test # 默认测试用户(自定义认证后失效)
      password: 123456

三、核心实战二:JWT 工具类封装(生成、解析、验证)

java

运行

复制代码
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expire}")
    private Long expire;
    @Value("${jwt.refresh-expire}")
    private Long refreshExpire;

    // 生成密钥(基于HMAC算法)
    private Key getSignKey() {
        return Keys.hmacShaKeyFor(secret.getBytes());
    }

    // ✅ 生成JWT令牌(含用户信息)
    public String generateToken(String username, String role) {
        // 存储用户信息(Payload部分)
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", username);
        claims.put("role", role); // 存储用户角色,用于后续授权

        return Jwts.builder()
                .setClaims(claims) // 自定义负载
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expire)) // 过期时间
                .signWith(getSignKey(), SignatureAlgorithm.HS256) // 签名算法
                .compact();
    }

    // ✅ 生成刷新令牌(有效期更长,用于刷新访问令牌)
    public String generateRefreshToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + refreshExpire))
                .signWith(getSignKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // ✅ 解析JWT令牌,获取用户信息
    public Claims parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(getSignKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("令牌已过期");
        } catch (MalformedJwtException e) {
            throw new RuntimeException("令牌格式错误");
        } catch (Exception e) {
            throw new RuntimeException("令牌验证失败");
        }
    }

    // ✅ 验证令牌有效性
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(getSignKey())
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 从令牌中获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("username", String.class);
    }

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

四、核心实战三:自定义认证授权逻辑(核心配置)

1. 自定义用户详情服务(对接数据库,替代默认用户)

java

运行

复制代码
import org.springframework.security.core.userdetails.User;
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 javax.annotation.Resource;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Resource
    private PasswordEncoder passwordEncoder;
    // 实际开发中注入UserMapper,对接数据库查询用户
    // @Resource
    // private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 从数据库查询用户(此处模拟数据,实际替换为userMapper查询)
        if (!"admin".equals(username)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 模拟数据库中的用户(密码已加密,实际从数据库获取)
        String encodedPassword = passwordEncoder.encode("123456");
        String role = "ROLE_ADMIN"; // 角色需以ROLE_开头,Spring Security约定

        // 2. 封装用户信息与权限(返回Spring Security内置User对象)
        return User.withUsername(username)
                .password(encodedPassword)
                .roles(role.substring(5)) // 去掉ROLE_前缀
                .build();
    }
}

2. JWT 认证过滤器(拦截请求,验证令牌)

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 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. 获取请求头中的JWT令牌
        String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7); // 去掉Bearer前缀
            username = jwtUtils.getUsernameFromToken(token);
        }

        // 2. 验证令牌,设置认证信息到上下文
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (jwtUtils.validateToken(token)) {
                // 加载用户详情
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                // 构建认证令牌
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                );
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 设置到安全上下文,后续授权会用到
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        // 3. 放行请求
        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.EnableGlobalMethodSecurity;
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 javax.annotation.Resource;

@Configuration
@EnableWebSecurity // 开启Web安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级权限控制
public class SecurityConfig {
    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

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

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

    // 核心安全配置
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF(跨站请求伪造),JWT无状态认证无需开启
                .csrf().disable()
                // 关闭Session(无状态认证)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 授权规则配置
                .authorizeRequests()
                .antMatchers("/login", "/refresh-token").permitAll() // 登录、刷新令牌接口放行
                .antMatchers("/admin/**").hasRole("ADMIN") // 管理员接口需ADMIN角色
                .anyRequest().authenticated(); // 其他接口需认证

        // 添JWT认证过滤器(在用户名密码认证过滤器之前执行)
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

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

1. 登录接口(生成 JWT 令牌)

java

运行

复制代码
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@RestController
public class LoginController {
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private JwtUtils jwtUtils;

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginDTO loginDTO) {
        try {
            // 1. 执行认证(调用CustomUserDetailsService查询用户,验证密码)
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword())
            );

            // 2. 认证成功,生成JWT令牌
            String username = authentication.getName();
            String role = authentication.getAuthorities().iterator().next().getAuthority();
            String token = jwtUtils.generateToken(username, role);
            String refreshToken = jwtUtils.generateRefreshToken(username);

            // 3. 返回令牌
            Map<String, String> result = new HashMap<>();
            result.put("token", token);
            result.put("refreshToken", refreshToken);
            result.put("msg", "登录成功");
            return result;
        } catch (AuthenticationException e) {
            throw new RuntimeException("用户名或密码错误");
        }
    }

    // 刷新令牌接口
    @PostMapping("/refresh-token")
    public Map<String, String> refreshToken(@RequestBody RefreshTokenDTO dto) {
        String refreshToken = dto.getRefreshToken();
        if (!jwtUtils.validateToken(refreshToken)) {
            throw new RuntimeException("刷新令牌无效");
        }
        String username = jwtUtils.parseToken(refreshToken).getSubject();
        // 实际开发中需查询用户角色,此处简化
        String newToken = jwtUtils.generateToken(username, "ROLE_ADMIN");
        Map<String, String> result = new HashMap<>();
        result.put("token", newToken);
        return result;
    }

    // 内部DTO类(接收请求参数)
    static class LoginDTO {
        private String username;
        private String password;
        // getter/setter
    }

    static class RefreshTokenDTO {
        private String refreshToken;
        // getter/setter
    }
}

2. 接口权限控制示例(方法级授权)

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("/admin")
public class AdminController {
    // 方法级权限控制:仅ADMIN角色可访问
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/user/list")
    public String getUserList() {
        return "管理员查询用户列表";
    }

    // 更精细的权限控制(结合权限标识)
    @PreAuthorize("hasAuthority('user:delete')")
    @GetMapping("/user/delete")
    public String deleteUser() {
        return "删除用户(需user:delete权限)";
    }
}

六、避坑指南

坑点 1:角色前缀问题导致授权失败

表现:配置了hasRole("ADMIN"),但用户拥有该角色仍无法访问;✅ 解决方案:Spring Security 要求角色必须以ROLE_开头,封装用户角色时需统一前缀,或使用hasAuthority()直接指定权限标识。

坑点 2:JWT 密钥长度不足导致加密失败

表现:启动时报IllegalArgumentException: The signing key's size is 18 bits which is less than the minimum required 256 bits;✅ 解决方案:密钥长度≥256 位(32 个字符以上),生产环境通过配置中心加密存储,避免硬编码。

坑点 3:过滤器顺序错误导致令牌验证失效

表现:请求携带令牌仍提示 "未认证";✅ 解决方案:确保 JWT 过滤器在UsernamePasswordAuthenticationFilter之前执行,通过addFilterBefore配置。

七、终极总结:认证授权的核心是「安全与灵活」

Spring Security 实战的核心,是通过合理配置实现「精准的身份验证 + 精细化的权限控制」,而 JWT 则为分布式系统提供了无状态、高可用的认证方案。企业级开发中,需兼顾安全性与灵活性 ------ 既要防止未授权访问、令牌伪造等安全问题,也要适配不同角色、不同权限的业务场景。

核心原则总结:

  1. 无状态优先:分布式系统优先使用 JWT 无状态认证,避免 Session 共享带来的复杂度;
  2. 权限精细化:结合「角色 + 权限标识」实现接口级、方法级授权,满足复杂业务需求;
  3. 安全不松懈:密钥加密存储、令牌过期控制、密码加密传输,守住安全底线。
相关推荐
二哈喇子!2 小时前
Java Web项目怎么创建 & 没有出现web.xml的解决方法
java·web·web.xml
2501_919219042 小时前
画册设计尺寸在不同设备(手机/平板)显示差异如何处理?
python·智能手机·电脑
tianyuanwo2 小时前
Jenkins Job管理实战指南:增删改查与批量操作技巧
运维·jenkins
螺旋小蜗2 小时前
docker-compose文件属性(3)顶部元素networks
运维·docker·容器
一起养小猫2 小时前
LeetCode100天Day13-移除元素与多数元素
java·算法·leetcode
Q16849645152 小时前
红帽Linux-文件权限管理
linux·运维·服务器
子午2 小时前
【2026原创】眼底眼疾识别系统~Python+深度学习+人工智能+CNN卷积神经网络算法+图像识别
人工智能·python·深度学习
不当菜虚困3 小时前
centos7虚拟机配置网络
运维·服务器·网络
fiveym3 小时前
CI/CD 核心原则 + 制品管理全解析:落地要求 + 存储方案
linux·运维·ci/cd
ACERT3333 小时前
10.吴恩达机器学习——无监督学习01聚类与异常检测算法
python·算法·机器学习