JWT
Json Web Token (简称JWT). 是一种开放标准(RFC 7519), 是目前较为流行的跨域认证解决方案,常被用于身份验证和授权机制。
常见的Session 用户认证方案
在谈论JWT之前,我们先聊聊session。目前我们最常见的用户认证方案是在当前会话(session)里面保存相关数据,客户端请求的的时候带上session以用来验证用户登录情况。大概步骤如下:
- 登录窗口,用户输入账号和密码向服务器发送登录请求
- 服务器收到请求,验证通过后,在当前会话(session)里面保存相关数据,比如用户角色,登录时间等。
- 服务器会根据这个session生成一个sessionId,返回给客户端,并写入用户的cookie中。
- 随后用户的每一次请求,都会通过cookie,将seesionID传给服务器
- 服务器收到sessionID就会验证当前用户身份和状态
这种方案肯定是没问题的,但也有一些缺点。第一就是扩展性不好,如果是服务器集群,就需要seession数据共享,保证每台服务器都能读取session
JWT数据结构
JWT是一个长的字符串,中间用点分割成3部分。大概长这样:
它的三个部分依次如下:
Header (头部)
Header部分是一个JSON对象,描述JWT的元数据,如下:
{
"algorithm": "HS256" ,
"type": "JWT
}
上面代码中,表示签名算法用的是HS256(HMAC SHA256); type 表示这个令牌的类型。最后用Base64URL算法将上面的JSON对象转成字符串
Payload (负载)
Payload部分也是一个JSON对象,用来存放实际要传递的数据。JWT规定了7个字段,分别是以下7个:
- iss(issuer):签发人
- exp(expiration time):过期时间
- sub(subject):主题
- aud(audience):受众
- nbf(Not before):生效时间
- iat(Issued At):签发时间
- 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的方法没有详细介绍,这里只是让大家了解下它的用法, 同时让不怎么接触后端的前端小伙伴更好地理解身份验证的流程。
- 首先用node命令将server.js文件启动起来
node server.js,监听8000端口。 - 这时候我们就可以在浏览器里访问
localhost:8000/index.html站点了。 大概长这样。页面只放置了一个查看列表的按钮
3. 点击
查看列表按钮,发送请求查看产品列表,这时候验证请求头没有token,显示登录窗口
4. 输入我们模拟的用户名和账号
5. 验证通过,返回JWT给到客户端,客户端重新发起请求获取产品列表
6. 然后我们到localstorage里面看看token长什么样,就是用点
(.)隔开的三段字符串
7. 我们可以尝试再次刷新页面,然后去点击
查看列表按钮,发现这时候就不再需要登录了。
JWT的特点
- JWT默认不加密,生成token后,可以用密钥再加密一次。所以没加密的时候不要把敏感信息写入
- JWT不仅可以用于认证还可以用于信息交换,降低服务器查询数据库次数
- JWT的缺点是,一旦JWT签发了,在到期之前一直有效,除非有额外的逻辑
- JWT本身包含了认证信息,一旦泄露,任何人都还可以获得该令牌的所有权限,所以有效期应该设置短些,如果是重要的权限还是需要二次认证的。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
结束语
JWT是现在比较流行的跨域身份验证解决方案,本文简单地介绍了它的原理,特点,数据组成等。同时用一个简单的小案例带大家了解它的实际用法,希望可以帮助到想要了解它的小伙伴。码字不易,喜欢的小伙伴点赞收藏吧