Commit 22ab4ed1 by Lizh

修改token解析失败问题

parent d3b47235
...@@ -39,6 +39,10 @@ ...@@ -39,6 +39,10 @@
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
......
package com.jomalls.custom.page; package com.jomalls.custom.page;
import lombok.Data; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
/** /**
* @Author: Lizh * @Author: Lizh
...@@ -34,11 +33,13 @@ public class PageRequest implements Pageable, Serializable { ...@@ -34,11 +33,13 @@ public class PageRequest implements Pageable, Serializable {
/** /**
* 当前页码 * 当前页码
*/ */
@Schema(description = "当前页码", example = "1", defaultValue = "1")
private long current; private long current;
/** /**
* 分页大小 * 分页大小
*/ */
@Schema(description = "分页大小", example = "10", defaultValue = "10")
private long size; 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);
}
}
package com.jomalls.custom.security; package com.jomalls.custom.security;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* Token验证处理服务 * Token 验证处理服务
*
* <p>兼容两种 token 格式获取用户信息:
* <ol>
* <li><b>custom-back 格式(优先)</b>:JWT 中仅含 login_user_key (UUID),
* 用户全量信息(userId, deptId, username, permissions 等)存储在
* Redis {@code login_tokens:{uuid}} 中,由 custom-back 登录时写入</li>
* <li><b>custom-server 原生格式(回退)</b>:用户信息直接嵌入 JWT claims</li>
* </ol>
*
* @author Lizh
*/ */
@Slf4j @Slf4j
@Component @Component
public class TokenHandle { public class TokenHandle {
// 令牌自定义标识 /** JWT claims 中的 UUID 键名 */
private static final String LOGIN_USER_KEY = "login_user_key";
/** Redis key 前缀 */
private static final String REDIS_TOKEN_PREFIX = "login_tokens:";
@Autowired
private TokenCompatibilityParser tokenCompatibilityParser;
@Autowired(required = false)
private StringRedisTemplate stringRedisTemplate;
@Value("${token.header:Authorization}") @Value("${token.header:Authorization}")
private String header; private String header;
// 令牌秘钥
@Value("${token.secret:custom}") @Value("${token.secret:custom}")
private String secret; private String secret;
private static final String BEARER = "bearer ";
/** /**
* 获取用户身份信息 * 获取用户身份信息
* *
* <p>查询优先级:
* <ol>
* <li>JWT 中有 login_user_key → Redis GET login_tokens:{uuid}
* → 解析 JSON 获取 userId, deptId, username, permissions 全量信息</li>
* <li>Redis 不可用或 key 不存在 → 回退到 JWT claims 直接提取</li>
* </ol>
*
* @param request 请求对象 * @param request 请求对象
* @return 用户信息 * @return 用户信息,解析失败返回 null
*/ */
public LoginUser getLoginUser(HttpServletRequest request) { public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request); String token = getToken(request);
// 获取token失败, 返回null
if (!StringUtils.hasText(token)) { if (!StringUtils.hasText(token)) {
return null; return null;
} }
try { try {
Claims claims = parseToken(token); Claims claims = parseToken(token);
// 解析用户信息
Long userId = claims.get("id", Long.class);
String username = claims.get("account", String.class);
if (username == null) {
username = claims.get("username", String.class);
}
if (userId != null && StringUtils.hasText(username)) {
LoginUser loginUser = new LoginUser(userId, null, username);
loginUser.setToken(token);
loginUser.setExpireTime(claims.getExpiration().getTime());
// 解析权限信息
Set<String> permissions = parsePermissions(claims);
loginUser.setPermissions(permissions);
// 优先走 custom-back 路径:Redis 获取全量用户信息
LoginUser loginUser = tryGetFromRedis(claims);
if (loginUser != null) {
loginUser.setToken(token);
return loginUser; return loginUser;
} }
// 回退:从 JWT claims 直接提取
loginUser = buildFromClaims(claims);
if (loginUser != null) {
loginUser.setToken(token);
}
return loginUser;
} catch (JwtException e) { } catch (JwtException e) {
log.error("JWT解析异常: {}", e.getMessage()); log.error("JWT解析异常: {}", e.getMessage());
} catch (Exception e) { } catch (Exception e) {
...@@ -72,68 +97,146 @@ public class TokenHandle { ...@@ -72,68 +97,146 @@ public class TokenHandle {
} }
/** /**
* 解析权限信息 * 从 Redis 获取用户全量信息(custom-back 兼容路径)
* *
* @param claims JWT claims * <p>custom-back 登录时将 LoginUser 序列化为 JSON 存入 Redis,
* @return 权限集合 * JWT 中只放 UUID。此处按相同路径取出。
*/
private LoginUser tryGetFromRedis(Claims claims) {
if (stringRedisTemplate == null) {
return null;
}
String uuid = claims.get(LOGIN_USER_KEY, String.class);
if (!StringUtils.hasText(uuid)) {
return null;
}
try {
String jsonValue = stringRedisTemplate.opsForValue().get(REDIS_TOKEN_PREFIX + uuid);
if (!StringUtils.hasText(jsonValue)) {
log.debug("Redis key [{}:{}] 不存在或已过期", REDIS_TOKEN_PREFIX, uuid);
return null;
}
// custom-back 使用 fastjson2 序列化(含 @type),用 fastjson2 解析
JSONObject data = JSON.parseObject(jsonValue);
Long userId = toLong(data.get("userId"));
Long deptId = toLong(data.get("deptId"));
String username = (String) data.get("username");
if (userId == null || !StringUtils.hasText(username)) {
log.warn("Redis 中用户数据不完整: userId={}, username={}", userId, username);
return null;
}
LoginUser loginUser = new LoginUser(userId, deptId, username);
// 过期时间
Object expireObj = data.get("expireTime");
if (expireObj instanceof Number) {
loginUser.setExpireTime(((Number) expireObj).longValue());
}
// 权限列表
@SuppressWarnings("unchecked")
HashSet<String> permissionsList = (HashSet<String>) data.get("permissions");
if (permissionsList != null) {
loginUser.setPermissions(new HashSet<>(permissionsList));
} else {
loginUser.setPermissions(new HashSet<>());
}
log.debug("从 Redis 获取用户[{}]信息成功,权限数: {}", username, permissionsList != null ? permissionsList.size() : 0);
return loginUser;
} catch (Exception e) {
log.warn("从 Redis 获取用户信息失败: {}", e.getMessage());
return null;
}
}
/**
* 从 JWT claims 直接构建 LoginUser(custom-server 原生路径,回退方案)
*/
private LoginUser buildFromClaims(Claims claims) {
Long userId = claims.get("id", Long.class);
String username = claims.get("account", String.class);
if (username == null) {
username = claims.get("username", String.class);
}
if (userId == null || !StringUtils.hasText(username)) {
log.warn("JWT claims 缺少 id 或 account/username");
return null;
}
Long deptId = claims.get("deptId", Long.class);
LoginUser loginUser = new LoginUser(userId, deptId, username);
loginUser.setExpireTime(claims.getExpiration().getTime());
loginUser.setPermissions(parsePermissionsFromClaims(claims));
log.debug("从 JWT claims 构建用户[{}]信息,权限数: {}", username, loginUser.getPermissions().size());
return loginUser;
}
/**
* 从 JWT claims 中解析权限列表
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Set<String> parsePermissions(Claims claims) { private Set<String> parsePermissionsFromClaims(Claims claims) {
Set<String> permissions = new HashSet<>(); Set<String> permissions = new HashSet<>();
// 从 claims 中解析权限列表
Object permissionsObj = claims.get("permissions"); Object permissionsObj = claims.get("permissions");
if (permissionsObj instanceof List) { if (permissionsObj instanceof List) {
List<String> permsList = (List<String>) permissionsObj; permissions.addAll((List<String>) permissionsObj);
permissions.addAll(permsList);
} else if (permissionsObj instanceof String) { } else if (permissionsObj instanceof String) {
// 如果是逗号分隔的字符串 for (String perm : ((String) permissionsObj).split(",")) {
String[] permsArray = ((String) permissionsObj).split(",");
for (String perm : permsArray) {
if (StringUtils.hasText(perm)) { if (StringUtils.hasText(perm)) {
permissions.add(perm.trim()); permissions.add(perm.trim());
} }
} }
} }
return permissions; return permissions;
} }
/** /**
* 安全转换 Object → Long
*/
private static Long toLong(Object value) {
if (value instanceof Number) {
return ((Number) value).longValue();
}
if (value instanceof String && StringUtils.hasText((String) value)) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException ignored) {}
}
return null;
}
/**
* 从请求头获取令牌 * 从请求头获取令牌
*
* @param request 请求对象
* @return 令牌字符串
*/ */
private String getToken(HttpServletRequest request) { private String getToken(HttpServletRequest request) {
String token = request.getHeader(header); String token = request.getHeader(header);
// 如果令牌以 Bearer 开头,去掉前缀 if (StringUtils.hasText(token) && token.toLowerCase().startsWith(BEARER)) {
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { return token.substring(BEARER.length());
token = token.replace("Bearer ", "");
} }
return token; return token;
} }
/** /**
* 解析令牌 (JJWT 0.12.x API) * 解析令牌(兼容新旧 JJWT 版本)
*
* @param token 令牌
* @return 声明
*/ */
private Claims parseToken(String token) { private Claims parseToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); return tokenCompatibilityParser.parseTokenCompatibly(token, secret);
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
} }
/** /**
* 验证令牌有效期 * 验证令牌有效期
*
* @param loginUser 登录信息
* @return 是否有效
*/ */
public boolean verifyToken(LoginUser loginUser) { public boolean verifyToken(LoginUser loginUser) {
if (loginUser == null || loginUser.getExpireTime() == null) { if (loginUser == null || loginUser.getExpireTime() == null) {
......
...@@ -59,11 +59,6 @@ ...@@ -59,11 +59,6 @@
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<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> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>provided</scope> <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 { ...@@ -17,9 +17,23 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(securityInterceptor()) registry.addInterceptor(securityInterceptor())
.excludePathPatterns("/swagger-ui/**", "/swagger-ui.html", "/doc.html","/*/api-docs/**", "/document.html", .excludePathPatterns("/swagger-ui/**",
"/webjars/**", "/swagger-resources/**", "/sys/Serf/Health/*", "/error", "/swagger-ui.html",
"/actuator/health", "/health/check"); "/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连接配置 ## 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.port=6379
spring.data.redis.password=joshine.dev spring.data.redis.password=joshine.dev
spring.data.redis.database=7 spring.data.redis.database=7
......
...@@ -36,7 +36,7 @@ TZ=Asia/Shanghai ...@@ -36,7 +36,7 @@ TZ=Asia/Shanghai
server.needAuthentication=true server.needAuthentication=true
# 令牌自定义标识 # 令牌自定义标识
token.header=Authorization token.header=Authorization
# 令牌密钥 # 令牌密钥(兼容旧版本)
token.secret=custom token.secret=custom
# 令牌有效期(默认30分钟) # 令牌有效期(默认30分钟)
token.expireTime=720 token.expireTime=720
\ No newline at end of file
...@@ -51,18 +51,13 @@ ...@@ -51,18 +51,13 @@
<version>3.0.0</version> <version>3.0.0</version>
</dependency> </dependency>
<!-- SpringDoc OpenAPI for Spring Boot 4.x --> <!-- SpringDoc OpenAPI for Spring Boot 4.x -->
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.7.0</version> <version>3.0.3</version>
</dependency> </dependency>
<!-- Orika Bean Mapper --> <!-- Orika Bean Mapper -->
<dependency> <dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
</dependency> </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