一文读懂JWT+JAVA的两种实现方式
发布日期:2021-06-29 21:37:45 浏览次数:2 分类:技术文章

本文共 9843 字,大约阅读时间需要 32 分钟。

在上文中,我们已经了解到了Token,本文中,我们将详细介绍JWT并进行实现。

文章目录

一、JWT概述

  Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

二、JWT组成

  第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 存放用户信息地方,不存放如密码等敏感信息),第三部分是签证(signature)。下面我们分别讨论这三部分。

2.1 Header

JWT头部承载两个部分信息:

声明类型,这里是jwt声明加密的算法 通常直接使用 HMAC SHA256对应着下面的JSON串:{
'typ': 'JWT', 'alg': 'HS256'}

头部信息我们一般不做修改,保持原装就好。头部信息进行Base64编码(Base64并不是一种加密算法,它只是一种编码方式 ) 之后就构成了JWT的第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2.2 Payload

载荷就是存放有效信息的地方。这些有效信息包含三个部分:

  • 标准定义的声明
  • 公共的声明
  • 私有的声明

标准定义的声明(不强制使用),最常使用的就是expjti

iss: jwt签发者。
sub: jwt所面向的用户。
aud: 接收jwt的一方。
exp: jwt的过期时间,这个过期时间必须要大于签发时间。
nbf: 定义在什么时间之前,该jwt都是不可用的。
iat: jwt的签发时间。
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明

  公共的声明可以添加任何的信息,一般添加用户的相关信息其他业务需要的必要信息不建议添加敏感信息,因为该部分在客户端可解密
私有的声明
  私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
一个playload可以定义为如下:

{
"sub": "1", "iss": "http://localhost:8000/auth/login", "iat": 1451888119, "exp": 1454516119, "nbf": 1451888119, "jti": "37c107e4609ddbcc9c096ea5ee76c667", "username": "tonghua", "userid": 12355486}

  上面简单的payload中我们存放用户的信息如userid和username进载荷中。同样,我们将其进行Base64编码(非常不建议将密码等敏感信息存放在这里,因为能被客户端解码得到信息) 后成为JWT的第二部分。

eyJleHAiOjE2MDYxOTk5NTEsInVzZXJJZCI6IjUxMjAxNTQyMzAiLCJ1c2VybmFtZSI6ImRpbmdsaSJ9

2.3 Signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

header (Base64后的)
payload (Base64后的)
secret

  这个部分需要Base64编码后的HeaderBase64编码后的Payload使用 .连接组成的字符串,然后通过Header中声明的加密方式进行加盐(secret)加密,然后就构成了JWT的第三部分。

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

最终的JWT的第三部分Signature如下:

wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4

然后,我们将JWT三部分用 .按照顺序连接起来构成完整的JWT:Header.Payload.Signature

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDYxOTk5NTEsInVzZXJJZCI6IjUxMjAxNTQyMzAiLCJ1c2VybmFtZSI6ImRpbmdsaSJ9.wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4

  注意:secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。

例如一个secret如下所示,这个就是和MD5加密中的加盐操作是一样的:

secret="csdntonghuasdfni#2323!@seirninfsasdf"

三、JWT的应用

3.1 在请求地址中添加token并验证

  CSRF攻击之所以能够成功,是因为攻击者可以伪造用户的请求,该请求中所有的用户验证信息都存在于Cookie中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的Cookie来通过安全验证。由此可知,抵御CSRF攻击的关键在于:在请求中放入攻击者所不能伪造的信息,并且该信息不存在于Cookie之中。鉴于此,系统开发者可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为可能是CSRF攻击而拒绝该请求。

3.2 在HTTP头中自定义属性并验证

  自定义属性的方法也是使用token并进行验证,和前一种方法不同的是,这里并不是把token以参数的形式置于HTTP请求之中,而是把它放到HTTP头中自定义的属性里。通过XMLHttpRequest这个类,可以一次性给所有该类请求加上csrftoken这个HTTP头属性,并把token值放入其中。这样解决了前一种方法在请求中加入token的不便,同时,通过这个类请求的地址不会被记录到浏览器的地址栏,也不用担心token会通过Referer泄露到其他网站。

  当然也不乏有将token添加到cookie中的,这种做法是非常不建议的。因为这可以被别人获取。

四、JWT的实现

  JWT的实现包有很多,本文将采用主流的包java-jwtjjwt实现。为方便将对象信息转换为JSON字符串,我使用了alibaba的开源fastjson

JJWT的github地址为:
依赖如下(直接在中搜索即可获取不同版本的依赖):

com.auth0
java-jwt
3.11.0
io.jsonwebtoken
jjwt
0.9.0
com.alibaba
fastjson
1.2.28

4.1 java-jwt实现

import com.auth0.jwt.JWT;import com.auth0.jwt.JWTCreator;import com.auth0.jwt.algorithms.Algorithm;import com.auth0.jwt.interfaces.DecodedJWT;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.util.Calendar;import java.util.Map;public class JavaJWTUtil {
private static String secret="csdntonghuasdfni#2323!@seirninfsasdf";//这个secret我们一般写在配置文件或者存放常量的类中,这里为了方便直接写在这儿 /** * 生成token header.payload.signature * @param map :payload中需要存放的信息,以map方式传入 * @param day :过期时间,以秒为单位 * @return */ public static String getToken(Map
map,Integer day){
Calendar instance=Calendar.getInstance(); instance.add(Calendar.SECOND,day); //创建jwt builder JWTCreator.Builder builder=JWT.create(); //payload,这里采用lambda表达式设置 map.forEach((k,v)->{
builder.withClaim(k,v); }); String token=builder.withExpiresAt(instance.getTime())//指定令牌过期时间 .sign(Algorithm.HMAC256(secret)); return token; } /** * 验证token合法性,不合法会抛出异常信息 * @param token : 前端传来的token * @return */ public static DecodedJWT verify(String token){
//如果有任何验证异常,此处都会抛出异常,因此我们可以捕获这些异常来反馈信息回前端 DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secret)).build().verify(token); return decodedJWT; }}

下面我们采用单元测试Junit进行测试:

4.1.1、首先生成token

@Test    void tokenJavaJwt(){
//生成token Map
map=new HashMap<>();//存储载荷中的信息 map.put("userId","123456"); map.put("username","tonghua"); String token= JavaJWTUtil.getToken(map,100); System.out.println(token); }

4.1.2、将生成的token填入下面进行测试,未抛出异常则验证成功

@Test    void tokenVerify(){
//验证token try {
String token=""; DecodedJWT result=JavaJWTUtil.verify(token); System.out.println(result.getClaim("userId").asString()); System.out.println(result.getClaim("username").asString()); } catch (SignatureVerificationException e) {
e.printStackTrace(); System.out.println("签名不一致!"); } catch (TokenExpiredException e) {
e.printStackTrace(); System.out.println("令牌过期!"); } catch (AlgorithmMismatchException e) {
e.printStackTrace(); System.out.println("算法不匹配!"); } catch (InvalidClaimException e) {
e.printStackTrace(); System.out.println("失效的payload!"); } catch (Exception e) {
e.printStackTrace(); System.out.println("无效令牌!"); } }

4.2 采用JJWT实现:

import com.alibaba.fastjson.JSONObject;import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.apache.tomcat.util.codec.binary.Base64;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;import java.util.Date;import java.util.HashMap;import java.util.Map;public class JJWTUtil {
/** * 创建jwt * @param id * @param subject * @param ttlMillis 过期的时间长度 * @return * @throws Exception */ private static String stringKey="csdntonghuasdfni#2323!@seirninfsasdf";//加的盐,也就是这里的密钥,有这东西就可以解码 public String getToken(String id, String subject, long ttlMillis) throws Exception {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。 long nowMillis = System.currentTimeMillis();//生成JWT的时间 Date now = new Date(nowMillis); Map
claims = new HashMap
();//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) claims.put("uid", "DSSFAWDWADAS..."); claims.put("user_name", "admin"); claims.put("nick_name","DASDA121"); SecretKey key = generalKey();//生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。 //下面就是在为payload添加各种标准声明和私有声明了 JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body .setClaims(claims) //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setId(id) //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。 .setIssuedAt(now) //iat: jwt的签发时间 .setSubject(subject) //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,username之类的,作为用户的唯一标志。 .signWith(signatureAlgorithm, key);//设置签名使用的签名算法和签名使用的秘钥 if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); //设置过期时间 } return builder.compact(); //就开始压缩为header.payload.signature这样的jwt } /** * 解密jwt * @param jwt : token校验,如果抛出异常则解析失败,也就是说token验证不通过 * @return * @throws Exception */ public Claims parseJWT(String jwt) throws Exception{
SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样 Claims claims = Jwts.parser() //得到DefaultJwtParser .setSigningKey(key) //设置签名的秘钥 .parseClaimsJws(jwt).getBody();//设置需要解析的jwt return claims; } /** * 这里多对这个key加一次加密,由stringKey字符串生成加密的secret * @return */ public SecretKey generalKey(){
byte[] encodedKey = Base64.decodeBase64(stringKey);//本地的密码解码 SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");// 根据给定的字节数组使用AES加密算法构造一个密钥,使用 encodedKey中的始于且包含 0 到前 leng 个字节这是当然是所有。 //常用的对称加密算法有DES,3DES,AES三种,这三种加密算法对应的密钥长度分别是8位、24位以及32位 return key; } public static void main(String[] args) throws Exception {
JJWTUtil util=new JJWTUtil(); //生成的token Map
map=new HashMap<>();//存放如username、userid之类的载荷信息 map.put("username","tonghua"); map.put("userid",233443435); String jsonString= JSONObject.toJSONString(map);//将map转为json字符串 String token=util.getToken("jwt", jsonString, 60000);//时间单位毫秒 System.out.println(token); //验证token String jwt=token; Claims c=util.parseJWT(jwt);//如果jwt已经过期了,这里会抛出jwt过期异常。 System.out.println(c.getId());//jwt System.out.println(c.getIssuedAt()); System.out.println(c.getSubject()); System.out.println(c.get("uid", String.class)); System.out.println(c.get("username")); }}

JWT生成部分一般我们使用在登录信息验证成功之后,验证部分需要在其他请求数据接口中使用。为避免重复写代码,我们在下篇文章中结合SpringBoot和拦截器进行整合。

参考文章:

1、
2、

转载地址:https://dh-butterfly.blog.csdn.net/article/details/109991182 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【SpringBoot】拦截器处理JWT验证
下一篇:【JVM】一文了解双亲委派机制及其作用

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月25日 13时56分37秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章