Commit 30bc7df6 by HuAnYing
parents 9fe6021d 22ab4ed1
......@@ -39,6 +39,10 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
......
package com.jomalls.custom.page;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* @Author: Lizh
......@@ -34,11 +33,13 @@ public class PageRequest implements Pageable, Serializable {
/**
* 当前页码
*/
@Schema(description = "当前页码", example = "1", defaultValue = "1")
private long current;
/**
* 分页大小
*/
@Schema(description = "分页大小", example = "10", defaultValue = "10")
private long size;
/**
......
package com.jomalls.custom.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.RequiredTypeException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* JJWT Claims 适配器 — 将 Map 包装为 Claims 接口
* 用于兼容旧版本 token 的手动解析场景
*
* @author Lizh
* @date 2026-06-03
*/
public class JwtClaimsAdapter implements Claims {
private final Map<String, Object> claimsMap;
public JwtClaimsAdapter(Map<String, Object> claimsMap) {
this.claimsMap = new LinkedHashMap<>(claimsMap);
}
@Override
public String getIssuer() {
return get(ISSUER, String.class);
}
@Override
public String getSubject() {
return get(SUBJECT, String.class);
}
@Override
@SuppressWarnings("unchecked")
public Set<String> getAudience() {
Object aud = get(AUDIENCE);
if (aud instanceof Set) {
return (Set<String>) aud;
}
if (aud instanceof String) {
return Set.of(((String) aud).split(","));
}
return null;
}
@Override
public Date getExpiration() {
Object exp = get(EXPIRATION);
if (exp instanceof Date) {
return (Date) exp;
}
if (exp instanceof Number) {
return new Date(((Number) exp).longValue() * 1000);
}
return null;
}
@Override
public Date getNotBefore() {
return get(NOT_BEFORE, Date.class);
}
@Override
public Date getIssuedAt() {
return get(ISSUED_AT, Date.class);
}
@Override
public String getId() {
return get(ID, String.class);
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(String claimName, Class<T> requiredType) {
Object value = claimsMap.get(claimName);
if (value == null) {
return null;
}
if (requiredType.isInstance(value)) {
return (T) value;
}
// 处理数字类型转换(Jackson 可能将整数解析为 Integer 而非 Long)
if (requiredType == Long.class && value instanceof Integer) {
return (T) Long.valueOf(((Integer) value).longValue());
}
if (requiredType == Long.class && value instanceof Number) {
return (T) Long.valueOf(((Number) value).longValue());
}
if (requiredType == String.class) {
return (T) value.toString();
}
if (requiredType == Date.class && value instanceof Number) {
return (T) new Date(((Number) value).longValue() * 1000);
}
throw new RequiredTypeException("无法将 claim '" + claimName
+ "' 转换为 " + requiredType.getName() + ",实际类型: " + value.getClass().getName());
}
@Override
public int size() {
return claimsMap.size();
}
@Override
public boolean isEmpty() {
return claimsMap.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return claimsMap.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return claimsMap.containsValue(value);
}
@Override
public Object get(Object key) {
return claimsMap.get(key);
}
@Override
public Object put(String key, Object value) {
return claimsMap.put(key, value);
}
@Override
public Object remove(Object key) {
return claimsMap.remove(key);
}
@Override
public void putAll(Map<? extends String, ?> m) {
claimsMap.putAll(m);
}
@Override
public void clear() {
claimsMap.clear();
}
@Override
public Set<String> keySet() {
return claimsMap.keySet();
}
@Override
public java.util.Collection<Object> values() {
return claimsMap.values();
}
@Override
public Set<Entry<String, Object>> entrySet() {
return claimsMap.entrySet();
}
}
package com.jomalls.custom.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Token 兼容性解析工具
*
* <p>custom-back 使用 JJWT 0.9.1 生成 token,密钥推导方式为:
* <pre>
* secret = "custom"
* signingKey = TextCodec.BASE64.encode(secret) → "Y3VzdG9t"
* signWith(HS512, signingKey.getBytes(UTF_8)) → 8字节短密钥
* </pre>
*
* <p>JJWT 0.12.x 强制 HS512 密钥 ≥ 64 字节,无法直接解析旧 token。
* 本工具通过手动验证 HMAC-SHA512 签名 + 手动解码 payload 来兼容旧格式。
*
* @author Lizh
* @date 2026-06-03
*/
@Slf4j
@Component
public class TokenCompatibilityParser {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* 兼容性解析 token(支持 JJWT 0.9.1 旧格式)
*
* @param token JWT token
* @param secret 原始密钥(如 "custom")
* @return Claims
* @throws JwtException 解析失败
*/
public Claims parseTokenCompatibly(String token, String secret) throws JwtException {
// 方式1:兼容旧版本 JJWT 0.9.1 短密钥 token
Claims result = tryLegacyParse(token, secret);
if (result != null) {
return result;
}
// 方式2:优先尝试 JJWT 0.12.x 标准方式(适用于新版长密钥生成的 token)
result = tryStandardParse(token, secret);
if (result != null) {
return result;
}
throw new JwtException("无法解析 token,签名密钥不匹配或 token 格式无效");
}
/**
* JJWT 0.12.x 标准方式:secret 直接作为原始密钥字节
*/
private Claims tryStandardParse(String token, String secret) {
try {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (JwtException e) {
log.debug("标准方式解析失败: {}", e.getMessage());
}
return null;
}
/**
* 兼容 JJWT 0.9.1 旧格式 token
*
* <p>custom-back 密钥推导: signingKey = Base64(secret.getBytes(UTF_8))
* <p>因为密钥只有 8 字节,JJWT 0.12.x 拒绝,所以手工验证签名后解码 payload
*/
private Claims tryLegacyParse(String token, String secret) {
try {
// 1. 按 JJWT 0.9.1 方式推导签名密钥
byte[] signingKey = deriveLegacySigningKey(secret);
// 2. 手动验证 HMAC-SHA512 签名
if (!verifyHmacSha512Signature(token, signingKey)) {
log.debug("旧版 token 签名验证失败");
return null;
}
// 3. 签名有效,手动解码 payload 为 Claims
return decodePayloadToClaims(token);
} catch (Exception e) {
log.debug("旧版兼容解析失败: {}", e.getMessage());
return null;
}
}
/**
* 按 JJWT 0.9.1 方式推导签名密钥
*
* <p>custom-back 调用链:
* <pre>
* signWith(HS512, TextCodec.BASE64.encode(secret))
* → Base64.encode("custom") → "Y3VzdG9t"
* → signWith 内部又 Base64.decode("Y3VzdG9t") → "custom" 字节
* → encode + decode = 恒等变换,实际密钥 = secret.getBytes(UTF_8)
* </pre>
*
* <p>同理 setSigningKey(TextCodec.BASE64.encode(secret)) 内部也是
* Base64.decode → secret 原始字节。
* 所以签名密钥始终是原始 secret 的 UTF-8 字节。
*
* @param secret 原始密钥字符串,如 "custom"
* @return 签名密钥字节 (= secret.getBytes(UTF_8))
*/
static byte[] deriveLegacySigningKey(String secret) {
return secret.getBytes(StandardCharsets.UTF_8);
}
/**
* 手动验证 HMAC-SHA512 签名
*
* @param token JWT token (header.payload.signature)
* @param keyBytes 签名密钥字节
* @return true=签名有效
*/
static boolean verifyHmacSha512Signature(String token, byte[] keyBytes) {
try {
String[] parts = token.split("\\.");
if (parts.length < 3) {
return false;
}
String headerPayload = parts[0] + "." + parts[1];
byte[] signatureBytes = Base64.getUrlDecoder().decode(parts[2]);
Mac mac = Mac.getInstance("HmacSHA512");
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA512");
mac.init(keySpec);
byte[] computedSignature = mac.doFinal(
headerPayload.getBytes(StandardCharsets.UTF_8));
return MessageDigest.isEqual(computedSignature, signatureBytes);
} catch (Exception e) {
log.debug("HMAC-SHA512 签名验证异常: {}", e.getMessage());
return false;
}
}
/**
* 手动解码 JWT payload 为 JJWT Claims
*/
@SuppressWarnings("unchecked")
private static Claims decodePayloadToClaims(String token) throws Exception {
String[] parts = token.split("\\.");
String payloadJson = new String(
Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
Map<String, Object> claimsMap = OBJECT_MAPPER.readValue(payloadJson, LinkedHashMap.class);
return new JwtClaimsAdapter(claimsMap);
}
}
......@@ -59,11 +59,6 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
......
package com.jomalls.custom.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisInterceptor {
}
\ No newline at end of file
......@@ -17,9 +17,23 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(securityInterceptor())
.excludePathPatterns("/swagger-ui/**", "/swagger-ui.html", "/doc.html","/*/api-docs/**", "/document.html",
"/webjars/**", "/swagger-resources/**", "/sys/Serf/Health/*", "/error",
"/actuator/health", "/health/check");
.excludePathPatterns("/swagger-ui/**",
"/swagger-ui.html",
"/doc.html",
"/document.html",
"/v3/api-docs/**",
"/v3/api-docs",
"/api-docs/**",
"/api-docs",
"/swagger-resources/**",
"/webjars/**",
"/sys/Serf/Health/*",
"/error",
"/actuator/health",
"/health/check",
"/.well-known/**",
"/favicon.ico",
"/static/**");
}
/**
......
## Redis连接配置
spring.data.redis.host=172.16.19.99
spring.data.redis.host=172.16.19.100
spring.data.redis.port=6379
spring.data.redis.password=joshine.dev
spring.data.redis.database=7
......
......@@ -36,7 +36,7 @@ TZ=Asia/Shanghai
server.needAuthentication=true
# 令牌自定义标识
token.header=Authorization
# 令牌密钥
# 令牌密钥(兼容旧版本)
token.secret=custom
# 令牌有效期(默认30分钟)
token.expireTime=720
\ No newline at end of file
......@@ -51,18 +51,13 @@
<version>3.0.0</version>
</dependency>
<!-- SpringDoc OpenAPI for Spring Boot 4.x -->
<dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version>
<version>3.0.3</version>
</dependency>
<!-- Orika Bean Mapper -->
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment