JWT实现token令牌中心

 

简介

JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;

官网:https://jwt.io

GitHub上jwt的java客户端:https://github.com/jwtk/jjwt

 

 

在进行JWT介绍之前,我们先看看下面一个场景

现在有一个接口, http://www.xxxx.com/user/account?userId = 1 , 它的功能是获取用户当前账户信息,比如用户名、余额等等,这些属于用户的隐私信息,不能随意被别人获取,在不做任何限制的情况下,我只要知道接口地址,并且改变请求参数,比如“userId=2”、“userId=3”,我就可以获取到任意用户的信息,这显示不是我们想要的。

所以服务端要对请求的人进行身份认证,知道你是谁,确保你有权限访问数据

 

解决方案1:标识ID + secretKey

实现方法大体是,服务端提前给用户分配一个id 和对应密钥,比如 id =100、secretKey=HD38Jab15R,客户端和服务端都要保存。

客户端请求时,将secretKey和请求参数拼接,得到:  userId=100&arg1=xx&arg2=yyy&secretKey=HD38Jab15R

再将上面的字符串进行MD5计算:  MD5( userId=100&arg1=xx&arg2=yyy&secretKey=HD38Jab15R )  = 2b405bcf0f21eb6ea4ade30b75f7b709

得到签名:2b405bcf0f21eb6ea4ade30b75f7b709

最后将请求参数和签名进行组合请求(不要传送secretKey)

http://www.xxxx.com/user/account?userId = 100 &arg1=xx&arg2=yyy & sign = 2b405bcf0f21eb6ea4ade30b75f7b709

 

服务端收到请求后,把保存的userId=100对应的密钥取出(secretKey=HD38Jab15R),和客户端做相同的运算步骤,也得到一个签名B,签名B和用户传送过来的sign参数做对比,如果相同身份认证成功,代表请求者的确是userId=100,可以返回它的数据。如果请求方生成sign之后,再请求时任何一个参数变化了,那么最后服务端计算的的MD5值和传过来的sign一定不相等,认为身份认证不通过。

这种方法, 需要客户端和服务端都保存 ID + secretKey,每次请求客户端和服务端都要进行计算,无需用户名与密码,无需登录,适用于客户端也是一台服务器的时候(或者有条件并适合保存ID + secretKey的终端),假如客户端是一个游览器,就不能用此方法。

 

解决方案2:cookie或session

传统的cookie和session实现方法本文就不详细介绍了,基本思路就是用户在游览器上通过userName和password进行登录,登录后的用户信息维护在cookie或session里,在请求接口时检查cookie或session里的值来判断身份。此方法只适用于单服务器。

 

多服务器中session不共享问题

假如有A、B、C多台服务器时, 由于每台服务器session是在各自的内存中相互隔离的,用户在A服务器上登录后,A保存了用户的session,但是B 和 C并没有保存session,需要重新登录,退出时也要在ABC服务器上都退出。

 

多服务器中cookie跨域问题

比如说,我们请求<https://www.csdn.com/>时,浏览器会自动把csdn.com的Cookie带过去给csdn的服务器,而不会把<https://www.baidu.com/>的Cookie带过去给csdn的服务器。

 

总结:现在前后端分离、微服务、分布式系统普及的情况下,这种传统的cookie或session做法显然已不适用。

 

解决方案3:token认证

客户端使用用户名跟密码请求登录,服务端收到请求,去验证用户名与密码;

验证成功后,服务端会根据一定规则(比如包含用户标识id)生成一个 Token,再把这个 Token 发送给客户端

客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里

客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
 

 

个人认为token身份认证是一个技术规范,JWT是其中一种实现方式,也可以自行实现token+redis存储。

 

 

JWT的构成

一个jwt字符串如下,由3段组成: header.playload.sign

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8

header(头部)

头部用于描述关于该JWT的最基本的信息,最重要的一个参数是"alg",它代表签名时的算法,默认是HS256(HMAC-SHA256),点击查看其他算法

header的明文是一个json字符串,比如: {"alg":"HS256"}

header的base64值是 : eyJhbGciOiJIUzI1NiJ9

 

playload(载荷)

载荷就是存放有效信息的地方,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

Standard Claims(标准声明)

JWT指定七个默认字段供选择,不是必填项

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

Custom Claims(自定义声明)

一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

playload的明文,比如: {"jti":"100","iat":1591172930,"exp":1591176530}

它的base64值是:eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0

 

 

sign(签名)

服务端会保存一个密钥secretKey,将上面的  header+“.”+playload 拼接好后,配合secretKey使用HS256进行计算,

得到签名sign,在一起组装成  header.playload.sign 的样子,将它返回给用户,最终token如下

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8

客户端保存这个好这个token,在每一次请求的时候带上,服务端收到token时会把传过来的header.playload 和 自己保存secretKey计算后,得到新的sign2,再和客户端传过来的sign做一次对比。

 

 

 

举个栗子

工具类JJWT(Java Json Web Token),导入maven依赖,参考文档 https://github.com/jwtk/jjwt

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-api</artifactId>
      <version>0.11.1</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-impl</artifactId>
      <version>0.11.1</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-jackson</artifactId> 
      <version>0.11.1</version>
      <scope>runtime</scope>
    </dependency>

 

 

生成密钥

    public static void main(String[] args){
        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);  // 呼叫JJWT帮我们生成一个密钥
        System.out.println("secretKey:"+ new String(Base64.getEncoder().encode(key.getEncoded()) ) );

    }

JJWT帮我生成的密钥是: k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw=

 

 

生成token

    public static void main(String[] args) {

        String userId = "100";     //在用户登录成功之后,我们假如得到了用户id
        String secretKey = "k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw=";   //刚才JJWT帮我生成的密钥
        SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));    //召唤JJWT帮我们封装成密钥对象
        Date now = new Date();
        Date exp = new Date(now.getTime() + 3600 * 1000);   //过期时间1小时
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("创建时间:" + df.format(now));
        System.out.println("过期时间:" + df.format(exp));

        //生成令牌
        String token = Jwts.builder().setId(userId)
                .setIssuedAt(now)
                .setExpiration(exp)
                .signWith(key)
                .compact();
        System.out.println(token);



    }

得到token : eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8

 

 

 

验证token

  public static void test(String[] args){

        try {
            String secretKey = "k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw=";   //刚才JJWT帮我生成的密钥,必须和加密时用同一个密钥
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));    //召唤JJWT帮我们封装成密钥对象
            String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8";
            Jws<Claims> jwt = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        }
        catch (JwtException e){
            e.printStackTrace();
        }

    }

如果验证失败,会抛出异常 io.jsonwebtoken.security.SignatureException 

如果过期,会抛出异常 io.jsonwebtoken.ExpiredJwtException

 

验证成功后,查看token中封装的数据

public static void test(String[] args){

        try {
            String secretKey = "k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw=";   //刚才JJWT帮我生成的密钥,必须和加密时用同一个密钥
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));    //召唤JJWT帮我们封装成密钥对象
            String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8";
            Jws<Claims> jwt = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);

            String userId = jwt.getBody().getId();    
            System.out.println(userId );    //看看发起请求的是谁

        }
        catch (JwtException e){
            e.printStackTrace();
        }

    }

 

 

 

总结JWT的特点

1、客户端从其中一个服务器拿到token后,部署多个服务器只要配置相同的secretKey就能成功解析,所以JWT实现了分布式的Web应用授权;

2、服务端生成token后直接返回给客户端了,服务端并不保存token(所以不依赖redis或者数据库),每一次客户端请求时服务端都是要通过secretKey重新计算来认证的,所以JWT是无状态的; 

3、由于客户端并不保存secretKey,所以客户端没有泄露secretKey的风险,变更secretKey时只要服务端自己变就行了;

4、由于服务端并不保存token,所以JWT无法对token进行一些业务操作,比如统计当前token数量,退出登录

5、JWT生成token时通常会指定有效期,而playload的任何字段一旦更改,校验就不会通过,所以无法通过修改有效期给token续签延期,到期后必须重新申请令牌

6、所有token实现方式都要面临“token泄露”的问题,初级的做法是设置token有效期不要太长(一般1-2小时,最长不超过1天),更好的方案楼主也在学习中;

 

针对4、5的问题,可以使用自实现token+redis的方法解决。

授权和身份认证是核心需求,它的解决方案有很多,根据项目复杂度和需求选择合适的解决方案才是最好的

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值