JWT在Node.js中的应用案例

253 阅读7分钟

JWT

Json Web Token (简称JWT). 是一种开放标准(RFC 7519), 是目前较为流行的跨域认证解决方案,常被用于身份验证和授权机制。

常见的Session 用户认证方案

在谈论JWT之前,我们先聊聊session。目前我们最常见的用户认证方案是在当前会话(session)里面保存相关数据,客户端请求的的时候带上session以用来验证用户登录情况。大概步骤如下:

  1. 登录窗口,用户输入账号和密码向服务器发送登录请求
  2. 服务器收到请求,验证通过后,在当前会话(session)里面保存相关数据,比如用户角色,登录时间等。
  3. 服务器会根据这个session生成一个sessionId,返回给客户端,并写入用户的cookie中。
  4. 随后用户的每一次请求,都会通过cookie,将seesionID传给服务器
  5. 服务器收到sessionID就会验证当前用户身份和状态

这种方案肯定是没问题的,但也有一些缺点。第一就是扩展性不好,如果是服务器集群,就需要seession数据共享,保证每台服务器都能读取session

JWT数据结构

JWT是一个长的字符串,中间用点分割成3部分。大概长这样:

image.png 它的三个部分依次如下:

Header (头部)

Header部分是一个JSON对象,描述JWT的元数据,如下:

{
"algorithm": "HS256" ,
"type": "JWT
}

上面代码中,表示签名算法用的是HS256(HMAC SHA256); type 表示这个令牌的类型。最后用Base64URL算法将上面的JSON对象转成字符串

Payload (负载)

Payload部分也是一个JSON对象,用来存放实际要传递的数据。JWT规定了7个字段,分别是以下7个:

  1. iss(issuer):签发人
  2. exp(expiration time):过期时间
  3. sub(subject):主题
  4. aud(audience):受众
  5. nbf(Not before):生效时间
  6. iat(Issued At):签发时间
  7. jti(JWT ID):编号

当然除了官方字段,你也可以定义私有字段(JWT默认是不加密的,所以不要把敏感信息放在这里),如:

{
"sub": "liebiao",
"admin": true
}

这个Payload对象也会被Base64URL算法转成字符串

Signature(签名)

Signature部分是对前两部分的签名,防止数据篡改。这里需要指定一个密钥(secret),可以是个字符串,只有服务端才知道,然后使用Header里面指定的签名算法,按照以下公式产生签名,最终生成JWT字符串。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

JWT的使用

客户端当收到服务端返回的JWT时,可以存在Cookie里或者localStorage. 随后,客户端与服务端的通信都要带上这个JWT,可以放在Cookie里自动发送但是不能跨域,更好的做法是放在HTTP的请求头Authorization字段里面

fetch(url,{
    method: 'POST',
    headers: {
        'authorization': token,
    }
})

使用案例

下面就用Node.js在本地实现下JWT的基本用法。

需求背景

用户访问网站主页不需要登录,但是当访问产品列表的时候需要登录,这时候我们可以让用户输入账号和密码,服务端验证通过后返回一个JWT给客户端,客户端讲JWT存到localstorage,随后再去访问产品列表的时候就在请求头的Authorization字段带上JWT,服务端验证成功返回产品列表,就不用再重复登录了。

前端页面

这里懒得搭建react环境,所以我直接引入的react,react-dom包和babel编译包。用的是react16,版本有点老了,但是不影响阅读。

**index.html**
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
    <script type="text/babel" src="./index.js"></script>
</body>
</html>

index.js 文件

index.js
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isShowButton: true,
            isShowLogin: false,
            userName: '',
            password: '',
            dataList: [],
        };
        this.renderData = this.renderData.bind(this);
        this.renderLogin = this.renderLogin.bind(this);
        this.getProductList = this.getProductList.bind(this);
        this.goLogin = this.goLogin.bind(this);

    }

    getProductList() {
        console.log('local', localStorage)
        const token = localStorage.getItem('token');
        fetch('/user/getlist', {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'authorization': token
            }
        }).then(res => {
            res.json().then((result) => {
                if (result.status === 200) {
                    this.setState({
                        isShowButton: false,
                        isShowLogin: false,
                        dataList: result.data,
                    })
                    return;
                }
                if (result.status === 403) {
                    console.log(result.msg);
                    this.setState({
                        isShowButton: false,
                        isShowLogin: true
                    })
                }
            })
        })
    }

    goLogin() {
        const { userName, password } = this.state; 
        fetch('/user/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ userName, password})
        }).then((res) => {
            res.json().then((result) => {
                if (result.status === 200) {
                    console.log('local', localStorage)
                    localStorage.setItem('token', result.token);
                    this.setState({
                        isShowButton: true,
                        isShowLogin: false,
                    });
                    return;
                }
                alert(`login err, status is: ${result.status}, reason is: ${result.msg}`);
            })
        }).catch((e) => {
            alert('login failed: ', e.message);
        })
    }

    renderLogin() {
        const { userName, password } = this.state;
        return <div>
            <div>账号: <input value={userName} onChange={(e) => { this.setState({ userName: e.target.value })}} /></div>    
            <div>密码: <input value={password} onChange={(e) => { this.setState({ password: e.target.value })}} /></div>    
            <div><button onClick={this.goLogin}>登录</button></div>    
        </div>
    }

    renderData() {
        const { dataList } = this.state;
        return dataList.map(item => {
            return <div key={item.name}>Product is {item.name}, Price is {item.price}</div>
        });
    }

    render() {
        return <div>
            { this.state.isShowButton && <button onClick={this.getProductList}>查看列表</button> }    
            { this.state.isShowLogin && this.renderLogin() }    
            { this.state.dataList.length ? this.renderData() : null}    
        </div>
    }
}
const container = document.querySelector('#container');
ReactDOM.render(<App/>, container);

服务端

服务端用的是express框架,简单易上手。 这里引入了一个require('./jwt')的文件,这个我们稍后介绍。首先用app.use()拦截了所有请求,如果是请求的user/getList则需要验证用户登录状态,如果请求头没有token或者token不合法则返回403状态码给到客户端。客户端就会显示登录页要求用户登录,这里申明了一个userList的JSON数组用来模拟数据库存储的客户信息,当用户登录的信息匹配某条数据时则表示该用户是合法用户,随后返回JWT以及产品列表给客户端。

**server.js**
const JwtUtil = require('./jwt');
const express = require('express');
const path = require("path");
const app = express();
const userList = [{
    userName: 'guoguo',
    password: '123',
    _id: '2873492791'
}, {
    userName: 'tt',
    password: '111',
    _id: '287sdkjfoii'
}];
const dataList = [{
    product: 'Apple',
    price: 12
}, {
    product: 'Banana',
    price: 20
}];

app.use(express.static(path.resolve(__dirname, "../public")));
app.use(express.json());

app.use(function (req, res, next) {
    // 获取列表需要校验token
    console.log('result is: ', req.url);
    if (req.url === '/user/getlist') {
        const token = req.headers.authorization;
        const jwt = new JwtUtil(token);
        const result = jwt.verifyToken();
        if (result.status) {
            next();
        } else {
            res.send({
                status: 403,
                msg: result.msg
            });
        }
    } else {
        // 不需要做token校验的请求直接进行下一步
        next();
    }
})

app.post('/user/login', function (req, res) {
    console.log('req is: ', req.body);
    const {
        userName,
        password
    } = req.body;
    new Promise((resolve, reject) => {
        const user = userList.find(item => item.userName === userName);
        if (user && user.password === password) {
            // 用户已存在于数据库中
            resolve(user);
        } else {
            reject('user is not exist');
        }
    }).then(result => {
        if (result) {
            const jwt = new JwtUtil(result._id);
            const token = jwt.generateToken();
            if (token) {
                res.send({
                    status: 200,
                    msg: 'login success',
                    token
                });
            }
        }
    }).catch((e) => {
        res.send({
            status: 404,
            msg: e
        });
    })


})

app.get('/user/getlist', function (req, res) {
    res.setHeader('Content-type', 'application/json');
    res.send({
        status: 200,
        data: dataList
    });
})

app.listen(8000, function (err) {
    console.log('listening port 8000...');
})

jwt.js 文件, 我们在这个文件里引入了JWT的包jsonwebtoken. 它自带一些方法,比如签名jwt.sign(), 校验jwt.verify(). 这里面的privateKey就是上文中我们说到的用于签名的密钥,这里用一个简单的字符串代替了。

**jwt.js**
const jwt = require('jsonwebtoken');

class JwtUtil {
    constructor(data) {
        this.data = data;
        this.privateKey = 'helloworld';
    }

    generateToken() {
        const data = this.data;
        const created = Math.floor(Date.now() / 1000);
        const token = jwt.sign({
            data,
            exp: created + 60 * 3 // 3 minutes will be expire
        }, this.privateKey);
        return token;
    }

    verifyToken() {
        const token = this.data;
        try{
            const result = jwt.verify(token, this.privateKey);
            const currentTime = Math.floor(Date.now() / 1000);
            if (result && currentTime <= result.exp) {
                return { status: true };
            }
            return { status: false, msg: 'token error'};
        }catch(e) {
            return { status: false, msg: e.message };
        }
    }
}

module.exports = JwtUtil;

案例运行步骤及效果

案例是非常简单的一个基于express的node.js案例,很多jwt的方法没有详细介绍,这里只是让大家了解下它的用法, 同时让不怎么接触后端的前端小伙伴更好地理解身份验证的流程。

  1. 首先用node命令将server.js文件启动起来node server.js,监听8000端口。
  2. 这时候我们就可以在浏览器里访问localhost:8000/index.html站点了。 大概长这样。页面只放置了一个查看列表的按钮

image.png 3. 点击查看列表按钮,发送请求查看产品列表,这时候验证请求头没有token,显示登录窗口

image.png 4. 输入我们模拟的用户名和账号

image.png 5. 验证通过,返回JWT给到客户端,客户端重新发起请求获取产品列表 image.png 6. 然后我们到localstorage里面看看token长什么样,就是用点(.)隔开的三段字符串

image.png 7. 我们可以尝试再次刷新页面,然后去点击查看列表按钮,发现这时候就不再需要登录了。

JWT的特点

  1. JWT默认不加密,生成token后,可以用密钥再加密一次。所以没加密的时候不要把敏感信息写入
  2. JWT不仅可以用于认证还可以用于信息交换,降低服务器查询数据库次数
  3. JWT的缺点是,一旦JWT签发了,在到期之前一直有效,除非有额外的逻辑
  4. JWT本身包含了认证信息,一旦泄露,任何人都还可以获得该令牌的所有权限,所以有效期应该设置短些,如果是重要的权限还是需要二次认证的。
  5. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

结束语

JWT是现在比较流行的跨域身份验证解决方案,本文简单地介绍了它的原理,特点,数据组成等。同时用一个简单的小案例带大家了解它的实际用法,希望可以帮助到想要了解它的小伙伴。码字不易,喜欢的小伙伴点赞收藏吧