如何使用 JJWT 从有效负载中获取自定义字段

Posted

技术标签:

【中文标题】如何使用 JJWT 从有效负载中获取自定义字段【英文标题】:How do I get a custom field out of the payload using JJWT 【发布时间】:2017-11-27 10:18:32 【问题描述】:

好的,我在生成 JWT 时向有效负载添加了几个自定义声明,我可以在前端 (javascript) 中很好地提取这些声明。然后我让我的 javascript 向一个微服务发送一个 ajax 调用,它会同时传递 JWT。我想从微服务中的 JWT 中获取我的自定义声明。我正在执行以下操作:

Claims claims = Jwts.parser().setSigningKey(Vars.SECRET_KEY).parseClaimsJws(token).getBody();
 User user = claims.get("customuser", User.class);

它会引发异常。

io.jsonwebtoken.RequiredTypeException: Expected value to be of type: class net.netdatacorp.netdauth.model.User, but was class java.util.LinkedHashMap
    at io.jsonwebtoken.impl.DefaultClaims.get(DefaultClaims.java:128)

这是我的自定义声明在前端的 JWT 检查器中的数据显示方式。


  jti: "83bffbad-7d36-4370-9332-21a84f2a3dce",
  iat: 1498241526,
  sub: "test",
  iss: "www.test.net",
  customuser: 
    userId: 1,
    userCd: "TMM",
    firstNm: "Testy",
    lastNm: "McTesty",
    userNm: "test",
    emailAddress: "jacob@test.net",
    active: true,
    createdDt: 1491355712000,
    createdByUserId: 0,
    lastUpdateDt: 1498199278000,
    lastUpdateByUserId: 0,
    lastLoginDt: 1484928016000
  

为了能够提取我的自定义声明,我缺少什么?

【问题讨论】:

我看了一下,看到提供的JwtParser 的唯一实现是DefaultJwtParser,它依赖于杰克逊的ObjectMapper,但遗憾的是没有公开它以允许自定义。甚至不能通过继承:方法 readValueprotected 但只会为对象返回 Map (see here in 0.7.0)。看来您需要自己从头开始实现JwtParser...或手动将地图转换为您的对象:-/ 在那种情况下,我好像要切换库了,看起来 jose4j 可以处理我想做的事情。 @JacobMiles,您介意发布jose4j 代码进行比较吗? 【参考方案1】:

好的,所以我转而使用 Jose4J 而不是 JJWT,在努力让每件事都正常工作之后,我意识到我可能可以用 JJWT 做类似的事情。所以我最终做的是使用 Gson 对 Object 执行 JSON 编码,并将生成的 JSON 字符串作为声明附加。因此,当我想恢复自定义声明时,我会将声明提取为字符串并使用 Gson 库将其转换回 POJO。

GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();

JwtConsumer jwtConsumer = getConsumer();

JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
String userStr = jwtClaims.getClaimValue("user", String.class);
User user = gson.fromJson(userStr, User.class);

【讨论】:

什么是getConsumer()方法?【参考方案2】:

我们可以使用 Jackson 的对象映射器将 Claims(Map<String, Object>)转换为我们自定义的 Claim java 对象。

final ObjectMapper mapper = new ObjectMapper();

Claims jwsMap = Jwts.parser()
       .setSigningKey("SECRET")
       .parseClaimsJws("jwt")
       .getBody();
return mapper.convertValue(jwsMap, MyCustomClaim.class);

还添加该代码以尝试捕获以确保我们处理丢失/篡改签名的情况。

【讨论】:

请补充一些解释。【参考方案3】:

JJWT 自 0.11.0 版本以来就具有此功能。

这个想法是 JWT 库本身不应承担编组行为,因为 1) 能够处理任何临时数据结构(参见 JAXB 和 Jackson 代码库作为示例)和 2) JSON 编组器确实是一项复杂的工作已经可以做到了——JJWT 重新发明那个***没有意义。

因此,为了利用编组器的内置支持,我们需要告诉它应该将哪些字段解组为自定义对象,以便它可以在解析时执行此操作。 (当 JSON 被完全解析时,JJWT 开始查看 JWT Map 时已经“为时已晚”,因此我们需要确保 marshaller 可以在解析时完成。

您可以通过告诉编组器应将哪些字段转换为自定义类型(例如,使用 Jackson)来做到这一点:

Jwts.parserBuilder()

    .deserializeJsonWith(new JacksonDeserializer(Maps.of("user", User.class).build())) // <-----

    .build()

    .parseClaimsJwt(aJwtString)

    .getBody()
    
    .get("user", User.class) // <-----

有关更多信息,请参阅 JJWT 的文档https://github.com/jwtk/jjwt#parsing-of-custom-claim-types

【讨论】:

【参考方案4】:

我知道您的主要目标是Customer 对象。其他数据已经存在于索赔的对象中。您可以像这样轻松管理自己的对象。

@Data //insted of this annotation, you can generate the getters and setters
@JsonIgnoreProperties(ignoreUnknown = true)
public class Customer 
    private Integer userId;
    private String userCd;
    private String firstNm;
    ........

JsonIgnoreProperties注解在从token体转换为对象时非常重要。它忽略了对象没有的其他属性。 (Jti,Lat,Iss)

现在你有了你想要的对象。让我们生成令牌。

Map<String, Object> claims = new HashMap<>(); //create a hashmap
Customer customer= new Customer(); //create your object

//assign the initial customer data
customer.setUserId(1);
customer.setUserCd("TMM");
customer.setFirstNm("Testy");

ObjectMapper oMapper = new ObjectMapper(); //create a objectmapper object
Map<String, Object> customerData = oMapper.convertValue(customer, Map.class); //convert the customer object into map of (String, Object)
claims.putAll(customerData ); //put all the customer data into claims map


//create the token using another required data
String token = Jwts.builder()
                .setClaims(claims) //this our object
                .setSubject("test")
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, "secret")
                .compact();


去https://jwt.io/把生成的token放进去看看怎么样。会是这样的。


  "sub": "test",
  "firstNm": "Testy", //customer data from the object
  "exp": 1622862855,
  "userId": 1, //customer data from the object
  "iat": 1622844855,
  "userCd": "TMM" //customer data from the object,
  ........

它还包含所有数据以及您的自定义客户数据。

现在让我们解码令牌

Jws<Claims> claimsJws = Jwts.parser().setSigningKey("secret").parseClaimsJws(token);
ObjectMapper mapper = new ObjectMapper();
Customer customer = mapper.convertValue(claimsJws.getBody(), Customer.class); //convert the claims body by mentioning the customer object class
System.out.println("customerData = " + customer);

现在您可以根据需要使用客户数据对象。 **特别的是@JsonIgnoreProperties(ignoreUnknown = true)注解。

【讨论】:

【参考方案5】:

向 JWT 添加自定义声明。

注意:我在 Spring Security 中使用过这个

保留声明

iss – 发行人 sub – 主题 aud – 观众 exp – 过期 nbf – 不是之前 iat – 发布于 jti – JWT ID

添加自定义声明

String token = Jwts.builder()
.setSubject(subject)
.setExpiration(expDate)
.claim("userId", "3232")
.claim("UserRole", "Admin")
.signWith(SignatureAlgorithm.HS512, secret )
.compact();

检索自定义声明

Claims claims = Jwts.parser()         
   .setSigningKey(tokenSecret)
   .parseClaimsJws(jwt).getBody();
 
// Reading Reserved Claims
System.out.println("Subject: " + claims.getSubject());
System.out.println("Expiration: " + claims.getExpiration());

// Reading Custom Claims
System.out.println("userId: " + claims.get("userId"));
System.out.println("userRole: " + claims.get("userRole"));

【讨论】:

以上是关于如何使用 JJWT 从有效负载中获取自定义字段的主要内容,如果未能解决你的问题,请参考以下文章

带有自定义 iOS 有效负载的 Amazon Simple Notification Service 并不那么简单

如何使用完全自定义的有效负载制作 JWT

Spring JWT - 添加自定义声明

通过JJWT解密JSON字符串删除引号-Java

如何从 WordPress 数据库中获取高级自定义字段字段键?

如何在 pubsub 节点上发送/接收自定义项目有效负载