首页 > 其他分享 >Spring Security集成JWT

Spring Security集成JWT

时间:2024-12-05 22:29:49浏览次数:11  
标签:令牌 String 过期 Spring JWT token 密钥 Security

1.JWT基础

1.1. JWT是什么:

JWT(JSON Web Token)是一种用于在不同系统之间安全地传递信息的无状态令牌。它通常用于 身份验证授权,尤其在现代Web应用和API中非常常见。JWT是通过对传输的数据进行编码和签名来确保其完整性和安全性。

JWT的特点

  • 无状态:JWT不需要在服务器端保存状态信息,因此不需要存储会话信息,减轻了服务器的负担。
  • 可验证性:JWT包含签名部分,可以确保信息在传输过程中没有被篡改。

1.2. JWT的组成:

JWT由三个部分组成,每一部分都是用Base64Url编码的,并通过"."(点)连接起来,格式如下:

header.payload.signature

header部分只包含了你所使用的签名算法,payload中才包含了关键信息(用户ID,该用户所分配的权限等等),因为每个请求中都带着JWT,所以只需检查你payload中的用户ID与分配的权限就能验证你是否有权限访问某些内容(授权)。

但是又因为我如果自己将我这个账户得到的JWT中的payload字段给修改了权限,那就会导致漏洞。所以又增加了一招,在每次用户进行身份验证完发放JWT时,在最后添加个signature字段,这个字段是通过对前两个字段进行header中的加密算法(密钥也参与加密)获得的,所以如果你一旦改变了payload中的字段内容,最后在系统验证你的JWT字段时会发现通过前两个字段进行响应加密算法得到的signature和这个JWT中的signature不匹配,那就证明你的JWT被修改了。

(1) Header(头部)

Header包含两部分内容:

  • 类型(type):通常为 "JWT"。
  • 签名算法(algorithm):指定JWT签名使用的算法。常见的算法有:
    • HMAC SHA256(对称加密,使用共享密钥)
    • RSA(非对称加密,使用公私钥对)
    • HS256RS256ES256等。

示例:

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

这里,"alg" 表示签名算法(HMAC SHA256),"typ" 表示JWT的类型。

(2) Payload(有效载荷)

Payload部分包含了声明(Claims),即传递的信息。声明可以分为三类:

  • 注册声明(Registered Claims):这些是预定义的声明,通常用来传递一些标准化的信息。常见的声明有:

    • sub(subject):主题,通常是用户的唯一标识符。
    • iss(issuer):签发者,标识JWT的发布者。
    • exp(expiration time):过期时间,指定JWT的有效期。
    • iat(issued at):签发时间,指定JWT的生成时间。
    • aud(audience):受众,指定JWT的接收者。
  • 公共声明(Public Claims):这些是应用开发者自定义的声明,通常是一些业务信息,如用户的角色、权限等。

  • 私有声明(Private Claims):这些是双方约定的声明,通常用于传递特定的数据,如用户的ID、权限等。

示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

这里,sub是用户ID,name是用户名,iat是JWT签发时间。

注意:Payload部分的数据是未加密的,它可以被解码和读取,因此不应存储敏感数据。

(3) Signature(签名)

Signature部分用于验证JWT的完整性,防止数据被篡改。签名是根据以下三部分生成的:

  1. HeaderPayload部分经过Base64Url编码后,拼接成字符串。
  2. 使用签名算法(如HS256)和密钥对该字符串进行签名,得到签名值。
  • 对称加密(如HMAC SHA256)时,签名算法会用一个共享密钥来签名。
  • 非对称加密(如RSA)时,签名算法使用私钥进行签名,验证时使用公钥

签名的作用是:

  • 验证消息的来源(确保它是由合法的授权方生成的)。
  • 验证消息是否在传输过程中被篡改。

示例(假设使用HMAC SHA256签名),以下是生成Signature(签名)的过程:

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

1.3. JWT如何工作:

JWT的工作流程分为三个主要阶段:生成、传递、验证

(1) 生成JWT
  • 用户登录:用户输入用户名和密码登录系统。
  • 服务器验证:服务器验证用户的身份,通常通过查询数据库等方式来确认用户是否存在以及密码是否正确。
  • 生成JWT:一旦用户身份验证成功,服务器会生成JWT。JWT包含了用户的身份信息和权限声明(如用户ID、角色等),并将其签名。
  • 返回JWT:JWT通过响应返回给客户端(如浏览器、移动端应用等)。
(2) 传递JWT
  • 客户端存储JWT:客户端通常会将JWT存储在本地存储(localStorage)或会话存储(sessionStorage)中,或者通过HTTP Cookie进行传递。

  • 携带JWT进行请求:客户端在后续的请求中,将JWT作为请求头的一部分(通常是Authorization头)发送给服务器,进行授权验证。

    Authorization: Bearer <your_jwt_token>
    
(3) 验证JWT
  • 服务器接收JWT:服务器收到客户端的请求后,提取JWT并进行验证。
  • 验证签名:服务器使用密钥(对称加密)或公钥(非对称加密)对JWT的签名进行验证,确保数据没有被篡改。
  • 验证过期时间:JWT中可以包含过期时间(exp),服务器会验证JWT是否已过期,若过期则拒绝访问。
  • 返回响应:如果JWT有效,服务器就允许客户端访问相应资源;否则,返回认证失败的响应。

1.4. JWT的优缺点

优点:

  • 无状态:JWT不需要在服务器端保存会话信息,减轻服务器负担,适合分布式和微服务架构。

  • 跨平台支持:JWT采用JSON格式,广泛支持各种编程语言和平台。

  • 灵活性:JWT支持不同的签名算法,可以根据需求选择对称加密或非对称加密。

缺点:

  • 不适合存储敏感信息:虽然JWT具有加密签名,但Payload部分是明文的,不适合存储敏感数据(如密码)。

  • 令牌泄露风险:如果JWT被泄露,攻击者可以使用它进行伪造请求,直到令牌过期或被撤销。

2. Spring Boot与Spring Security集成JWT

在Spring Boot项目中集成JWT,可以通过Spring Security来实现用户认证和授权。使用JWT作为认证令牌,可以避免传统的基于Session的身份管理方式,减轻服务器的负担,提供无状态的认证方式。下面详细讲解如何在Spring Boot与Spring Security中集成JWT。

2.1 添加依赖

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

    <!-- Spring Boot Starter Validation (optional, if validating requests) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

 <!-- JJWT: Java library for creating and verifying JSON Web Tokens -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.2</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.2</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.2</version>
    </dependency>

</dependencies>
  • spring-boot-starter-validation依赖对一些请求参数进行验证,会自动导入相关的验证功能,通常与@Valid@Validated注解结合使用,帮助我们验证请求体中的数据是否符合约定。

jjwt 是一个用于创建和验证JWT(JSON Web Tokens)的Java库。它帮助我们简化JWT的处理,如生成、解析和验证令牌。jjwt提供了一些简单易用的API来处理JWT的创建和验证,而不需要手动处理底层的编码、解码和签名过程。

  • jjwt-api 提供了操作 JWT 的公共 API。
  • jjwt-impl 包含了 API 的具体实现。
  • jjwt-jackson 提供了使用 Jackson 进行 JSON 解析和序列化的支持。

最好不要通过直接引进整个jjwt artifact的依赖,这样会在下载Maven依赖的时候出问题,最好将其拆分成以上三个模块分别引入。

2.2 JWT生成:创建JwtUtil工具类 (可被设为Bean)

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
import java.util.Date;

@Component
public class JwtUtil {

    // 密钥,用于签名和验证JWT
    private String secretKey = "mysecretkey"; // 应放在环境变量或配置文件中

    // JWT有效期:设置JWT的过期时间
    private long validityInMilliseconds = 3600000; // 1 hour

    /**
     * 生成JWT令牌
     * @param userId 用户ID
     * @param username 用户名
     * @return 生成的JWT令牌
     */
    public String generateToken(Long userId, String username) {
        return Jwts.builder()
                .setSubject(userId.toString())  // 设置JWT的subject为userId
                .claim("username", username)    // 设置额外的claim(如用户名)
                .setIssuedAt(new Date())       // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS256, secretKey) // 使用HS256算法进行签名
                .compact();  // 返回生成的JWT令牌
    }

    /**
     * 从JWT中提取用户名
     * @param token JWT令牌
     * @return 用户名
     */
    public String extractUsername(String token) {
        return extractClaim(token, claims -> claims.get("username", String.class)); // 提取username
    }

    /**
     * 从JWT中提取用户ID
     * @param token JWT令牌
     * @return 用户ID
     */
    public Long extractUserId(String token) {
        return Long.parseLong(extractClaim(token, Claims::getSubject)); // 提取userId(subject部分)
    }

    /**
     * 从JWT中提取特定的claim(声明)
     * @param token JWT令牌
     * @param claimsExtractor 提取函数
     * @param <T> 提取的结果类型
     * @return 提取的claim值
     */
    public <T> T extractClaim(String token, ClaimsExtractor<T> claimsExtractor) {
        final Claims claims = extractAllClaims(token); // 获取JWT中的所有claims
        return claimsExtractor.extract(claims); // 提取指定的claim
    }

    /**
     * 解析JWT中的所有claims
     * @param token JWT令牌
     * @return JWT中的所有claims
     */
    private Claims extractAllClaims(String token) {
        return Jwts.parser()
                .setSigningKey(secretKey) // 使用密钥验证JWT
                .parseClaimsJws(token) // 解析JWT
                .getBody(); // 获取claims部分
    }

    /**
     * 验证JWT是否过期
     * @param token JWT令牌
     * @return 是否过期
     */
    public boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date()); // 判断过期时间
    }

    /**
     * 提取JWT的过期时间
     * @param token JWT令牌
     * @return 过期时间
     */
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration); // 获取过期时间
    }

    /**
     * 验证JWT的有效性
     * @param token JWT令牌
     * @param userId 用户ID
     * @return JWT是否有效
     */
    public boolean validateToken(String token, Long userId) {
        return (userId.equals(extractUserId(token)) && !isTokenExpired(token)); // 验证userId是否匹配并检查是否过期
    }

    /**
     * ClaimsExtractor接口,用于提取JWT中的具体claim
     * @param <T> 提取的结果类型
     */
    @FunctionalInterface
    public interface ClaimsExtractor<T> {
        T extract(Claims claims); // 从claims中提取信息
    }
}

(1)secretKey - 用于签名和验证JWT的密钥

private String secretKey = "mysecretkey"; // 用于签名的密钥,生产环境中应保密
  • secretKey 是我们用于签署JWT的密钥。此密钥必须保持机密性,不能暴露给外部用户。通常,密钥应该存储在环境变量中,或通过配置文件安全地管理。

  • 在这个示例中,我们使用了一个简单的字符串作为密钥,但在生产环境中需要使用更复杂且保密的密钥。

(2)validityInMilliseconds - JWT的过期时间

private long validityInMilliseconds = 3600000; // 1 hour
  • 这里的 validityInMilliseconds 设置JWT的有效期为1小时,单位是毫秒。即,生成的JWT将在1小时后过期,用户需要重新登录或刷新JWT。

  • 这个有效期值可以根据实际需求进行调整,例如设置为30分钟、1天等。

(3)generateToken - 生成JWT

public String generateToken(String userId, String username) {
    return Jwts.builder()
            .setSubject(userId)  // 设置JWT的主体部分为用户ID
            .claim("username", username) // 设置自定义的声明,例如用户名
            .setIssuedAt(new Date())  // 设置签发时间
            .setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds))  // 设置过期时间
            .signWith(SignatureAlgorithm.HS256, secretKey)  // 使用HS256算法和密钥进行签名
            .compact();
}
  • setSubject(userId)

    • setSubject 方法设置JWT的subject部分为 userId。在JWT标准中,subject通常用于存储用户的唯一标识符。

    • 使用 userId 作为subject,确保每个JWT代表的是一个独立的用户,可以在后续的请求中唯一标识用户身份。

  • claim("username", username)

    • 使用 claim 方法,我们将额外的自定义信息(在这个例子中是用户名)添加到JWT的负载(payload)中。JWT允许存储多个声明(claims),我们可以在JWT中添加其他有用的信息(如角色、权限等)。

    • 这个声明将会存在JWT的负载部分,可以在解码JWT时提取并使用。

  • setIssuedAt(new Date())

    • setIssuedAt 设置JWT的签发时间(iat)。此字段用于表示JWT何时被创建。这里使用 new Date() 获取当前时间。

  • setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds))

    • setExpiration 设置JWT的过期时间(exp)。它的值为当前时间加上JWT的有效期(1小时)。当JWT过期时,用户将无法再使用该JWT访问系统,必须重新获取一个有效的JWT。

  • signWith(SignatureAlgorithm.HS256, secretKey)

    • signWith 方法使用指定的签名算法(这里选择了HS256)和secretKey密钥来签署JWT,确保令牌在传输过程中不会被篡改。

    • HS256 是一种常用的对称加密算法,表示“使用HMAC(Hash-based Message Authentication Code)算法和SHA-256散列函数进行签名”。

  • compact()

    • compact 方法将所有的声明、签名和头部信息组合成一个单独的字符串,这个字符串就是我们最终的JWT令牌。

(4)extractUserId - 提取JWT中的用户ID

public String extractUserId(String token) {
    return extractClaim(token, Claims::getSubject);
}
  • extractUserId 方法用于从JWT中提取用户的ID(subject)。它通过调用 extractClaim 方法,传入Claims::getSubject来获取JWT的subject字段。

  • subject 存储了我们在生成JWT时设置的用户ID,因此可以用它来识别用户。

(5)extractUsername - 从JWT中提取用户名

public String extractUsername(String token) {
    return extractClaim(token,claims -> claims.get("username", String.class)); // 提取username
}
  • 这个方法用于从JWT中提取存储在claims部分的username。它通过调用 extractClaim 来提取claims中的username

(6)extractClaim - 提取JWT中的特定声明

public <T> T extractClaim(String token, ClaimsExtractor<T> claimsExtractor) {
    final Claims claims = extractAllClaims(token);
    return claimsExtractor.extract(claims);
}
  • extractClaim 方法从JWT中提取特定的声明。它首先通过 extractAllClaims 获取JWT的所有声明,然后使用 claimsExtractor 提取我们需要的信息。

  • ClaimsExtractor 是一个函数式接口,可以方便地提取各种声明,如用户名、过期时间等。

(7)extractAllClaims - 解析JWT并提取所有声明

private Claims extractAllClaims(String token) {
    return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody();
}
  • extractAllClaims 方法解析JWT并返回所有的声明。它通过 Jwts.parser() 解析JWT,使用签名密钥 secretKey 验证JWT的完整性。

  • parseClaimsJws 返回一个Jws对象,其中包含JWT的所有信息。调用 getBody() 返回JWT的负载部分(即声明部分)。

(8)isTokenExpired - 判断JWT是否过期

public boolean isTokenExpired(String token) {
    return extractExpiration(token).before(new Date());
}
  • isTokenExpired 方法用于检查JWT是否过期。它通过 extractExpiration 获取JWT的过期时间,并与当前时间进行比较。

  • 如果JWT的过期时间早于当前时间,则说明JWT已经过期。

(9)validateToken - 验证JWT的有效性

public boolean validateToken(String token, String userId) {
    return (userId.equals(extractUserId(token)) && !isTokenExpired(token));
}
  • validateToken 方法用于验证JWT的有效性。它会检查JWT中的userId是否与传入的userId匹配,同时确认JWT是否未过期。

  • 只有在两者条件都满足时,JWT才被视为有效。

(10) ClaimsExtractor 接口

@FunctionalInterface
public interface ClaimsExtractor<T> {
    T extract(Claims claims);
}
  • ClaimsExtractor 是一个函数式接口,用于从JWT的声明中提取具体的信息。例如,可以用它来提取subjectexpiration等信息。

如果我将ClaimsExtractor<T> claimsExtractor作为某个个方法声明的参数。例如:

  • public <T> T extractClaim(String token, ClaimsExtractor<T> claimsExtractor)

当我要调用这个方法extractClaim(token, Claims::getSubject);

这里解释为什么Claims::getSubject可以作为ClaimsExtractor<T> claimsExtractor类型的参数

  • ClaimsExtractor<T>是一个函数式接口的对象,而在现代当需要一个函数时接口的对象作为参数的时候,一般都是用lambda表达式来作为参数。
  • 函数式接口内部定义的时候是 T extract(Claims claims); 这里就代表当你需要lambda表达式来表示这个函数式接口对象的时候,需要这个lambda表达式的参数为Claims类型的对象,而返回结果因为是T,意思就是在这里返回结果类型不作任何限制。
  • Claims::getSubject是方法引用,等价于claims -> claims.getSubject();所以满足了参数为Claims类型,而返回结果随意,有返回值就行。claims.getSubject()具有一个string类型的返回值。

2.3 JWT验证:创建请求过滤器

在Spring Security中,JWT的验证通常通过自定义过滤器(如 JwtAuthenticationFilter)来实现。该过滤器的任务是从请求中提取JWT、验证其有效性,并根据其内容设定当前的认证信息。这个过滤器将JWT令牌解析成用户身份信息,并将其放入Spring Security的SecurityContext中,以便后续的请求能够进行身份验证。

为什么需要请求过滤器

我们已经有了JWT生成的工具类,现在需要通过过滤器来执行以下操作:

  1. 提取JWT:从请求头中获取JWT令牌,通常会放在Authorization头部,形式为Bearer <JWT>
  2. 验证JWT:使用JwtUtil工具类对JWT进行解析和验证(例如,检查是否过期)。
  3. 提取用户信息:从JWT中提取用户ID、用户名等信息。
  4. 将用户信息设置到Spring Security的SecurityContext:如果JWT有效,则将用户信息保存到Spring Security的上下文中,让后续的请求能够访问到用户身份。

下面是一个简单的JWT认证过滤器的实现。这个过滤器继承自OncePerRequestFilter,该类确保过滤器每次请求只会被调用一次。

在较新版本的 Spring Boot 中(特别是 Spring Boot 3.x),javax.servlet 包已经被迁移到了 jakarta.servlet。这是因为 Java EE 被迁移到了 Jakarta EE。

如果你使用的是 Spring Boot 3.x,需要将导入语句改为使用 jakarta.servlet

package com.aqian.wenlike.common;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private  JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 从请求头中获取JWT
        String token = getJwtFromRequest(request);

        // 如果JWT存在且有效,则进行验证
        if (token != null && jwtUtil.validateToken(token, jwtUtil.extractUserId(token))) {
            Long userId = jwtUtil.extractUserId(token);
            String username = jwtUtil.extractUsername(token);

            // 创建Authentication对象并设置到SecurityContext中
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    username, null, null); // 用户名和权限信息,这里没有权限信息所以传null
            SecurityContextHolder.getContext().setAuthentication(authentication); // 设置认证信息
        }

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

    /**
     * 从HTTP请求头中提取JWT令牌
     * @param request HTTP请求
     * @return JWT令牌
     */
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization"); // 从Authorization头部获取JWT
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) { // 如果以"Bearer "开头
            return bearerToken.substring(7); // 去掉"Bearer ",返回JWT部分
        }
        return null; // 如果没有JWT令牌,返回null
    }
}

(1)JwtAuthenticationFilter 继承 OncePerRequestFilter

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;
  • JwtAuthenticationFilter继承自OncePerRequestFilter,它会确保过滤器在每个请求中只被执行一次。
  • 过滤器的构造函数接受JwtUtil工具类,这样就可以使用该工具类来验证JWT令牌。

(2) doFilterInternal 方法

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    // 从请求头中获取JWT
    String token = getJwtFromRequest(request);

    // 如果JWT存在且有效,则进行验证
    if (token != null && jwtUtil.validateToken(token, jwtUtil.extractUserId(token))) {
        Long userId = jwtUtil.extractUserId(token);
        String username = jwtUtil.extractUsername(token);

        // 创建Authentication对象并设置到SecurityContext中
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                username, null, null); // 用户名和权限信息,这里没有权限信息所以传null
        SecurityContextHolder.getContext().setAuthentication(authentication); // 设置认证信息
    }

    // 继续执行过滤链
    filterChain.doFilter(request, response);
}
  • 获取JWT:通过 request.getHeader("Authorization") 从 HTTP 请求头中提取 JWT。通常前缀是 Bearer ,我们去掉前缀后得到实际的 JWT 字符串。
  • 验证JWT:使用 jwtUtil.validateToken(token) 方法来验证 JWT 是否有效。
  • 提取用户ID:调用 jwtUtil.extractUserId(token) 从 JWT 中提取出存储的 userId
  • 加载用户信息:通过 customUserDetailsService.loadUserById(userId) 获取当前用户的详细信息(通常是从数据库或缓存中加载用户数据)。
  • 设置认证信息:创建 UsernamePasswordAuthenticationToken 并将其放入 SecurityContext 中,确保 Spring Security 能够识别并使用当前用户的身份信息。

(3) getJwtFromRequest 方法

private String getJwtFromRequest(HttpServletRequest request) {
    String bearerToken = request.getHeader("Authorization"); // 从Authorization头部获取JWT
    if (bearerToken != null && bearerToken.startsWith("Bearer ")) { // 如果以"Bearer "开头
        return bearerToken.substring(7); // 去掉"Bearer ",返回JWT部分
    }
    return null; // 如果没有JWT令牌,返回null
}
  • getJwtFromRequest方法用于从HTTP请求的Authorization头部提取JWT令牌。JWT通常放在Authorization头部,以Bearer <token>的形式传递。我们提取出Bearer后的部分,就是实际的JWT令牌。

2.4. 配置过滤器

创建了JwtAuthenticationFilter过滤器之后,我们还需要在Spring Security的过滤链中注册它。可以通过SecurityConfig配置类来配置过滤器。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Autowired  // 自动注入 JwtAuthenticationFilter
    private  JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()  // 禁用CSRF保护
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login", "/register").permitAll()  // 允许不需要认证的请求
                .anyRequest().authenticated()  // 其他请求需要认证
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);  // 将 JwtAuthenticationFilter 加入过滤链

        return http.build();
    }
}
  • addFilterBefore:我们通过addFilterBefore方法将JwtAuthenticationFilter过滤器添加到Spring Security的过滤链中。它会在UsernamePasswordAuthenticationFilter之前执行,这样就可以先验证JWT令牌,然后再进行基于用户名和密码的认证。

2.5.首次请求校验与后续请求校验 

自动处理权限校验

Spring Security 会在请求的生命周期内自动管理 SecurityContext,并根据 SecurityContext 中存储的用户认证信息来执行权限校验。这个过程是自动完成的,无需你手动干预。

  • 首次请求:在第一次请求中,JwtAuthenticationFilter 等过滤器会从请求头中提取 JWT,验证它并解析出用户的信息(如用户名、角色)。然后,Spring Security 会将这些信息存入 SecurityContext 中。

  • 后续请求:在后续的请求中,Spring Security 会从 SecurityContext 中自动获取用户的认证信息,包括用户的权限和角色。当请求进入控制器时,Spring Security 会自动检查 SecurityContext 中的认证信息,并根据权限要求进行校验。

如何自动校验权限

Spring Security 提供了许多机制来自动校验权限,比如基于注解的控制(如 @PreAuthorize@Secured)。这些注解会在方法执行之前自动验证当前用户是否有访问该方法的权限,基于 SecurityContext 中的认证信息。

(1)权限注解(如 @PreAuthorize:这些注解会在方法调用之前自动获取 SecurityContext 中的认证信息(即当前用户的角色和权限),然后与注解中定义的权限要求进行比较。

例如,@PreAuthorize("hasRole('ADMIN')") 注解会检查当前用户是否具备 ADMIN 角色,如果用户有权限,则执行方法;如果没有,则会抛出异常,拒绝访问。

(2)基于 URL 的授权:除了注解,Spring Security 还可以通过配置 HTTP 请求路径与角色/权限的映射来进行访问控制。例如,在 HttpSecurity 配置中,你可以指定哪些路径需要哪些角色访问:

http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/admin/**").hasRole("ADMIN")  // 只有具有 ADMIN 角色的用户可以访问 /admin/**
        .requestMatchers("/user/**").hasRole("USER")    // 只有具有 USER 角色的用户可以访问 /user/**
        .requestMatchers("/public/**").permitAll()      // 所有人都可以访问 /public/**
        .anyRequest().authenticated()                  // 其他请求都需要认证才能访问
    );

在这个配置中,Spring Security 会自动根据 SecurityContext 中存储的用户认证信息(如角色)来决定是否允许访问 /admin/**/user/** 路径。

流向总结

首次请求

  • Spring Security 从请求头中提取 JWT。
  • 验证 JWT 并解析出用户信息。
  • 创建 UsernamePasswordAuthenticationToken 并将其存入 SecurityContext

后续请求

  • Spring Security 自动从 SecurityContext 中获取 Authentication 对象。
  • 根据 Authentication 对象中的权限和角色,执行权限校验。
  • 使用权限注解(如 @PreAuthorize)或 HTTP 请求路径配置来自动控制访问权限。

3. JWT安全性

JWT(JSON Web Token)作为一种常用于身份验证和授权的机制,在实际应用中需要特别注意其安全性问题。为了确保JWT的安全性,开发者需要遵循一系列的最佳实践。接下来,我将详细讲解关于JWT安全性的几个方面。

3.1 JWT过期时间(Expiration Time)

为什么需要设置过期时间?

JWT通常包含敏感信息,如用户身份、角色和权限等。如果一个JWT没有过期时间,它就会一直有效,可能会被恶意用户滥用,造成安全风险。因此,设置合理的过期时间exp)是十分重要的。

如何设置过期时间?

JWT规范中允许设置exp(Expiration Time)字段,用于指定JWT的过期时间。JWT的过期时间可以是一个时间戳,表示从创建时刻开始多久后JWT会失效。

  • 短期JWT: 通常建议JWT的过期时间设置为较短的时间,比如30分钟、1小时等,这样即使JWT被盗用,恶意用户也只能在短时间内使用。
  • 长期有效的JWT: 对于某些场景,如用户在一个会话中长时间活动,可以考虑设置较长的有效期。但需要与刷新令牌机制结合使用。

如何在JWT中设置过期时间?

在JWT的生成过程中,可以设置exp字段。例如,如果你使用jjwt库来生成JWT,可以这样设置过期时间:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;

public class JwtUtil {

    private static final String SECRET_KEY = "yourSecretKey";

    public String generateToken(String username) {
        long now = System.currentTimeMillis();
        long expirationTime = 1000 * 60 * 60;  // 设置过期时间为1小时
        Date expirationDate = new Date(now + expirationTime);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date(now))
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }
}

在这个例子中,setExpiration方法用来设置JWT的过期时间。

如何应对过期的JWT?

一旦JWT过期,它就不再有效,必须重新进行认证。如果JWT过期并且没有刷新令牌机制,用户需要重新登录。而如果有刷新令牌机制,用户可以通过提供刷新令牌来获取新的JWT。


3.2 刷新Token机制(Refresh Token)

刷新Token的作用

由于JWT通常采用较短的过期时间(比如30分钟、1小时),而用户可能会在不频繁登录的情况下长时间使用应用。为了解决这个问题,可以使用刷新令牌(Refresh Token)。

刷新令牌是一个独立的、较长时间有效的令牌,通常用于在JWT过期后,通过刷新令牌获取新的JWT。

刷新Token工作原理

  1. 用户登录: 用户第一次登录时,系统会生成两个令牌:

    • 访问令牌(Access Token):短期有效的JWT,用于访问受保护的资源。
    • 刷新令牌(Refresh Token):长期有效的令牌,用于刷新过期的访问令牌。
  2. JWT过期: 当访问令牌过期时,用户无需重新登录,只需提供有效的刷新令牌。

  3. 刷新令牌请求: 用户通过发送一个请求,将刷新令牌发送到授权服务器。

  4. 返回新的访问令牌: 如果刷新令牌有效,服务器会生成一个新的访问令牌(JWT)并返回给用户。

  5. 刷新令牌的过期:刷新令牌本身也应该有过期时间,通常设置为几天或几周,防止被滥用。

如何实现刷新Token机制

通过刷新令牌,你可以在后台实现一个API接口来刷新JWT。例如,使用Spring Security时,可以设计一个刷新令牌的接口来实现。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

@Component
public class JwtRefreshTokenUtil {

    @Value("${jwt.secret}")
    private String secretKey;

    public String refreshToken(String refreshToken) {
        // 解析刷新令牌并验证有效性
        if (isValid(refreshToken)) {
            String username = extractUsername(refreshToken);  // 从刷新令牌提取用户信息
            return generateAccessToken(username);  // 生成新的访问令牌
        } else {
            throw new InvalidTokenException("Invalid refresh token.");
        }
    }

    // 刷新令牌的有效性检查
    private boolean isValid(String refreshToken) {
        // 在此处进行刷新令牌的有效性检查
        return true;
    }

    // 根据用户信息生成新的访问令牌
    private String generateAccessToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60))  // 设置新的过期时间
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
}

3.3 密钥管理(Key Management)

为何密钥管理至关重要?

JWT的安全性依赖于密钥的安全性。如果密钥泄露,攻击者可以伪造JWT,导致严重的安全漏洞。因此,密钥的生成、存储、轮换和保护需要特别关注。

  • 随机性和复杂性:密钥应该是随机生成的,包含大写字母、小写字母、数字和特殊字符,以增加其复杂度。越复杂的密钥,破解的难度越大。
  • 足够长的长度:为了保证安全性,密钥应该足够长。推荐的长度至少为 256位(即 32 字节)。如果使用对称加密算法(例如 HMAC),可以使用 256 位或更长的密钥。

使用 application.properties

你可以在 application.properties 中定义密钥:

jwt.secretKeyString=mysecretkey

然后通过 @Value 注解在你的类中读取这个密钥:

@Value("${jwt.secretKeyString}")
private String secretKeyString;

        // 方式1:将密钥字符串转换为 byte[],用于 HS256 签名
        byte[] secretKey = secretKeyString.getBytes(StandardCharsets.UTF_8);
        
        // 生成 JWT Token
        return Jwts.builder()
                .setSubject(userId.toString())  // 设置用户 ID
                .claim("username", username)    // 设置用户名为自定义的 claim
                .setIssuedAt(new Date())       // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 使用 byte[] 作为密钥
                .compact();  // 生成并返回 JWT Token
    

    
        // 方式2:将密钥字符串转换为 Base64 编码的字符串,适用于 String 密钥方式
        String secretKey = Base64.getEncoder().encodeToString(secretKeyString.getBytes(StandardCharsets.UTF_8));
        
        // 生成 JWT Token
        return Jwts.builder()
                .setSubject(userId.toString())  // 设置用户 ID
                .claim("username", username)    // 设置用户名为自定义的 claim
                .setIssuedAt(new Date())       // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS256, secretKeyBase64)  // 使用 Base64 编码的 String 作为密钥
                .compact();  // 生成并返回 JWT Token
    

   
        // 方式3:将密钥字符串转换为 SecretKey 对象,适用于 Key 类型密钥
        SecretKey secretKey = new SecretKeySpec(secretKeyString.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
        
        // 生成 JWT Token
        return Jwts.builder()
                .setSubject(userId.toString())  // 设置用户 ID
                .claim("username", username)    // 设置用户名为自定义的 claim
                .setIssuedAt(new Date())       // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + validityInMilliseconds)) // 设置过期时间
                .signWith(secretKey)  // 使用 SecretKey 对象作为密钥
                .compact();  // 生成并返回 JWT Token

密钥管理的最佳实践

  • 使用强密钥:生成足够复杂的密钥,避免使用容易猜测的密钥(例如“123456”)。建议使用高强度的密钥。

  • 密钥存储:不要将密钥硬编码在代码中,而是应当通过环境变量、配置文件或专门的密钥管理服务(如AWS KMS、HashiCorp Vault)来安全存储密钥。

  • 密钥轮换:定期轮换密钥,减少密钥泄漏的风险。如果密钥泄露,立即撤销并更换密钥。

  • 密钥保护:密钥需要加密存储,并确保仅授权的人员和系统可以访问密钥。

示例:强密钥的生成

import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.NoSuchAlgorithmException;

public class SecretKeyUtil {
    public static String generateStrongSecretKey() throws NoSuchAlgorithmException {
        // 使用 KeyGenerator 确保密钥的加密安全性
        KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA256");
        keyGenerator.init(256);
        SecretKey secretKey = keyGenerator.generateKey();
        
        // 将密钥转换为 Base64 编码的字符串,方便存储和传输
        return Base64.getEncoder().encodeToString(secretKey.getEncoded());
    }

    // 重载方法,允许指定密钥长度
    public static String generateStrongSecretKey(int keyLength) throws NoSuchAlgorithmException {
        KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA256");
        keyGenerator.init(keyLength);
        SecretKey secretKey = keyGenerator.generateKey();
        
        return Base64.getEncoder().encodeToString(secretKey.getEncoded());
    }
}
  • 高安全性
    • 使用 KeyGenerator 生成符合加密算法标准的密钥
    • 利用 HmacSHA256 算法确保密钥的随机性和安全性
  • 便于存储和传输
    • 通过 Base64 编码将密钥转换为字符串
    • 可以轻松存储在配置文件、数据库或环境变量中
    • 支持跨平台、跨语言的密钥传输
  • 直接适用于加密算法
    • 生成的密钥可直接用于 HMAC、JWT 签名等加密操作
    • 密钥格式已经标准化,无需额外转换
  • 灵活性
    • 提供重载方法,可自定义密钥长度
    • 支持根据具体需求调整密钥生成参数

 使用示例

try {
    // 生成默认 256 位密钥
    String secretKey = SecretKeyUtil.generateStrongSecretKey();
    System.out.println(secretKey);

    // 生成自定义长度的密钥
    String customLengthKey = SecretKeyUtil.generateStrongSecretKey(512);
    System.out.println(customLengthKey);
} catch (NoSuchAlgorithmException e) {
    // 处理异常
    e.printStackTrace();
}

3.4 HTTPS传输(Secure Transmission)

为何使用HTTPS?

JWT的安全性不仅仅依赖于密钥和过期时间,还需要保证令牌在传输过程中不被截获。如果JWT通过普通的HTTP协议传输,攻击者可能在中间人攻击(MITM)中截取到令牌,导致令牌泄露。

如何确保安全传输?

  • 强制HTTPS:使用HTTPS加密HTTP请求,确保JWT在传输过程中不会被窃取或篡改。

  • SSL/TLS证书:确保服务器配置了有效的SSL/TLS证书,客户端和服务器之间的通信都通过加密通道进行。

配置Spring Boot强制HTTPS

// application.properties
server.port=443
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=yourpassword
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=tomcat

3.5 防止Token滥用

如何防止Token滥用?

  • 使用短期JWT:尽量将JWT的有效期设置为较短时间,这样即使JWT被盗用,恶意用户能使用的时间也有限。
  • 结合刷新令牌:使用刷新令牌来替代长时间有效的JWT。通过刷新令牌机制,用户可以在访问令牌过期后重新获取新的JWT,而刷新令牌本身有效期较长。
  • Token撤销机制:可以在服务端实现令牌撤销列表,在刷新令牌时检查该令牌是否已被撤销。

标签:令牌,String,过期,Spring,JWT,token,密钥,Security
From: https://blog.csdn.net/m0_73837751/article/details/144223862

相关文章

  • JWT详解
    JWT简介1.什么是JWT在介绍JWT之前,我们先来回顾一下利用token进行用户身份验证的流程:客户端使用用户名和密码请求登录服务端收到请求,验证用户名和密码验证成功后,服务端会签发一个token,再把这个token返回给客户端客户端收到token后可以把它存储起来,比如放到cookie中客户端每......
  • SpringBoot自动装配原理
    SpringBoot自动装配原理在介绍SpringBoot自动装配原理之前我们需要先看一个注解-->SpringBootApplication@SpringBootApplicationpublicclassBackendApp{publicstaticvoidmain(String[]args){SpringApplication.run(BackendApp.class,args);......
  • 基于 SpringBoot 的招生管理系统
    摘要在Internet高速发展的今天,我们生活的各个领域都涉及到计算机的应用,其中包括招生管理系统的网络应用,在外国招生管理系统已经是很普遍的方式,不过国内的管理网站可能还处于起步阶段。招生管理系统具有招生公告信息管理功能的选择。招生管理系统采用java技术,基于springboo......
  • SpringBoot外卖点餐系统
    摘要随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势社会的发展和科学技术的进步,互联网技术越来越受欢迎。网络计算机的生活方式逐渐受到广大人民群众的喜爱,也逐渐进入了每个用户的使用。互联网具有便利性,速度快,效率高,成本低等优点......
  • Spring注解之@ConditionalOnProperty
    @ConditionalOnProperty是SpringFramework提供的一个注解,用于控制某个配置类或Bean是否加载到Spring容器中。它的行为取决于某些配置属性的值。作用@ConditionalOnProperty会检查指定的配置属性(通常来自application.yml或application.properties文件),如果属性满......
  • Spring Boot教程之十八:Cache Provider
    SpringBoot–CacheProviderSpring框架支持透明地向应用程序添加缓存。CacheProvider授权程序员在应用程序中明确配置缓存。它整合了各种CacheProvider,例如EhCache、Redis、Guava、Caffeine等 。它使经常访问的对象、图像和数据更靠近您需要它们的位置,通过不多次访问......
  • Spring Boot教程之十九:日志记录
    SpringBoot–日志记录SpringBoot中的日志记录在SpringBoot应用程序中起着至关重要的作用,用于记录应用程序内的信息、操作和事件。它还用于监视应用程序的性能、了解应用程序的行为以及识别应用程序内的问题。SpringBoot通过提供各种日志记录框架来提供灵活的日志记......
  • IntelliJ+SpringBoot项目实战(27)--实现国际化项目开发
        因为有时候承接一些国外项目,项目需要支持国际化。国际化包括页面静态文字的国际化,接口中返回的信息的国际化,JS脚本里内容的国际化。一、配置国际化语言    在openjweb-core工程中增加默认国际化语言配置类:packageorg.openjweb.core.config;importja......
  • springboot毕设 青少年视力筛查系统 程序+论文
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着现代科技的飞速发展,青少年群体对于电子产品的依赖程度日益加深,长时间面对屏幕不仅改变了他们的生活方式,也对视力健康构成了严峻挑战。近年来,我国......
  • springboot毕设 汽车管理系统 程序+论文
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着现代科技的飞速发展,汽车行业正经历着前所未有的变革。智能化、信息化已成为汽车产业发展的新趋势。在这一背景下,汽车管理系统的研发显得尤为重要......