Spring Security + JWT 实战:前后端分离项目的认证授权方案(从入门到落地)

一、先搞懂:Spring Security 到底能解决什么问题?

很多刚接触的同学会问:“我自己写个 Token 验证不行吗?为啥要用 Spring Security?”其实 Spring Security 不是 “重复造轮子”,而是帮我们封装了 认证(“你是谁”) 和 授权(“你能做什么”) 的全流程,比如:

  • 用户名密码校验、第三方登录(OAuth2.0);
  • 接口权限控制(谁能访问 /admin/*,谁只能看 /user/*);
  • 密码加密(BCrypt 不可逆加密,避免明文存储);
  • 安全防护(防 CSRF、XSS,限制登录失败次数)。

但 Spring Security 默认依赖 Session 存储状态,在 前后端分离 / 微服务 场景下会有跨域、Session 共享的问题 —— 这时候就需要结合 JWT 实现 “无状态认证”,两者搭配堪称 “黄金组合”!

二、核心概念:3 分钟搞懂关键组件

不用死记硬背,用 “机场安检” 类比一下:

组件作用(类比机场安检)核心职责
Authentication旅客的 “身份证 + 机票”存储用户身份信息(用户名、角色、权限)
AuthenticationManager安检负责人主导认证流程,调用 UserDetailsService 查用户
UserDetailsService航空公司的 “旅客信息库”自定义用户查询逻辑(从数据库 / Redis 查用户)
SecurityContext安检后的 “临时通行证” 存储处保存当前登录用户的 Authentication 对象
JWT可随身携带的 “电子通行证”无状态存储用户信息,避免服务端存 Session

三、实战:Spring Security + JWT 落地步骤(附完整代码)

1. 环境准备:引入依赖

首先创建 Spring Boot 项目,在 pom.xml 中加入核心依赖(JWT 用 JJWT 工具包):

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT 工具包(JJWT) -->
<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>

<!-- 数据库相关(MyBatis + MySQL) -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

2. 第一步:自定义用户认证(UserDetailsService)

Spring Security 默认用 “user / 随机密码” 登录,我们需要改成从 数据库查用户,所以实现 UserDetailsService 接口:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper; // 自己写的Mapper,查用户和角色

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 从数据库查用户(这里根据自己的表结构调整)
        User user = userMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在!");
        }

        // 2. 查用户的角色/权限(比如 ADMIN、USER)
        List<String> roles = userMapper.selectRolesByUserId(user.getId());
        // 转换为 Spring Security 能识别的权限对象
        Collection<? extends GrantedAuthority> authorities = 
            roles.stream()
                 .map(SimpleGrantedAuthority::new)
                 .collect(Collectors.toList());

        // 3. 返回 UserDetails 对象(包含用户名、加密后的密码、权限)
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(), // 数据库存的是 BCrypt 加密后的密码
            authorities
        );
    }
}

3. 第二步:JWT 工具类(生成 / 验证 Token)

写一个工具类封装 JWT 的生成、解析、验证逻辑,注意 密钥要保密(建议放配置文件):

@Component
public class JwtUtils {

    // 从配置文件读取密钥和过期时间(这里简化写死,实际用 @Value 注入)
    private static final String SECRET_KEY = "your-secret-key-123456"; // 至少32位
    private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000; // 1天过期

    // 1. 生成 JWT Token
    public String generateToken(Authentication authentication) {
        // 获取当前登录用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME);

        // 构建 Token(包含用户名、过期时间、签名)
        return Jwts.builder()
                .setSubject(userDetails.getUsername()) // 主题:用户名
                .setIssuedAt(now) // 签发时间
                .setExpiration(expirationDate) // 过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法
                .compact();
    }

    // 2. 从 Token 中获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    // 3. 验证 Token 是否有效(未过期、签名正确)
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            // Token 过期、签名错误等都会抛异常
            return false;
        }
    }
}

4. 第三步:JWT 过滤器(拦截请求并认证)

前后端分离项目中,前端每次请求会在 Authorization 头带 Token,我们需要写一个过滤器 拦截请求→解析 Token→完成认证

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {

        // 1. 从请求头获取 Token(格式:Bearer xxxxxx)
        String authorizationHeader = request.getHeader("Authorization");
        String token = null;
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7); // 去掉 "Bearer " 前缀
        }

        // 2. 验证 Token 并完成认证
        if (token != null && jwtUtils.validateToken(token)) {
            // 从 Token 中获取用户名
            String username = jwtUtils.getUsernameFromToken(token);
            // 查用户信息(含权限)
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 构造 Authentication 对象,存入 SecurityContext
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities()
            );
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

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

5. 第四步:配置 Spring Security(SecurityConfig)

这是核心配置类,控制 “哪些接口需要认证”“用什么方式认证”:

@Configuration
@EnableWebSecurity // 启用 Spring Security
@EnableMethodSecurity // 启用方法级别的权限控制(@PreAuthorize)
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private UserDetailsService userDetailsService;

    // 密码编码器(BCrypt 加密)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 配置 AuthenticationManager(认证管理器)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    // 核心配置:接口权限、过滤器顺序
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 1. 关闭 CSRF(前后端分离项目不需要,否则会拦截 POST 请求)
            .csrf(csrf -> csrf.disable())
            
            // 2. 配置跨域(解决前端跨域请求问题)
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // 3. 配置接口权限
            .authorizeHttpRequests(auth -> auth
                // 放行登录接口(不需要认证就能访问)
                .requestMatchers("/api/login").permitAll()
                // 放行静态资源(如 swagger、前端页面)
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                // 管理员接口:只有 ADMIN 角色能访问
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                // 其他接口:需要认证(登录后才能访问)
                .anyRequest().authenticated()
            )
            
            // 4. 配置登录逻辑(自定义登录接口返回 Token)
            .formLogin(form -> form
                .loginProcessingUrl("/api/login") // 登录请求路径
                .successHandler((request, response, authentication) -> {
                    // 登录成功:生成 Token 并返回给前端
                    String token = jwtUtils.generateToken(authentication);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(JSON.toJSONString(
                        Result.success("登录成功", token) // 自定义返回格式
                    ));
                })
                .failureHandler((request, response, exception) -> {
                    // 登录失败:返回错误信息
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(JSON.toJSONString(
                        Result.error("登录失败:" + exception.getMessage())
                    ));
                })
            )
            
            // 5. 添加 JWT 过滤器(在 UsernamePasswordAuthenticationFilter 之前执行)
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    // 跨域配置(允许前端域名访问)
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://localhost:8080"); // 前端域名
        config.addAllowedMethod("*"); // 允许所有请求方法
        config.addAllowedHeader("*"); // 允许所有请求头
        config.setAllowCredentials(true); // 允许携带 Cookie

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

6. 第五步:方法级权限控制(@PreAuthorize)

除了路径匹配授权,还能在 方法上直接加注解 控制权限,更灵活:

@RestController
@RequestMapping("/api/activity")
public class ActivityController {

    // 只有 ADMIN 角色能创建活动
    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/create")
    public Result createActivity(@RequestBody ActivityDTO activityDTO) {
        // 业务逻辑...
        return Result.success("活动创建成功");
    }

    // 登录用户就能查看活动(USER/ADMIN 都可以)
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/{id}")
    public Result getActivityDetail(@PathVariable Long id) {
        // 业务逻辑...
        return Result.success(activityDetail);
    }
}

四、避坑指南:这些问题我踩过!

  1. 跨域问题:一定要配置 corsConfigurationSource,否则前端请求会被拦截;
  2. Token 过期:可以加 “刷新 Token” 机制(生成一个过期时间更长的 Refresh Token);
  3. 密码加密:数据库存的必须是 BCrypt 加密后的密码,不能存明文(用 passwordEncoder.encode("123456") 加密);
  4. CSRF 关闭:前后端分离项目必须关 CSRF,否则 POST/PUT 请求会被拒绝;
  5. Token 黑名单:如果需要 “用户登出后立即失效 Token”,可以用 Redis 存黑名单(Token 未过期但已登出)。

五、总结

Spring Security + JWT 的组合,完美解决了前后端分离 / 微服务的认证授权问题:

  • Spring Security 负责 “身份校验” 和 “权限控制”,不用自己写重复逻辑;
  • JWT 负责 “无状态传递”,不用存 Session,服务端更轻量,分布式部署更简单。

如果你正在做 Java 项目,这套方案可以直接落地!如果有疑问,欢迎在评论区交流~ 后续还会分享 Spring Security 结合 OAuth2.0 实现第三方登录(微信、GitHub),关注我不迷路~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Pretend________

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值