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();
    }
}
相关推荐
MengFly_3 分钟前
Compose 脚手架 Scaffold 完全指南
android·java·数据库
PPPPickup4 分钟前
application.yml或者yaml文件不显示绿色问题
java·数据库·spring
*小海豚*4 分钟前
springcloud项目运行启动类无法启动,IDEA也没有任何提示
java·ide
qq_2562470519 分钟前
Google 账号防封全攻略:从避坑、保号到申诉解封
后端
zhougl99631 分钟前
Java 枚举类(enum)详解
java·开发语言·python
想七想八不如1140832 分钟前
2019机试真题
java·华为od·华为
恋爱绝缘体135 分钟前
Java语言提供了八种基本类型。六种数字类型【函数基数噶】
java·python·算法
MX_93591 小时前
使用Spring的BeanFactoryPostProcessor扩展点完成自定义注解扫描
java·后端·spring
弹简特1 小时前
【JavaEE05-后端部分】使用idea社区版从零开始创建第一个 SpringBoot 程序
java·spring boot·后端
1104.北光c°1 小时前
【黑马点评项目笔记 | 登录篇】Redis实现共享Session登录
java·开发语言·数据库·redis·笔记·spring·java-ee