json web token

json web token 前后端总结

一. 关于 Token 简介

1. 基于 Token 的 WEB 后端认证机制

几种常见的认证机制 >- HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。

  • OAuth(开放授权)

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;

  • Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效

  • Token Auth

image

2. Token Auth的优点

Token机制相对于Cookie机制又有什么好处呢?

  • 支持跨域访问:

    • Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  • 无状态(也称:服务端可扩展行):

    • Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  • 更适用CDN:

    • 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
  • 去耦:

    • 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  • 更适用于移动应用:

    • 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF:

    • 因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能:

    • 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
  • 不需要为登录页面做特殊处理:

    • 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
  • 基于标准化:

    • 你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

二. 关于 JWT 三段原理理解

1. JWT的组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
  • #### 头部(Header) JWT需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
  "typ": "JWT",
  "alg": "HS256"
}

在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。

对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部)。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • #### 载荷(Payload) 我们可以把一些业务需求操作描述成一个JSON对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT。
{
    "iss": "John Wu JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "jrocket@example.com",
    "from_user": "B",
    "target_user": "A"
}

这五个字段都是由JWT的标准所定义的: - iss: 该JWT的签发者 - sub: 该JWT所面向的用户 - aud: 接收该JWT的一方 - exp(expires): 什么时候过期,这里是一个Unix时间戳 iat(issued at): 在什么时候签发的

将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
  • #### 签名(签名)

将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

这一部分的过程在node-jws的源码中有体现

最后,我们将上面拼接完的字符串用HS256算法进行加密。

在加密的时候,我们还需要提供一个密钥(secret)。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容

rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

这一部分又叫做签名。

image

最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

三. 前后端传递出来 Token 流程原理

用户认证八步走

所谓用户认证(Authentication),就是让用户登录,并且在接下来的一段时间内让用户访问网站时可以使用其账户,而不需要再次登录的机制。

小知识: 可别把用户认证和用户授权(Authorization)搞混了。用户授权指的是规定并允许用户使用自己的权限,例如发布帖子、管理站点等。

首先,服务器应用(下面简称“应用”)让用户通过Web表单将自己的用户名和密码发送到服务器的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

image

接下来,应用和数据库核对用户名和密码。

image

核对用户名和密码成功后,应用将用户的id(图中的user_id)作为JWT Payload的一个属性,将其与头部分别进行Base64编码拼接后签名,形成一个JWT。这里的JWT就是一个形同lll.zzz.xxx的字符串。

image

应用将JWT字符串作为该请求Cookie的一部分返回给用户。注意,在这里必须使用HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击(XSS攻击)。

image

在Cookie失效或者被删除前,用户每次访问应用,应用都会接受到含有jwt的Cookie。从而应用就可以将JWT从请求中提取出来。

image

应用通过一系列任务检查JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。

image

应用在确认JWT有效之后,JWT进行Base64解码(可能在上一步中已经完成),然后在Payload中读取用户的id值,也就是user_id属性。这里用户的id为1025。

image

应用根据用户请求进行响应。 image

四. 代码 demo 实现

首先我先建立一个 jwt.js 文件来专门生产token。

/**
 * @function    jwtProducer
 * @description jwt的统一生产方法
 * @returns     {{username: *, token: *}}
 */
const jwt = require('jsonwebtoken');

module.exports =  {

    /*这里的 username 是 用户的账号+密码*/
    jwtProducer: function (username, dbName, userId) {
        // JWT Id
        const jti = (Math.random() * 100000000000000000).toString();
        // JWT 的签发者
        const iss = 'localhost';
        // 签发时间
        const iat = Date.now() / 1000;
        // 失效时间
        const exp = iat + 7200;
        // 用户 ID
        const sub = userId;
        // 数据库名称
        const location = dbName;

        const payload = {
            jti: jti,
            iss: iss,
            iat: iat,
            exp: exp,
            sub: sub,
            location: location
        };

        var token = jwt.sign(payload, 'Nodezhang'); // 'Nodezhang'是自定义的校验码。

        return {
            operatorUsername: username,
            token: token
        };

    }


};

然后你可以 在你的 login.js 中开始业务代码处理了

const jwtP = require('../model/jwt');
const jwt = require('jsonwebtoken');

/**
 * @function lookToken
 * @description 检查 jwt token 密码
 * @param token
 * @param callback
 */
function lookToken(token, callback) {
    jwt.verify(token, 'Nodezhang', function (error, payload) { // 检验Token的合法性 用到 检验码
        if (error) {
            console.log(error);
            return callback;
        }
        callback(null, payload);

    });
}
......//省略代码处


var token = req.body.token;
    // console.log('This is :'+token);
    if (token === undefined || !token) {
        return res.status(401).end('invalid token');
    }

    // 检查 jwt token 密码
    lookToken(token, function (error, payload) {
        if (error) {
            return res.status(402)
                .end('invalid token');
        }

        if (payload.exp < Date.now() / 1000) {
            return res.status(405)
                .end('expired token');
        }
        return payload;

    });
    
    
    
/*后端验证登录密码正确后调用生产token*/
 var tokenData = jwtP.jwtProducer(loginData, 'nodezhang', result[0].id);
 result = {
                            code: 200,
                            msg: '密码正确',
                            tokenData: tokenData,
                        };

前端部分

$.ajax({
                      url: "/login/userLogin",
                      data: {
                        username: $("#username").val(),
                        password: $("#password").val(),
                           token: tokenCook
                      },
                      type: "POST",
                      timeout: 36000,
                      dataType: "text",
                      success: function (data, textStatus) {
                        //alert(data);
                        var dataJson = eval("(" + data + ")");
                        if (dataJson.code === 200) {
                          alert("登录成功");
                          var toke = dataJson.tokenData.token;
                          console.log(toke);
                          document.cookie = toke;
                          alert(document.cookie);
                          debugger;
                          window.location.href = "/";
                        } else if (dataJson.code === 300) {
                          popup.show();
                          popupContent.html("账号不存在,请重新输入!");
                        } else if (dataJson.code === 400) {
                          popup.show();
                          popupContent.html("密码有误,请重新输入!");
                        } else {
                          popup.show();
                          popupContent.html("登录出错!");
                        }
                      },
                      error: function (XMLHttpRequest, textStatus, errorThrown) {
                        alert("error:" + textStatus);
                      }
                    }
            );