Spring Security + JWT

一、项目依赖配置

首先,在 pom.xml中添加必要的依赖:

复制代码
<dependencies>
    <!-- Spring Boot Starter Web (包含Spring MVC) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Boot Starter Security (安全核心) -->
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-security</artifactId>  
        <version>2.7.5</version>  <!-- 建议使用稳定版本 -->
    </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>

</dependencies>

二、安全配置类

创建 SecurityConfig配置类,这是 Spring Security 的核心配置:

复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 启用方法级安全控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
    }
    
    // 1. 密码编码器(必须配置)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  // 禁用CSRF保护(API项目通常不需要)
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 无状态会话
            .and()
            .authorizeRequests()
            // 公开访问的接口
            .antMatchers("/api/auth/login", "/api/auth/register").permitAll()
            .antMatchers("/api/public/**", "/test/**").permitAll()
            .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()  // Swagger文档
            .anyRequest().authenticated();  // 其他所有请求都需要认证
        
        // 添加JWT过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, 
                           UsernamePasswordAuthenticationFilter.class);
    }
}

三、JWT工具类

创建 JwtUtil用于生成和验证JWT令牌:

复制代码
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtil {
    
    // 从配置文件中读取JWT密钥和有效期
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private Long expiration;
    
    // 生成安全的密钥
    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(secret.getBytes());
    }
    
    // 生成JWT令牌
    public String generateToken(String username, Long userId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", username);
        claims.put("userId", userId);
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }
    
    // 从令牌中获取用户名
    public String getUsernameFromToken(String token) {
        return getAllClaimsFromToken(token).getSubject();
    }
    
    // 从令牌中获取用户ID
    public Long getUserIdFromToken(String token) {
        return getAllClaimsFromToken(token).get("userId", Long.class);
    }
    
    // 获取所有声明
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    // 验证令牌
    public boolean validateToken(String token, String username) {
        final String tokenUsername = getUsernameFromToken(token);
        return (tokenUsername.equals(username) && !isTokenExpired(token));
    }
    
    // 检查令牌是否过期
    private boolean isTokenExpired(String token) {
        final Date expiration = getAllClaimsFromToken(token).getExpiration();
        return expiration.before(new Date());
    }
}

四、JWT认证过滤器

创建 JwtAuthenticationTokenFilter处理HTTP请求中的JWT令牌:

复制代码
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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 JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;
    
    public JwtAuthenticationTokenFilter(JwtUtil jwtUtil, 
                                      UserDetailsService userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain)
            throws ServletException, IOException {
        
        // 1. 从请求头中获取Token
        final String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;
        
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            try {
                username = jwtUtil.getUsernameFromToken(token);
            } catch (Exception e) {
                logger.error("JWT令牌解析失败", e);
            }
        }
        
        // 2. 验证Token并设置认证信息
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 从数据库加载用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 验证Token有效性
            if (jwtUtil.validateToken(token, username)) {
                // 创建认证令牌
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities());
                
                authentication.setDetails(new WebAuthenticationDetailsSource()
                        .buildDetails(request));
                
                // 将认证信息设置到Security上下文
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        
        // 3. 继续过滤器链
        filterChain.doFilter(request, response);
    }
}

五、自定义UserDetailsService

实现 UserDetailsService接口,用于从数据库加载用户信息:

复制代码
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    private final UserMapper userMapper;  // MyBatis Mapper
    
    public CustomUserDetailsService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
    
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查询用户
        User user = userMapper.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException(
                        "用户不存在: " + username));
        
        // 转换为Spring Security的UserDetails
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())  // 数据库存储的是加密后的密码
                .authorities("ROLE_USER")  // 设置角色/权限
                .accountExpired(false)
                .accountLocked(false)
                .credentialsExpired(false)
                .disabled(false)
                .build();
    }
}
相关推荐
我命由我123452 小时前
Android 项目路径包含非 ASCII 字符问题:Your project path contains non-ASCII characters
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
沛沛老爹2 小时前
Web开发者突围AI战场:Agent Skills元工具性能优化实战指南——像优化Spring Boot一样提升AI吞吐量
java·开发语言·人工智能·spring boot·性能优化·架构·企业开发
小杨同学492 小时前
C 语言实战:超市水果结算系统(深度解析与优化)
后端·算法·设计
Full Stack Developme2 小时前
Redis 实现主从同步
java·redis·spring
yangminlei2 小时前
Spring Boot 响应式 WebFlux 从入门到精通
java·spring boot·后端
曹轲恒2 小时前
SpringBoot配置文件
java·spring boot
Apifox2 小时前
Apifox CLI + Claude Skills:将接口自动化测试融入研发工作流
前端·后端·测试
亓才孓2 小时前
[认识异常和错误]java
java·开发语言