认证和授权,其实吧简单来说就是:认证就是让服务器知道你是谁,授权就是服务器让你知道你什么能干,什么不能干(例如下面meta元信息),认证授权俩种方式:Session-Cookie与JWT,下面我们就针对这两种方案就行阐述。
说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。毕竟我们在使用jwt之前,是使用session认证的。
http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,还需要认证,所以cookie机制可以是HTTP保存状态。而session是基于cookie的,当 client通过用户名密码请求server并通过身份认证后,server就会生成身份认证相关的 session 数据,并且保存在内存或者内存数据库。并将对应的 sesssion_id返回给client,client会把保存session_id(可以加密签名下防止篡改)在cookie中。此后client的所有请求都会附带该session_id,以确定server是否存在对应的session数据以及检验登录状态以及拥有什么权限,如果通过校验,可以请求接口数据,否则则重新登录。
优势:
劣势:
session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
cookie + session在跨域场景表现并不好
基于 cookie 的机制很容易被CSRF 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里。
优势:
因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
它不需要在服务端保存会话信息, 所以它易于应用的扩展
保护好secret私钥,该私钥非常重要。
比较了session认证和token之后,我们应该会了解各有各的优劣,下面我们来手写JWT。
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案,相较于session机制,服务器就不需要保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。JWT 实际上是一个令牌(Token),服务器会将一些元数据、指定的secret进行签名并生成token,并返回给客户端,客户端得到这个服务器返回的令牌后,需要将其存储到 Cookie 或 localStorage 中,此后,每次与服务器通信都要带上这个令牌,可以把它放到 Cookie 中自动发送,但这样做不能跨域,所以更好的做法是将其放到 HTTP 请求头 Authorization 字段里面。
token存储在localstorage中的。
session、cookie、sessionStorage、localstorage的区别与特性
session : 主要存放在服务器端,相对安全
cookie :可设置有效时间,默认是关闭浏览器后失效,主要存放在客户端,并且不是很安全,可存储大小约为5m
sessionStorage:仅在当前会话下有效,关闭页面或浏览器后被清除
localstorage: 除非被清除,否则永久保存
需要在服务端安装并引入jsonwebtoken模块
安装JWT库:
npm i jsonwebtoken
let jwt = require("jsonwebtoken")
//然后创建签名数据,生成token:
let secret = "xwc"
token:jwt.sign({username:'admin'},secret,{
expiresIn:20
})
生成的token字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoi5byg5LiJIiwiaWF0IjoxNDYyODgxNDM3fQ.uVWC2h0_r1F4FZ3qDLkGN5KoFYbyZrFpRJMONZrJJog
当客户端带着token到服务端时,对token字符串,可以这样解码:
jwt.verify(token,secret,(err,decode)=>{
// decode就是解密出来的信息
if(err){
return res.json({
code:1,
data:"token失效了"
})
}else{
// token合法 在这里,需要把token的失效延长
res.json({
code:0,
username:decode.username,
token:jwt.sign({username:'admin'},secret,{
expiresIn:20
})
})
}
})
有以上分析可见,生成的token为一个很长的字符串,分为三部分,每部分由.号隔开,即 头部.载荷.签名,20秒后token校验结果为error,即token已经过期,校验的时候,会得到token的解码数据,主要包括生成token时候的元数据、token的签发时间(iat)、token的过期时间(exp)
其实这是一个分为 3 段的字符串,段与段之间用 点号 隔开
Header.Payload.Signature(头部.载荷.签名)
在字符串中每一段都是被 base64url 编码后的 JSON,其中 Payload 段可能被加密。
JWT 的 Header 通常包含两个字段,分别是:typ(type) 和 alg(algorithm)。
const header = {
// 加密算法
alg: 'HS256',
type: 'jwt'
}
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
const payload = {
// 表示 jwt 创建时间
iat: 1532135735,
// 表示 jwt 过期时间
exp: 1532136735,
// 用户 id,用以通信
user_id: 10086
}
Sign 由 Header,Payload 以及 secretOrPrivateKey 计算而成。
对于 secretOrPrivateKey,如果加密算法采用 HMAC,则为字符串,如果采用 RSA 或者 ECDSA,则为 PrivateKey。
// 由 HMACSHA256 算法进行签名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)
// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign
从生成 jwt 规则可知客户端可以解析出 payload,因此不要在 payload 中携带敏感数据,比如用户密码
在生成规则中可知,jwt 前两部分是对 header 以及 payload 的 base64 编码。
当服务器收到客户端的 token 后,解析前两部分得到 header 以及 payload,并使用 header 中的算法与 secretOrPrivateKey 进行签名,判断与 jwt 中的签名是否一致。
由上可知,jwt 并不对数据进行加密,而是对数据进行签名,保证不被篡改。除了在登录中可以用到,在进行邮箱校验和图形验证码也可以用到。
------------------------下面对JWT在登录中使用验证一把。-------------------------------
由于在使用jwt认证的时候,客户端向服务器发起请求的时候,都要带上token,即要获取到token并将其放到请求头的Authorization字段中,服务器才能从authorization中取出token并进行校验,所以我们必须通过拦截器去实现,在每次请求之前将请求进行拦截,然后添加上token,再继续向服务器发起请求。
import axios from "axios" //封装一个比较好用的 ajax
import store from "../store"
import { getLocal } from "../libs/local"
class AjaxRequest {
constructor() {
this.baseURL = process.env.NODE_ENV == "production" ? "/" : "http://localhost:3030"
this.timeout = 3000;//访问接口时间 超时了 3s
this.queue = {}; // 存放每一次的请求
}
merge(options) {
return { ...options, baseURL: this.baseURL, timeout: this.timeout }
}
setInterceptor(instance, url) {
// 每次请求时,都要加上一个loading效果
instance.interceptors.request.use((config) => {
// 在请求拦截中,每次请求,都会加上一个Authorization头
config.headers.Authorization = getLocal("token");
// 第1次请求时,显示Loading动画
if (Object.keys(this.queue).length === 0) {
store.commit('showLoading'); //只让小动画 显示一次 后来在一直请求 就不用一直转转
}
this.queue[url] = url;
return config;
});
instance.interceptors.response.use((res) => {
delete this.queue[url]
if (Object.keys(this.queue).length === 0) {
store.commit('hideLoading')
}
// store.commit('hideLoading')
return res.data; //相应拦截 过滤数据
});
}
request(options) {
let instance = axios.create(); //创建一个ajax实例 发出请求
this.setInterceptor(instance, options.url); // 设置拦截
let config = this.merge(options);
return instance(config) //实例 表示调用ajax 返回一个ajax的实例
}
}
export default new AjaxRequest;
export const setLocal = (key,value)=>{
if(typeof value == 'object'){
value = JSON.stringify(value)
}
localStorage.setItem(key,value)
}
export const getLocal = (key)=>{
return localStorage.getItem(key)
}
我们需要在路由跳转之前,进行登录校验,即校验登录的token是否已经过期,
分两种情况:
如果token没有失效,则可以继续访问页面,还可以设置那些路由是在登录条件后才可以访问的
如果token已经失效,那么检查一下所访问的页面是否需要登录才能访问,如果是需要登录后才能访问,那么跳转到登录页面;如果是不需要登录也能访问的页面则继续访问;并且在登录之后,再点击登录页面,不能显示登录界面,要跳转到其他页面。
// 每一次切换路由时,beforeEach都执行
router.beforeEach(async (to, from, next) => {
// console.log("hello") isLogin是否登录了
let isLogin = await store.dispatch('validate')
// needLogin 表示哪些路由需要在登录条件下才能访问
let needLogin = to.matched.some(match=>match.meta.needLogin)
if(needLogin){
// 需要登录
if(isLogin){
// 登录过了
next()
}else{
next("/login")
}
}else{
// 不需要登录
if(isLogin && to.path === "/login"){
next("/");
}else{
next()
}
}
})
needLogin 这个needLogin是在router中进行自定义配置的,在配置路由的时候,允许通过meta属性配置一些自定义的元数据,如下所示:
{
path: '/profile',
name: 'profile',
component: () => import('./views/Profile.vue'),
meta:{
needLogin:true
}
}
// 验证token的接口
app.get("/validate",(req,res)=>{
let token = req.headers.authorization;
jwt.verify(token,secret,(err,decode)=>{
// decode就是解密出来的信息
if(err){
return res.json({
code:1,
data:"token失效了"
})
}else{
// token合法 在这里,需要把token的失效延长
res.json({
code:0,
username:decode.username,
token:jwt.sign({username:'admin'},secret,{
expiresIn:20
})
})
}
})
})
token:jwt.sign({username:‘admin’},secret,{expiresIn:20}):作用如果用户一直在操作路由界面,那token的失效时间一直延长。这样避免了用户在玩着 玩着 突然token 过期,那么还需要重新登录的问题。
在路由钩子里面调用api接口:
let isLogin = await store.dispatch('validate')
async validate({commit}) {
let r = await validate();
if (r.code === 0) {
commit('setUser', r.username)
//当重新演唱token的时间时,会重新设置保存token
setLocal("token", r.token)
}
return r.code === 0; // 返回token是否失效
}
jwt认证,主要就是Vue路由钩子beforeEach()的应用,以及请求拦截器的封装,在每次路由跳转前进行token认证(校验),检测token是否失效,其校验过程就是向服务器发起一个请求,比如"/validate",由于客户端请求拦截器的作用,会在发起"/validate"请求之前,在请求头的Authorization字段加上token,服务器收到token后就能对token是否有效进行校验了,然后返回token校验结果,客户端再根据token的校验结果进行路由的具体跳转。
参考:
参考一
参考二