From f803241c78b2f773ad584be095adfe8f4e179d7f Mon Sep 17 00:00:00 2001 From: bunny <1319900154@qq.com> Date: Wed, 16 Jul 2025 00:05:10 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E7=94=9F=E6=88=90JWT=E4=BB=A4?= =?UTF-8?q?=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring-security/ReadMe.md | 166 +++++++++++++ spring-security/step-2/pom.xml | 8 +- .../security/service/DbUserDetailService.java | 6 +- .../security/service/JwtBearTokenService.java | 86 +++++++ .../com/spring/step2/utils/JwtTokenUtil.java | 223 ++++++++++++++++++ .../step-2/src/main/resources/application.yml | 7 + .../config/JwtBearTokenServiceTest.java | 56 +++++ 7 files changed, 548 insertions(+), 4 deletions(-) create mode 100644 spring-security/step-2/src/main/java/com/spring/step2/security/service/JwtBearTokenService.java create mode 100644 spring-security/step-2/src/main/java/com/spring/step2/utils/JwtTokenUtil.java create mode 100644 spring-security/step-2/src/test/java/com/spring/step2/security/config/JwtBearTokenServiceTest.java diff --git a/spring-security/ReadMe.md b/spring-security/ReadMe.md index 946ecc9..e22737f 100644 --- a/spring-security/ReadMe.md +++ b/spring-security/ReadMe.md @@ -1355,3 +1355,169 @@ public class AuthenticationEvents { } } ``` + +## 实现JWT的认证 + +### 生成JWT令牌 + +> [!TIP] +> +> 如果需要JWTUtil工具类需要到项目的`com.spring.step2.utils.JwtTokenUtil`下寻找。 + +在Spring的配置文件中写入下面的内容。 + +```yaml +jwtToken: + # 密钥 + secret: aVeryLongAndSecureRandomStringWithAtLeast32Characters + # 主题 + subject: SecurityBunny + # 过期事件 7天 + expired: 604800 +``` + +之后要读取配置文件中的信息。 + +在文件中包含三个方法,分别是:创建令牌,解析令牌为用户名,解析令牌为用户Id。 + +```java +@Configuration +@ConfigurationProperties(prefix = "jwt-token") +public class JwtBearTokenService { + + @Value("${jwtToken.secret}") + public String secret; + + @Value("${jwtToken.subject}") + public String subject; + + // private final SecretKey securityKey = Keys.hmacShaKeyFor("Bunny-Auth-Server-Private-SecretKey".getBytes(StandardCharsets.UTF_8)); + // JWT 的 秘钥 + @Value("${jwtToken.expired}") + public Long expired; + + /** + * 创建Token + * + * @param userId 用户Id + * @param username 用户名 + * @return 令牌Token + */ + public String createToken(Long userId, String username, + List roles, List permissions) { + SecretKey key = getSecretKey(); + // return JwtTokenUtil.createToken(userId, username, subject, key, expired); + return JwtTokenUtil.createToken(userId, username, roles, permissions, subject, key, expired); + } + + /** + * 获取安全密钥 + * + * @return {@link SecretKey} + */ + private SecretKey getSecretKey() { + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + return new SecretKeySpec(secretBytes, "HmacSHA256"); + } + + /** + * 根据Token获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getUsername(token, secretKey); + } + + /** + * 根据Token获取用户Id + * + * @param token 令牌 + * @return 用户Id + */ + public Long getUserIdFromToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getUserId(token, secretKey); + } + + /** + * 根据Token获取用户Id + * + * @param token 令牌 + * @return 用户Id + */ + public Map getMapByToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getMapByToken(token, secretKey); + } + +} +``` + +之后对方法进行测试。 + +```java +@SpringBootTest +class JwtBearTokenServiceTest { + + private String token; + + @Autowired + private JwtBearTokenService jwtBearTokenService; + + @Autowired + private DbUserDetailService dbUserDetailService; + + @Test + void createToken() { + long userId = 1944384432521744386L; + List roles = dbUserDetailService.findUserRolesByUserId(userId); + List permissions = dbUserDetailService.findPermissionByUserId(userId); + + String token = jwtBearTokenService.createToken(userId, + "Bunny", + roles, + permissions); + this.token = token; + System.out.println(token); + } + + @Test + void getUsernameFromToken() { + createToken(); + + String username = jwtBearTokenService.getUsernameFromToken(token); + System.out.println(username); + + Long userId = jwtBearTokenService.getUserIdFromToken(token); + System.out.println(userId); + + Map map = jwtBearTokenService.getMapByToken(token); + + List roles = (List) map.get("roles"); + System.out.println(roles); + + // 安全转换 permissions + List permissions = (List) map.get("permissions"); + System.out.println(permissions); + } +} +``` + +输出以下内容: + +```properties +# 生成的令牌 +eyJ6aXAiOiJHWklQIiwiYWxnIjoiSFMyNTYifQ.H4sIAAAAAAAA_0WMzQ6CMBCE32XPNIHSH-hNowcOepB4Mh5qWRKMFNLSRGJ8d6tiPEyyM7PzPcCHCyio0QTXTfM6WDtDAngfQWWSU16yjLMEgkdXNTErGcuLKMppJt-3-JZW9xhBP4AbbuhBnWC12VX7GBzr7QHOCYzo-s77brCf-m-VcqibZbqY-H-duojFFAUvTUtaqTVhVEpSmEIQnjV5m_NUtIbC8wV0LG6kzgAAAA.lf4TXuafIQ2X5Ec3CsKR5ZN3q9KpJkeEBiYQbmNMuoQ +# 解析的用户名 +Bunny +# 解析的用户Id +1944384432521744386 +# 解析的用户角色 +[ADMIN, USER] +# 解析的用户权限 +[permission::read, role::read] +``` + diff --git a/spring-security/step-2/pom.xml b/spring-security/step-2/pom.xml index 0eae0c1..c5f9dd6 100644 --- a/spring-security/step-2/pom.xml +++ b/spring-security/step-2/pom.xml @@ -19,9 +19,15 @@ 17 17 17 + 0.12.6 - + + + io.jsonwebtoken + jjwt + ${jwt.version} + diff --git a/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java b/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java index d80ed6d..02c6312 100644 --- a/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java +++ b/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java @@ -37,7 +37,7 @@ public class DbUserDetailService implements UserDetailsService { Long userId = userEntity.getId(); // 设置用户角色 - String[] roles = findUserRolesByUserId(userId); + String[] roles = findUserRolesByUserId(userId).toArray(String[]::new); // 设置用户权限 List permissionsByUserId = findPermissionByUserId(userId); @@ -66,9 +66,9 @@ public class DbUserDetailService implements UserDetailsService { * @param userId 用户id * @return 当前用户的角色信息 */ - public String[] findUserRolesByUserId(Long userId) { + public List findUserRolesByUserId(Long userId) { List roleList = userMapper.selectRolesByUserId(userId); - return roleList.stream().map(RoleEntity::getRoleCode).toArray(String[]::new); + return roleList.stream().map(RoleEntity::getRoleCode).toList(); } /** diff --git a/spring-security/step-2/src/main/java/com/spring/step2/security/service/JwtBearTokenService.java b/spring-security/step-2/src/main/java/com/spring/step2/security/service/JwtBearTokenService.java new file mode 100644 index 0000000..b3162a8 --- /dev/null +++ b/spring-security/step-2/src/main/java/com/spring/step2/security/service/JwtBearTokenService.java @@ -0,0 +1,86 @@ +package com.spring.step2.security.service; + +import com.spring.step2.utils.JwtTokenUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "jwt-token") +public class JwtBearTokenService { + + @Value("${jwtToken.secret}") + public String secret; + + @Value("${jwtToken.subject}") + public String subject; + + // private final SecretKey securityKey = Keys.hmacShaKeyFor("Bunny-Auth-Server-Private-SecretKey".getBytes(StandardCharsets.UTF_8)); + // JWT 的 秘钥 + @Value("${jwtToken.expired}") + public Long expired; + + /** + * 创建Token + * + * @param userId 用户Id + * @param username 用户名 + * @return 令牌Token + */ + public String createToken(Long userId, String username, + List roles, List permissions) { + SecretKey key = getSecretKey(); + // return JwtTokenUtil.createToken(userId, username, subject, key, expired); + return JwtTokenUtil.createToken(userId, username, roles, permissions, subject, key, expired); + } + + /** + * 获取安全密钥 + * + * @return {@link SecretKey} + */ + private SecretKey getSecretKey() { + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + return new SecretKeySpec(secretBytes, "HmacSHA256"); + } + + /** + * 根据Token获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getUsername(token, secretKey); + } + + /** + * 根据Token获取用户Id + * + * @param token 令牌 + * @return 用户Id + */ + public Long getUserIdFromToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getUserId(token, secretKey); + } + + /** + * 根据Token获取用户Id + * + * @param token 令牌 + * @return 用户Id + */ + public Map getMapByToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getMapByToken(token, secretKey); + } + +} diff --git a/spring-security/step-2/src/main/java/com/spring/step2/utils/JwtTokenUtil.java b/spring-security/step-2/src/main/java/com/spring/step2/utils/JwtTokenUtil.java new file mode 100644 index 0000000..01d8200 --- /dev/null +++ b/spring-security/step-2/src/main/java/com/spring/step2/utils/JwtTokenUtil.java @@ -0,0 +1,223 @@ +package com.spring.step2.utils; + + +import com.spring.step2.domain.vo.result.ResultCodeEnum; +import com.spring.step2.exception.SecurityException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.micrometer.common.lang.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Slf4j +public class JwtTokenUtil { + + /** + * 使用默认主题,默认秘钥,自定义时间,创建集合形式token + * + * @param map 集合 + * @param day 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, SecretKey key, Long day) { + return Jwts.builder() + .subject(subject) + .signWith(key) + .expiration(new Date(System.currentTimeMillis() + day)) + .claims(map) + .id(UUID.randomUUID().toString()) + .compressWith(Jwts.ZIP.GZIP).compact(); + } + + /** + * 使用自定义主题,自定义时间,创建集合形式token + * + * @param map 集合 + * @param subject 主题 + * @param time 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, SecretKey key, Date time) { + return Jwts.builder() + .subject(subject) + .expiration(time) + .claims(map) + .id(UUID.randomUUID().toString()) + .signWith(key) + .compressWith(Jwts.ZIP.GZIP).compact(); + } + + /** + * 创建集合形式token + * + * @param map 集合 + * @param subject 主题 + * @param day 过期时间 + * @return token + */ + public static String createTokenWithMap(Map map, String subject, SecretKey key, Integer day) { + return Jwts.builder() + .subject(subject) + .expiration(new Date(System.currentTimeMillis() + day)) + .claims(map) + .id(UUID.randomUUID().toString()) + .signWith(key) + .compressWith(Jwts.ZIP.GZIP).compact(); + } + + /** + * 根据用户名和ID创建token + * + * @param userId 用户ID + * @param username 用户名 + * @param day 过期时间 + * @return token值 + */ + public static String createToken(Long userId, String username, String subject, SecretKey key, Long day) { + return Jwts.builder() + .subject(subject) + .expiration(new Date(System.currentTimeMillis() + day)) + .claim("userId", userId) + .claim("username", username) + .id(UUID.randomUUID().toString()) + .signWith(key) + .compressWith(Jwts.ZIP.GZIP).compact(); + } + + /** + * 根据用户名和ID创建token + * 在载体中添加角色和权限 + * + * @param userId 用户ID + * @param username 用户名 + * @param day 过期时间 + * @return token值 + */ + public static String createToken(Long userId, String username, + List roles, List permissions, + String subject, SecretKey key, Long day) { + return Jwts.builder() + .subject(subject) + .expiration(new Date(System.currentTimeMillis() + day)) + .claim("userId", userId) + .claim("username", username) + .claim("roles", roles) + .claim("permissions", permissions) + .id(UUID.randomUUID().toString()) + .signWith(key) + .compressWith(Jwts.ZIP.GZIP).compact(); + } + + /** + * 使用token获取map集合,使用默认秘钥 + * + * @param token token + * @return map集合 + */ + public static Map getMapByToken(String token, SecretKey key) { + try { + if (!StringUtils.hasText(token)) throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + // 将 body 值转为map + return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload(); + + } catch (Exception exception) { + throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + } + } + + @Nullable + private static String getSubjectByTokenHandler(String token, SecretKey key) { + try { + if (!StringUtils.hasText(token)) throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + Jws claimsJws = Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + Claims body = claimsJws.getPayload(); + + return body.getSubject(); + + } catch (Exception exception) { + throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + } + } + + /** + * 根据token获取主题 + * + * @param token token + * @return 主题 + */ + public static String getSubjectByToken(String token, SecretKey key) { + return getSubjectByTokenHandler(token, key); + } + + /** + * 根据token获取用户ID + * + * @param token token + * @return 用户ID + */ + public static Long getUserId(String token, SecretKey key) { + try { + if (!StringUtils.hasText(token)) throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + + Jws claimsJws = Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + Claims claims = claimsJws.getPayload(); + + return Long.valueOf(String.valueOf(claims.get("userId"))); + } catch (Exception exception) { + throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + } + } + + /** + * 根据token获取用户名 + * + * @param token token + * @return 用户名 + */ + public static String getUsername(String token, SecretKey key) { + try { + if (!StringUtils.hasText(token)) return ""; + + Jws claimsJws = Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + Claims claims = claimsJws.getPayload(); + return (String) claims.get("username"); + } catch (Exception exception) { + throw new SecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED); + } + } + + /** + * 判断token是否过期 + * + * @param token token + * @return 是否过期 + */ + public static boolean isExpired(String token, SecretKey key) { + return isExpiredUtil(token, key); + } + + /** + * 判断是否过期 + * + * @param token token + * @return 是否过期 + */ + private static boolean isExpiredUtil(String token, SecretKey key) { + try { + Jws claimsJws = Jwts.parser().verifyWith(key).build().parseSignedClaims(token); + Date expiration = claimsJws.getPayload().getExpiration(); + + return expiration != null && expiration.before(new Date()); + } catch (Exception exception) { + log.error(exception.getMessage(), exception); + return true; + } + } +} \ No newline at end of file diff --git a/spring-security/step-2/src/main/resources/application.yml b/spring-security/step-2/src/main/resources/application.yml index ac05e9f..de3cc38 100644 --- a/spring-security/step-2/src/main/resources/application.yml +++ b/spring-security/step-2/src/main/resources/application.yml @@ -48,3 +48,10 @@ logging: com.spring: debug org.springframework.security: debug +jwtToken: + # 密钥 + secret: aVeryLongAndSecureRandomStringWithAtLeast32Characters + # 主题 + subject: SecurityBunny + # 过期事件 7天 + expired: 604800 diff --git a/spring-security/step-2/src/test/java/com/spring/step2/security/config/JwtBearTokenServiceTest.java b/spring-security/step-2/src/test/java/com/spring/step2/security/config/JwtBearTokenServiceTest.java new file mode 100644 index 0000000..d1c130f --- /dev/null +++ b/spring-security/step-2/src/test/java/com/spring/step2/security/config/JwtBearTokenServiceTest.java @@ -0,0 +1,56 @@ +package com.spring.step2.security.config; + +import com.spring.step2.security.service.DbUserDetailService; +import com.spring.step2.security.service.JwtBearTokenService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; +import java.util.Map; + +@SpringBootTest +class JwtBearTokenServiceTest { + + private String token; + + @Autowired + private JwtBearTokenService jwtBearTokenService; + + @Autowired + private DbUserDetailService dbUserDetailService; + + @Test + void createToken() { + long userId = 1944384432521744386L; + List roles = dbUserDetailService.findUserRolesByUserId(userId); + List permissions = dbUserDetailService.findPermissionByUserId(userId); + + String token = jwtBearTokenService.createToken(userId, + "Bunny", + roles, + permissions); + this.token = token; + System.out.println(token); + } + + @Test + void getUsernameFromToken() { + createToken(); + + String username = jwtBearTokenService.getUsernameFromToken(token); + System.out.println(username); + + Long userId = jwtBearTokenService.getUserIdFromToken(token); + System.out.println(userId); + + Map map = jwtBearTokenService.getMapByToken(token); + + List roles = (List) map.get("roles"); + System.out.println(roles); + + // 安全转换 permissions + List permissions = (List) map.get("permissions"); + System.out.println(permissions); + } +} \ No newline at end of file