Spring Security JWT令牌生成与解析

好嘞,各位看官老爷,今天咱们就来聊聊Spring Security JWT令牌这档子事儿!🚀 想象一下,咱们的网站就像一座戒备森严的城堡,用户想要进出,总得有个凭证吧?传统的Session-Cookie机制就像是给每个人发一张房卡,服务器还得记住每张卡对应谁,时间一长,卡多了,服务器就懵圈了,记不住了!🤯

这时候,JWT令牌就闪亮登场了!它就像是一种“自包含”的通行证,里面包含了用户的信息,服务器拿到通行证,不用查数据库,一看就知道你是谁,能干啥。这玩意儿,轻便、高效,简直是分布式架构下的完美搭档! 😎

一、啥是JWT?(这玩意儿怎么来的?)

JWT,全称JSON Web Token,是一种基于JSON的开放标准(RFC 7519),用于在各方之间安全地传输信息。 简单来说,它就是个字符串,但这个字符串里塞满了信息,而且还经过了加密处理,防止被人篡改。

你可以把JWT想象成一个封装精美的快递包裹📦,里面装了用户信息,外面贴了标签(头部信息和签名),确保包裹在运输过程中不会被拆开偷东西。

二、JWT长啥样?(拆开包裹看看!)

一个JWT令牌通常由三部分组成,用点号(.)分隔:

  1. Header(头部): 描述令牌的元数据,通常包含令牌的类型(JWT)和使用的签名算法(例如HMAC SHA256或RSA)。

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    这部分会进行Base64Url编码。

  2. Payload(载荷): 包含令牌声明(Claims),这些声明是关于用户或其他实体以及额外数据的声明。 有三种类型的声明:

    • Registered claims(注册声明): 一些预定义的声明,建议使用,但不是强制性的。 例如:iss(签发者)、sub(主题)、aud(受众)、exp(过期时间)、nbf(生效时间)、iat(签发时间)、jti(JWT ID)。
    • Public claims(公共声明): 可以自定义的声明,但为了避免冲突,建议在IANA JSON Web Token Registry注册。
    • Private claims(私有声明): 自定义的声明,用于在应用程序之间共享信息。
    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true,
      "iat": 1516239022
    }

    这部分同样会进行Base64Url编码。

  3. Signature(签名): 用于验证令牌的完整性,确保令牌没有被篡改。 签名通过以下方式计算:

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret
    )

    其中secret是一个只有服务器知道的密钥。

    简单来说,就是用头部和载荷的信息,加上一个神秘的钥匙(secret),通过加密算法生成一个签名。

三、Spring Security + JWT:强强联合!

Spring Security是Java领域最流行的安全框架,它提供了强大的认证和授权功能。 结合JWT,我们可以构建出更加灵活、可扩展的安全系统。

1. 引入依赖

首先,你得在你的pom.xml文件中添加JWT相关的依赖,比如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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>

2. 定义UserDetailsService

UserDetailsService是Spring Security用来加载用户信息的接口。 你需要实现这个接口,从数据库或其他数据源中获取用户信息。

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository; // 假设你有一个UserRepository

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));

        // 将User对象转换成UserDetails对象
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                AuthorityUtils.createAuthorityList(user.getRole()) // 假设你的User类有个getRole()方法
        );
    }
}

3. JWT工具类

接下来,我们需要一个JWT工具类,用来生成和解析JWT令牌。

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret; // 从配置文件中读取密钥

    @Value("${jwt.expiration}")
    private long expiration; // 从配置文件中读取过期时间,单位是秒

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        // 可以添加自定义的claims,例如用户ID
        claims.put("userId", "123");
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 转换成毫秒
                .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) // 使用HS256算法
                .compact();
    }

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

4. JWT过滤器

我们需要一个JWT过滤器,拦截请求,从请求头中提取JWT令牌,并验证令牌的有效性。

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

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

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7); // 去掉"Bearer "前缀
            try {
                username = jwtUtil.getUsernameFromToken(jwt);
            } catch (ExpiredJwtException e) {
                // Token过期
                System.out.println("JWT令牌已过期");
            } catch (SignatureException e) {
                // 签名不匹配
                System.out.println("JWT签名无效");
            } catch (Exception e) {
                System.out.println("解析JWT令牌失败: " + e.getMessage());
            }
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {

                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

5. Spring Security配置

最后,我们需要配置Spring Security,将JWT过滤器添加到过滤器链中。

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) // 启用方法级别的安全控制
public class SecurityConfig {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 使用BCryptPasswordEncoder进行密码加密
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/authenticate").permitAll() // 允许访问/authenticate接口
                        .requestMatchers("/register").permitAll() // 允许访问/register接口
                        .anyRequest().authenticated() // 其他所有请求都需要认证
                )
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 使用STATELESS,不创建session
                )
                .authenticationProvider(authenticationProvider()) // 添加自定义AuthenticationProvider
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // 将JWT过滤器添加到UsernamePasswordAuthenticationFilter之前

        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
}

6. 创建一个AuthenticationController

这个Controller用于处理用户登录,并返回JWT Token。

@RestController
public class AuthenticationController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {

        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
            );
        } catch (DisabledException e) {
            throw new Exception("USER_DISABLED", e);
        } catch (BadCredentialsException e) {
            throw new Exception("INVALID_CREDENTIALS", e);
        }

        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());

        final String token = jwtUtil.generateToken(userDetails);

        return ResponseEntity.ok(new AuthenticationResponse(token));
    }
}

// AuthenticationRequest 和 AuthenticationResponse 类需要自己定义

四、配置文件的秘密

别忘了在application.propertiesapplication.yml文件中配置密钥和过期时间!

jwt.secret=YourSecretKey  # 替换成一个安全的密钥,例如使用 UUID 生成
jwt.expiration=3600        # 单位是秒,这里表示1小时

五、优点和缺点(不能光看贼吃肉,也得看贼挨打!)

优点:

  • 无状态: 服务器不需要存储会话信息,减轻了服务器的负担,易于扩展。
  • 可扩展性: 适用于分布式系统和微服务架构。
  • 安全性: 通过签名保证令牌的完整性,防止篡改。
  • 跨域支持: 可以跨域共享认证信息。

缺点:

  • 令牌撤销困难: 一旦JWT被签发,在过期之前都是有效的,无法主动撤销。 需要依赖短过期时间和黑名单机制来解决。
  • 令牌大小: 相比于Session ID,JWT令牌的体积较大,可能会增加网络传输的负担。
  • 密钥安全: 密钥的安全性至关重要,一旦泄露,所有令牌都可能被伪造。

六、实战演练(光说不练假把式!)

  1. 注册用户: 创建一个注册接口,将用户信息保存到数据库,并使用BCryptPasswordEncoder对密码进行加密。
  2. 登录认证: 创建一个登录接口,验证用户的用户名和密码,如果验证成功,则生成JWT令牌并返回给客户端。
  3. 访问受保护的资源: 在客户端的每个请求头中添加Authorization字段,值为Bearer <JWT令牌>。 服务器端的JWT过滤器会验证令牌的有效性,并授权用户访问受保护的资源。

七、注意事项(细节决定成败!)

  • 密钥安全: 务必保护好你的密钥,不要将其暴露在代码中或公开的配置文件中。 建议使用环境变量或专门的密钥管理工具。
  • 过期时间: 设置合理的过期时间,防止令牌被滥用。
  • 黑名单机制: 可以使用Redis或其他缓存系统维护一个黑名单,用于存储被吊销的JWT令牌。
  • 刷新令牌: 可以使用刷新令牌机制,允许用户在令牌过期后无需重新登录即可获取新的令牌。

八、总结(敲黑板,划重点!)

Spring Security + JWT 是一种强大的安全解决方案,可以帮助你构建出更加安全、可扩展的Web应用程序。 但是,你需要理解JWT的原理和优缺点,并根据你的实际需求进行合理的配置和优化。

希望这篇文章能帮助你更好地理解Spring Security JWT令牌,并在你的项目中成功应用! 如果你还有任何问题,欢迎在评论区留言,我会尽力解答! 😉

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注