zoukankan      html  css  js  c++  java
  • JWT(Json Web Token)初探与实践


    协议标准:https://tools.ietf.org/html/rfc7519
    jwt.io:https://jwt.io
    开箱即用:https://jwt.io/#libraries

    前言

    最近网站后台迎来第三次改版,原来采用的是jquery+bootstrap这样常规的方式,但是随着网站的交互越来越多,信息量越来越大,就非常力不从心了,每次写动态交互都好痛苦。趁着这次机会,决定采用MVVM的新JS框架,最终评估选择vue.js大礼包,没错!正因为如此,前后端实现了完全分离,就不能采用session这样简单的登陆校验机制了,取而代之的是令牌+RESTful的方式进行交互,此时JWT闪亮登场!

    什么是JWT?

    JWT(Json Web Token)是一个开放标准(RFC 7519),它基于json对象定义了一种紧凑并且自包含的方式进行安全信息传输。由于消息经过了数字签名,所以是可以被校验和信任的。另外JWT可以使用密匙,或者使用RSA的公钥/私钥进行签名。
    其中的一些概念:

    • 紧凑:由于其较小的尺寸,JWT可通过URL,POST参数或HTTP标头内发送。 另外,较小的尺寸意味着传输速度很快。
    • 自包含:JWT的数据中可以包含用户的必要信息,避免了多次查询数据库的情况。

    为什么使用JWT?

    session认证
    因为http本身是无状态的协议,所以每一次的请求其实都要校验,session的原理就初次登陆的时候将相关信息保存到服务端,响应一个cookie保存到客户端,这样每次请求都携带cookie,服务器能够实现校验,这会面临3个问题
    1、难以实现单点登录,除非不同服务器之间共享session
    2、session默认保存在服务端,增加服务器的存储压力
    3、API调试麻烦

    OAuth 2.0
    OAuth 一般用于第三方接入的场景,管理对外的权限,比如什么第三方登录,微信授权,开放平台等,类似这些更加严谨的场景,相对来说也更加安全,但是部署过程复杂,授权流程也是麻烦,感觉是有些小题大做。而JWT更适用于类似RESTful API(微服务)之间的交互。

    自建token协议
    这种情况当然最灵活,但是除非有雄厚的资金实例,多余的时间和必要的情况,否则没必要重复造轮子呐。
    曾经我们还用过简单的办法,登陆之后根据用户信息进行加盐hash,该hash值即为token,然后以(hash,value)的形式存储在缓存或者数据库中,每次请求携带hash,然后读取校验该hash是否存在,否则校验失败。这种方式也不失为一种简单快捷的好办法,但是仅仅只能当做token校验,并且相关数据存储在服务器,每次访问都还需要进行一次查询,增加服务器开销

    什么时候使用JWT?

    下面是一些JWT有用的场景
    1、身份校验
    这是最常见的的使用场景,一旦用户完成了登陆校验,后面每一次的请求豆浆携带JWT,从而校验用户是否允许访问路由、服务、资源。更重要的是,通过JWT可以非常容易实现SSO(Single Sign On)单点登录,因为开销很小,这就意味着,在一个主站登陆了,别的站点就都可以轻松使用JWT访问。
    2、信息交换
    从上文可知,JWT是能够被签名的的,所以在安全信息传输中,是一个不错的方案,例如使用公钥私钥时,你可以确定收件人是谁,另外还可以校验确保内容是否被篡改。这样,就可以在一些类似下单、交易等等重要的场合使用。

    JWT的基本结构


    JWT由三部分组成,他们中间由.分隔:

    • Header 头部
    • Payload 数据
    • Signature 签名

    因此,典型的JWT看起来是这样的
    xxxxx.yyyyy.zzzzz

    Header

    头部主要包含2个部分,token类型和采用的加密算法。

    1. {
    2. "alg": "HS256",
    3. "typ": "JWT"
    4. }

    然后用Base64Url进行编码,就成了JWT的第一个部分

    Payload

    数据部分包含了主要的声明字段以及相应的值,声明主要包括3种类型:reserved , public 和 private

    • Reserved claims: 这些字段是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用。
      常用的有:
    1. ississuer): jwt签发者
    2. subsubject): 签发的项目
    3. audaudience): 接收jwt的一方
    4. expexipre): jwt的过期时间,这个过期时间必须要大于签发时间
    5. nbfnot before): 定义在什么时间之前,该jwt是不可用的.
    6. iatissued at): jwt的签发时间
    7. jtijwt token id): jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

    需要注意的是,声明名称只有三个字符长度,这是为了让JWT保持紧凑

    • Public claims:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.
    • Private claims:私有声明是提供者和消费者所共同定义的声明

    简单示例如下:

    1. {
    2. "iss": "www",
    3. "iat": 1441593502,
    4. "exp": 1441594722,
    5. "aud": "www.example.com",
    6. "sub": "www@example.com",
    7. "from_user": "B",
    8. "target_user": "A"
    9. }

    然后用Base64Url进行编码,就成了JWT的第二个部分

    Signature

    为了创建签名,你需要先对前面的部分进行Base64的编码,然后加上私匙,对其进行签名。
    例如,你想使用HMAC SHA256算法进行前面,那么创建过程如下:

    1. HMACSHA256(
    2. base64UrlEncode(header) + "." +
    3. base64UrlEncode(payload),
    4. secret)

    签名的目的是为了校验JWT的携带者信息,并且检验是否有篡改过所携带的JWT信息。
    HMAC SHA256算法计算之后的二进制数据默认进行Base64编码,就是JWT的第三个部分了

    将他们放在一起

    最终的结果是三段Base64字符串,通过.拼接在一起,这样就很容易在HTML和HTTP环境中传输,与基于XML的标准相比,更加紧凑节省资源。

    1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

    调试工具:https://jwt.io/#debugger-io

    项目实践JWT

    后端

    项目使用的是基于php的thinkphp5.0框架作为后端提供服务。前端则是vue+element-ui+axios,至于php类库,采用的是php中Star最多的
    https://github.com/lcobucci/jwt
    后端php通过composer安装之后使用起来非常的简单,新建一个类专门用于校验

    1. use LcobucciJWTBuilder;
    2. use LcobucciJWTParser;
    3. use LcobucciJWTSignerHmacSha256;
    4. use LcobucciJWTValidationData;
    5. class Auth
    6. {
    7. const KEY = 'febcbaae13751fa2ds44c2f107afb08d';
    8. const VALID_INFO = [
    9. 'Issuer' => 'http://www.xxxx.com',
    10. 'Audience' => 'http://aaa.xxxx.com',
    11. 'Subject' => 'test',
    12. 'Expire' => 259200
    13. ];
    14. public static function check()
    15. {
    16. $jwt = request()->header('jwt');
    17. $valid = new ValidationData();
    18. $valid->setIssuer(self::VALID_INFO['Issuer']);
    19. $valid->setAudience(self::VALID_INFO['Audience']);
    20. $valid->setSubject(self::VALID_INFO['Subject']);
    21. //校验jwt信息,同时校验签名,否则可以伪造信息
    22. $signer = new Sha256();
    23. if ($jwt->validate($valid) && $jwt->verify($signer, self::KEY)) {
    24. $uinfo = $jwt->getClaim('uinfo');
    25. //取出数据的时候是对象而不是数组
    26. $uinfo->id
    27. //后续的权限校验过程……
    28. }
    29. }
    30. public static function getSignedJWT($userinfo)
    31. {
    32. $signer = new Sha256();
    33. $token = (new Builder())
    34. ->setIssuer(self::VALID_INFO['Issuer'])
    35. ->setAudience(self::VALID_INFO['Audience'])
    36. ->setSubject(self::VALID_INFO['Subject'])
    37. ->setIssuedAt(time())
    38. ->setExpiration(time() + self::VALID_INFO['Expire'])
    39. //可以直接保存数组或对象
    40. ->set('uinfo', $userinfo)
    41. ->sign($signer, self::KEY)
    42. ->getToken()->__toString();
    43. return $token;
    44. }
    45. }

    前端

    登陆的时候保存JWT到localStorage,退出登录时前端删除保存的JWT即可。

    1. apiLogin.login(this.$data.loginForm).then(res => {
    2. if (res.data.ret === 0) {
    3. this.$local.set('jwt', res.data.jwt)
    4. this.$local.set('menu', res.data.menu)
    5. this.$local.set('rules', res.data.rules)
    6. this.$local.set('username', this.loginForm.username)
    7. this.$local.set('title', res.data.title)
    8. this.$local.set('gpid', res.data.gpid)
    9. this.$router.push('index')
    10. // 原本没有jwt,所以登陆获取之后手动设置一次
    11. this.$http.defaults.headers.common['jwt'] = this.$local.get('jwt')
    12. } else {
    13. this.isLogining = false
    14. this.$message.error(res.data.msg)
    15. }
    16. }).catch(() => {
    17. this.isLogining = false
    18. })

    base_api.js

    1. import axios from 'axios'
    2. import { Message } from 'element-ui'
    3. import local from 'store'
    4. // Add a request interceptor
    5. axios.interceptors.request.use(function (config) {
    6. return config
    7. }, function (error) {
    8. Message.error({
    9. showClose: true,
    10. message: '网络异常,请检查您的网络'
    11. })
    12. console.log(error)
    13. // Do something with request error
    14. return Promise.reject(error)
    15. })
    16. // Add a response interceptor
    17. axios.interceptors.response.use(function (response) {
    18. // 授权过期,无授权信息,跳出登陆
    19. if (response.data.ret === 4011 || response.data.ret === 4013) {
    20. window.location.href = '/#/login'
    21. // 删除本地的token令牌
    22. local.remove('jwt')
    23. Message.error({
    24. showClose: true,
    25. message: response.data.msg
    26. })
    27. return
    28. }
    29. if (response.data.ret === 4012) {
    30. // 无权限返回
    31. window.history.back()
    32. Message.error({
    33. showClose: true,
    34. message: response.data.msg
    35. })
    36. return
    37. }
    38. return response
    39. }, function (error) {
    40. Message.error({
    41. showClose: true,
    42. message: '网络异常,请检查您的网络'
    43. })
    44. return Promise.reject(error)
    45. })
    46. const baseUrl = process.env.API_ROOT
    47. axios.defaults.baseURL = baseUrl
    48. // 初始化的时候加载本地储存过的jwt
    49. if (local.get('jwt')) {
    50. axios.defaults.headers.common['jwt'] = local.get('jwt')
    51. }
    52. export const http = axios

    关于安全性

    Cookie 可以启用 HttpOnly 和 Secure:

    • HttpOnly:禁止浏览器的 JavaScript 环境访问 Cookie,防御针对 Cookie 的 XSS。
    • Secure:Cookie 只在 HTTPS 请求中被传输。

    但是为了实现正真意义上的无状态和跨域单点,还是坚持存储在LocalStorage,而目前localStorage存储没有对XSS攻击有任何抵御机制,一旦出现XSS漏洞,那么存储在localStorage里的数据就极易被获取到。
    如果一个网站存在XSS漏洞,那么攻击者注入如下代码,就可以获取使用localStorage存储在本地的所有信息。
    HTML5本地存储的安全性
    所以务必做好过滤安全检查。

    总结

    1、JWT并不包含权限校验部分,只包含Token校验,所以在Token校验完成之后,权限部分还需自行校验一次。
    2、jwt的payload数据部分不要存放敏感信息,此部分是任何人都可以解密查看的,而jwt主要依靠签名校验身份,同时也不建议存放易改动的信息,否则需要token过期或者重新登录才能来获取最新的信息。
    3、签名所用的secret私匙一定要保管好!!!
    4、务必使用https,否则用户被截获到token,就可以进行伪造攻击。
    5、JWT使用的场景中,一般是要跨域的,所以服务端需要做好CORS的策略支持。见这里
    6、若需要强制过期JWT,则在用户表新建一个签名时间字段即可,在登陆的时候检查,若JWT保存签名时间小于服务器签名时间,即强制过期

    参考

    1 2 3 4 5

  • 相关阅读:
    使用Xtrabackup 备份mysql数据库
    Myeclipse总结
    intellij idea问题及技巧
    Tomcat相关配置
    Spark常用算子总结
    前端开发经验
    最近用到的SQL语句
    subline text使用心得
    天龙八部谁是主角?(MR词频统计)
    elasticsearch CURL命令
  • 原文地址:https://www.cnblogs.com/leestar54/p/7242165.html
Copyright © 2011-2022 走看看