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. 安全不松懈:密钥加密存储、令牌过期控制、密码加密传输,守住安全底线。
相关推荐
Mark_ZP13 小时前
【锁2】锁的分类与概念
java·
caishenzhibiao13 小时前
顺势捕猎者副图 同花顺期货通指标
java·c语言·c#
Jay-r13 小时前
手势粒子特效系统 Gesture Particle FX(附源码下载)
python·ai·编程·pygame·百度云·手势控制
额恩6613 小时前
ResearchPilot 第三阶段总结报告
python·算法
有续技术14 小时前
哈斯 (Haas) 机床 IP、端口号配置内容总结
运维·服务器
小张同学a.14 小时前
LAMP架构2
linux·运维·网络·架构·负载均衡
红叶舞14 小时前
基于线段树的数据结构
数据结构·python·算法
千河殇 多想和她14 小时前
如何在Reaonix中使用CodeGraph以及CodeGraph效果实测经验分享
linux·运维·ubuntu
VortMall14 小时前
出海引流运维双升级|VortMall微服务商城系统v1.3.12 升级更新
运维·微服务·facebook
心念枕惊14 小时前
新写了个直播录制工具,可录制抖音快手斗鱼直播
运维·服务器·数据库