JWT实现单点登录

Posted 程序员爱叨叨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JWT实现单点登录相关的知识,希望对你有一定的参考价值。

1 什么是JWT

JWT是JSON WEB TOKEN的缩写,是为了在网络应用环境建传递声明而执行的一种基于JSON的开放标准。该Toekn被设计为紧凑并且安全的,特别适用于分布式站点的单点登录(SSO)

2 传统的session认证

传统的session认证是为了让我们的而英勇能识别是哪个用户发送的请求,需要在服务器存储一份用户的登陆信息,这份登陆信息会在返给用户的响应时传递给浏览器,浏览器保存为cookie,下次请求时发送给服务器,用来验证用户身份。

基于session认证存在一些问题:

  1. 用户增多,服务器保存的用户信息越来越多,服务端开销明显增大

  2. 在分布式的应用上,会限制负载均衡器的能力

  3. 由于是基于cookie来进行用户识别的,如果cookie被截取,会受到跨站请求伪造的攻击

3 基于token的鉴权机制

基于token的鉴权机制类似于http协议,也是无状态的,不需要在服务端保留用户的认证信息或者会话信息。这就意味着基于token认证的应用不需要考虑用户在哪一台服务器登陆了,为应用的扩展提供了遍历。

主要的流程如下:

  1. 用户使用用户名和密码来请求服务器登陆

  2. 服务器验证用户信息

  3. 服务器对通过验证的用户下发一个token

  4. 客户端收到token并存储,并且在以后的每次请求时再请求头中携带这个token

  5. 服务端在收到后续请求后对token验证,通过时返回对应数据

4 JWT的构成

JWT由三段信息构成,第一部分为头部(header),第二部分为载荷(payload),第三部分是签名(signature)

image

4.1 header

header用来描述描述关于该JWT的最基本的信息,承载两部分信息:

  1. 声明类型,这里是JWT

  2. 声明签名算法

这也可以被表示成一个JSON对象。

1{
2  'typ''JWT',
3  'alg''HS256'
4}

然后将头部进行base64加密,构成了第一部分

1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

4.2 payload

载荷是存放有效信息的地方,包含三个部分:

  1. 标准中注册的声明(建议但不强制使用)

    • iss: jwt签发者

    • sub: jwt所面向的用户

    • aud: 接收jwt的一方

    • exp: jwt的过期时间,这个过期时间必须要大于签发时间

    • nbf: 定义在什么时间之前,该jwt都是不可用的

    • iat: jwt的签发时间

    • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

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

  3. 私有的声明:提供者和消费者所共同定义的声明,不建议存放敏感信息,因为base64编码并不是加密过程,可以归类为明文信息

一个payload如下:

1{
2  "sub""1234567890",
3  "name""John Doe",
4  "admin"true
5}

然后对其进行base64编码,得到JWT的第二部分

1eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

4.3 signature

签名信息由三个部分组成:

  1. header(base64编码后的)

  2. payload(base64编码后的)

  3. secret

secret是利用HS256T算法进行加密时的密钥,将header和payload使用secret进行加密后,构成了jwt的第三部分

1// javascript
2var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
3
4var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt

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

5 如何发送JWT

5.1 使用`fetch`发送

发送jwt时一般是在请求头里加入Authorization,并加上Bearer标注:

1fetch('api/user/1', {
2  headers: {
3    'Authorization''Bearer ' + token
4  }
5})

服务端会验证token,如果验证通过就会返回相应的资源。

5.2 使用`axios`发送

在Vue中经常使用axios来帮助我们处理网络请求,使用axios设置请求头如下

 1import axios from 'axios';
2import JsonWebToken from 'jsonwebtoken';
3
4// 在登录成功后应该已经将token存到本机了
5const token = sessionStorage.getItem('userToken');
6
7// iss是我们预先定义信息,用来识别token是我们所需要的token
8const isTokenValid = !!(token && JsonWebToken.decode(token) && (JsonWebToken.decode(token).iss === config.userToken.iss));
9
10// 如果token有效
11if (isTokenValid) {
12  // get请求
13  axios.get('/api/user/deatil', {
14    headers: {
15      'Authorization''Bearer ' + token,
16      'Cookie' : 'sessionId=' + sessionId + '; recId=' + recId,
17    }
18    params: {
19      param1'string',
20      param2'string'
21    },
22  })
23
24  // post请求
25  axios.post('/api/user/deatil'
26    {
27      data: {value1'value}'
28    },
29    {
30      headers: {
31        'Authorization''Bearer ' + token,
32        'Cookie' : 'sessionId=' + sessionId + '; recId=' + recId,
33      }
34  })
35
36  // 全局设定
37  axios.headers.common['Authorization'] = 'Bearer ' + token
38}

6 在Koa2中的应用

jsonwebtoken是Node环境下JWT的实现,提供了一些API帮助我们快速的实现JWT的设计。

安装:

1$ npm install jsonwebtoken --save

6.1 签发token(加密)

1jwt.sign(payload, secretOrPrivateKey, [options, callback])
  • payload:可以是以object或者string或者buffer(object会被使用JSON.stringify方法转换为字符串)

  • secretOrPrivateKey:用来加密的密钥

  • options

    • `algorithm`:加密算法(默认值HS256)--(`alg`)

    • `expriesIn`:失效时间,数字默认单位为秒,字符串需指明时间单位,如`60`、`2 days`、`10h`、`7d`(如果是字符串并且没有单位,单位默认为毫秒)--(`exp`)

    • `notBefore`:生效时间,单位规则同`expriesInd`--(`nbf`)

    • `audience`:token的接受者--(`aud`)

    • `issuer`:签名的发行者--(`iss`)

    • `jwtid`:JWT的ID

    • `subject`:JWT的主题--(`sub`)

    • `noTimestamp`:不需要时间戳

    • `header`:头部信息

  • callback:加密失败的回调函数

生成一个有效期为1小时的token:

1jwt.sign({
2  data'foobar'
3}, 'secret', { expiresIn60 * 60 });
4
5jwt.sign({
6  data'foobar'
7}, 'secret', { expiresIn'1h' });

也可以用下面这种形式:

1jwt.sign({
2  expMath.floor(Date.now() / 1000) + (60 * 60),
3  data'foobar'
4}, 'secret');

6.2 验证token

验证token的合法性:

1jwt.verify(token,secretOrPublicKey,[options,callback])

来看验证时的一些简单的用法,更详细的用法参考官网。

 1// 同步验证成功
2var decoded = jwt.verify(token, 'shhhhh');
3console.log(decoded.foo) // bar
4
5// 异步验证成功
6jwt.verify(token, 'shhhhh'function(err, decoded{
7  console.log(decoded.foo) // bar
8});
9
10// 同步验证失败--token无效
11try {
12  var decoded = jwt.verify(token, 'wrong-secret');
13catch(err) {
14  // err
15}
16
17// 异步验证失败--token无效
18jwt.verify(token, 'wrong-secret'function(err, decoded{
19  // err
20  // decoded undefined
21})

6.3 解码token的payload

将payload解码,注意:这个过程无论验证是否通过都会执行

1jwt.decode(token [, options])
2
3// get the decoded payload ignoring signature, no secretOrPrivateKey needed
4var decoded = jwt.decode(token);
5
6// get the decoded payload and header
7var decoded = jwt.decode(token, {completetrue});
8console.log(decoded.header);
9console.log(decoded.payload)
  • token:JWT的token

  • options

    • `json`:强制使用`JSON.parse()`机械payload,即使头信息中不包括`"typ": "JWT"`

    • `complete`:返回一个包含解码后的payload和对象

看一下例子:

1// 解码payload不关心签名,不需要提供私钥
2var decoded = jwt.decode(token);
3
4// 获得对象
5var decoded = jwt.decode(token, {completetrue});
6console.log(decoded.header);
7console.log(decoded.payload)

6.4 实际例子

来看一个实际的例子:

 1const jwt = require('jsonwebtoken');
2
3//密钥,不能暴露
4const secret = 'aaa'
5
6// payload中不应该包含敏感信息
7const payload = {
8  username'jay',
9  userId123
10}
11
12// 选项(非必须)
13const options = {
14  expiresIn60 * 60 * 24 // 或者为"1d"
15}
16
17// 生成token
18const token = jwt.sign(payload, secret, options)
19
20// 验证token
21jwt.verify(token, secret, function (err, decoded{
22  // 如果验证通过
23  if (!err){
24    console.log(decoded.username);  // 'jay'
25  }
26})

7 kow-jwt

在Koa2中实现JWT的认证有中间件koa-jwt帮助我们完成,但是签发token的过程还是要安装上述过程,使用jsonwebtoken实现

直接看一个简单的例子:

 1import Koa from 'koa';
2import jwt from 'koa-jwt';
3
4const app = new Koa();
5const router = new KoaRouter();
6const authRouter = auth.router;
7
8// 除了/public/之外的请求都需要经过JWT验证
9app.use(jwt({ secret'shared-secret' }).unless({ path: [/^\/public/] }));
10
11// 无论请求中是否存在Authorization header都保证执行
12app.use(jwt({ secret'shared-secret'passthroughtrue }));
13
14// 所有走/api/打头的请求都需要经过JWT验证
15router.use('/api', jwt({ secret'shared-secret' }), apiRouter.routes()); 

8 JWT的缺点和适用场景

这篇文章提出了一些自己观点,可以参考一下,它认为jwt的token方案最适合的场景是无状态的场景,若需要考虑token注销和某些token续签的场景,实际上就是给token加上了状态,这就类似于传统的方案了,但是相比传统方案的优势是,服务端可以不用长期保存用户状态,仅仅在一定时间段内保留一小部分状态即可。

9 总结

JWT的优点如下:

  1. 因为JSON的通用性,所以JWT是可以跨语言支持的

  2. 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息

  3. JWT体积很小,便于传输

  4. 不需要在服务端保存回话信息,所以易于应用的扩展

在安全方面需要注意:

  1. 不应该在payload部分存放敏感信息,因为该部分是非加密的

  2. 保护好secret私钥,不应该暴露给客户端

  3. 应该使用https协议

Koa2中的具体实现:

  1. 签发token使用jsonwebtoken

  2. 验证token使用koa-jwt中间件

参考

  • https://www.jianshu.com/p/576dbf44b2ae

  • http://blog.leapoahead.com/2015/09/06/understanding-jwt/

  • http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/?utm_source=tuicool&utm_medium=referral

  • https://www.jianshu.com/p/a7882080c541

  • https://blog.csdn.net/hzwy23/article/details/53224724

  • https://www.npmjs.com/package/koa-jwt

  • https://www.npmjs.com/package/jsonwebtoken

  • https://blog.csdn.net/u010467784/article/details/78623173

  • https://juejin.im/entry/5993a030f265da24941202c2


以上是关于JWT实现单点登录的主要内容,如果未能解决你的问题,请参考以下文章

第十篇单点登录原理和JWT实现

第十篇单点登录原理和JWT实现

Spring Security + JWT 实现单点登录,还有谁不会。。。

初识单点登录及JWT实现

使用jwt技术实现系统间的单点登录

JWT实现单点登录