协议标准: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. iss(issuer): jwt签发者
  2. sub(subject): 签发的项目
  3. aud(audience): 接收jwt的一方
  4. exp(exipre): jwt的过期时间,这个过期时间必须要大于签发时间
  5. nbf(not before): 定义在什么时间之前,该jwt是不可用的.
  6. iat(issued at): jwt的签发时间
  7. jti(jwt 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 Lcobucci\JWT\Builder;
  2. use Lcobucci\JWT\Parser;
  3. use Lcobucci\JWT\Signer\Hmac\Sha256;
  4. use Lcobucci\JWT\ValidationData;
  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存储在本地的所有信息。



所以务必做好过滤安全检查。

总结

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

最新文章

  1. 常用的SQL语句
  2. Mac Pro 安装 Homebrew 软件包管理工具
  3. oracle 创建数据表空间和用户
  4. android 自定义组件
  5. Fragstats景观分析研究
  6. 【转】Android WebRTC 音视频开发总结(一)
  7. 关于MD5校验和java工程下的校验
  8. centos6.5静态IP和DNS设置
  9. 使用CAEmitterLayer实现下雪效果
  10. Asp.net禁用页面缓存的方法
  11. 一个用于每一天JavaScript示例-SVG中间javaScript画廊
  12. xcode5向APP store上传应用的时候注意点
  13. oracle中的常用函数1-------decode方法
  14. 容易产生错误的where条件
  15. 在Ubuntu下永久修改主机名
  16. Python爬虫——城市公交、地铁站点和线路数据采集
  17. vue不是内部或外部命令,配置一个Path系统变量就可以解决
  18. [LeetCode] Set Intersection Size At Least Two 设置交集大小至少为2
  19. HBase replication
  20. CS通用项目系统搭建——三层架构第一天

热门文章

  1. 暂停、恢复CALayer 动画
  2. 【小梅哥SOPC学习笔记】设置Eclipse在编译(build)前自动保存源代码文件
  3. Hadoop的Windows伪分布式学习
  4. hibernate3的配置
  5. php的数组汉字符串常用函数
  6. jenkinsapi操作Jenkins,提示:No valid crumb was included in the request
  7. ASP .Net Core 2.0 修改默认端口
  8. 「HNOI 2014」 江南乐
  9. 201621123012 《Java程序设计》第9周学习总结
  10. HTTP响应状态码参考