cookie、session、token、OAuth实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(
token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。
cookie 或者 session 去实现| 属性 | 说明 |
|---|---|
name=value | 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型。如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。 |
domain | 指定 cookie 所属域名,默认是当前域名 |
path | 指定 cookie 在哪个路径(路由)下生效,默认是 ‘/’。 |
| 如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。 | |
maxAge | cookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。- 比 expires 好用。 |
expires | 过期时间,在设置的某个时间点后该 cookie 就会失效。 |
| 一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除 | |
secure | 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。 |
| 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效 | |
httpOnly | 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全 |
Cookie的特点
pnpm install cookie-parser --save
const cookieParser=require("cookie-parser");
app.use(cookieParser());
res.cookie("name",'zhangsan',{maxAge: 1000*60*60, httpOnly: true});
//res.cookie(名称,值,{配置信息})
req.cookies.name;
下面是一个基础实例:
const express=require("express");
const cookieParser=require("cookie-parser");var app=express();//设置中间件
app.use(cookieParser());app.get("/",function(req,res){res.send("首页");
});//设置cookie
app.get("/set",function(req,res){//如果不进行任何设置,有效期默认为1个会话,浏览器关闭即失效// res.cookie('isLogin','true');res.cookie("userName",'张三',{maxAge: 1000*60*60, httpOnly: true});res.send("设置cookie成功");
});//获取cookie
app.get("/get",function(req,res){console.log(req.cookies.userName);res.send("获取cookie成功,cookie为:"+ req.cookies.userName);
});app.listen(8080);
当访问set路由后会设置cookie,当访问get路由后会获取到设置的cookie值。当然你也可以在其他页面继续获取当前cookie,以实现cookie共享。cookie和session都可以在网页的响应头看到set-cookie
cookie加密是让客户端用户无法的获取cookie明文信息,是数据安全的重要部分;一般的我们可以在保存cookie时对cookie信息进行加密,或者在res.cookie中对option对象的signed属性设置设置成true即可。
const express = require("express");
const cookieParser = require("cookie-parser");var app = express();
app.use(cookieParser('secret'));//签名 (加密) 要指定miyao ,什么名字都星行,列如:"xiaoxuesheng"app.get("/",function(req,res){res.send("主页");
});//获取cookie
app.use(function(req,res,next){console.log(req.signedCookies.name);next();
});//设置cookie
app.use(function(req,res,next){console.log(res.cookie("name","zhangsan",{httpOnly: true,maxAge: 200000,signed: true}));res.end("cookie为:"+req.signedCookies.name);
});app.listen(8080);

session 认证流程:
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
举一个例子
- cookie就像去理发店办了张会员卡,下次去带会员卡(在响应头中设置cookie,以后改域名下每次请求的请求头都会附带cookie)
- session就像去理发店办了张卡,但卡留在了那,记住卡号就行。
pnpm install express-session --save
const session=require("express-session");
app.use(session(options));
主要方法 : session(options)
通过option来设置session存储,除了session ID外,session中的任何数据都不存储在cookie中。
参数
cookie: {// Cookie Options// 默认为{ path: '/', httpOnly: true, secure: false, maxAge: null }/** maxAge: 设置给定过期时间的毫秒数(date)* expires: 设定一个utc过期时间,默认不设置,http>=1.1的时代请使用maxAge代替之(string)* path: cookie的路径(默认为/)(string)* domain: 设置域名,默认为当前域(String)* sameSite: 是否为同一站点的cookie(默认为false)(可以设置为['lax', 'none', 'none']或 true)* secure: 是否以https的形式发送cookie(false以http的形式。true以https的形式)true 是默认选项。 但是,它需要启用 https 的网站。 如果通过 HTTP 访问您的站点,则不会设置 cookie。 如果使用的是 secure: true,则需要在 express 中设置“trust proxy”。* httpOnly: 是否只以http(s)的形式发送cookie,对客户端js不可用(默认为true,也就是客户端不能以document.cookie查看cookie)* signed: 是否对cookie包含签名(默认为true)* overwrite: 是否可以覆盖先前的同名cookie(默认为true)*/},// 默认使用uid-safe这个库自动生成idgenid: req => genuuid(), // 设置会话的名字,默认为connect.sidname: 'value', // 设置安全 cookies 时信任反向代理(通过在请求头中设置“X-Forwarded-Proto”)。默认未定义(boolean)proxy: undefined,// 是否强制保存会话,即使未被修改也要保存。默认为trueresave: true, // 强制在每个响应上设置会话标识符 cookie。 到期重置为原来的maxAge,重置到期倒计时。默认值为false。rolling: false,// 强制将“未初始化”的会话保存到存储中。 当会话是新的但未被修改时,它是未初始化的。 选择 false 对于实现登录会话、减少服务器存储使用或遵守在设置 cookie 之前需要许可的法律很有用。 选择 false 还有助于解决客户端在没有会话的情况下发出多个并行请求的竞争条件。默认值为 true。saveUninitialized: true,// 用于生成会话签名的密钥,必须项 secret: 'secret',// 会话存储实例,默认为new MemoryStore 实例。store: new MemoryStore(),// 设置是否保存会话,默认为keep。如果选择不保存可以设置'destory'unset: 'keep'
Api
req.session
要存储或访问会话数据,只需使用请求属性 req.session,它以JSON的形式存储序列化,对JS开发非常友好。
如下列代码示例:
const express=require("express");
const session=require("express-session");
const MongoStore = require("connect-mongo");var app=express();//配置中间件
//session会自带一个httpOnly
app.use(session({name: 'session-id',secret: "this is session", // 服务器生成 session 的签名resave: true, //每次是否都刷新到期时间saveUninitialized: true, //强制将为初始化的 session 存储(该session_id是没有用的)cookie: {maxAge: 1000 * 60 * 10,// 过期时间secure: false, // 为 true 时候表示只有 https 协议才能访问cookie},//自动在mongodb中创建一个数据库存储session,并且过期时间也会同步刷新store: MongoStore.create({mongoUrl: 'mongodb://127.0.0.1:27017/ds2_session',ttl: 1000 * 60 * 10 // 过期时间}),})
);// 授权中间件,在这个之后的路由,除了错误处理,都是需要授权的。
app.use((req, res, next) => {//排除login相关的路由和接口(因为login就不需要重定向到login了)if (req.url.includes("login")) {next()return}if (req.session.user) {//重新设置以下sesssionreq.session.mydate = Date.now()//加这个设置才能访问刷新过期时间next()} else {//是接口, 就返回错误码//不是接口,就重定向(因为ajax请求是不能重定向的,只能前端接收错误码做处理)req.url.includes("api")? res.status(401).json({ ok: 0 }) : res.redirect("/login")}
})app.use('/login',function(req,res){//设置sessionreq.session.userinfo='张三';res.send("登陆成功!");
});app.use('/',function(req,res){//获取sessionif(req.session.userinfo){res.send("hello "+req.session.userinfo+",welcome");}else{res.send("未登陆");}
});app.listen(8080);

前端错误处理
update.onclick= ()=>{fetch("/api/user/6257ad33341e112715f25cb5",{method:"PUT",body:JSON.stringify({username:"修改的名字",password:"修改的密码",age:1 }),headers:{"Content-Type":"application/json"}}).then(res=>res.json()).then(res=>{console.log(res)//session验证失败会返回ok:0if(res.ok===0){location.href="/login"}})}
要重新生成会话,只需调用该方法。 完成后,将在 req.session 处初始化一个新的 SID 和 Session 实例,并调用回调。
销毁会话并取消设置 req.session 属性。 完成后,将调用回调。
从存储重新加载会话数据并重新填充 req.session 对象。 完成后,将调用回调。
将会话保存回 store,用内存中的内容替换 store 上的内容。
如果会话数据已更改,则在 HTTP 响应结束时自动调用此方法。
在某些情况下调用此方法很有用,例如重定向、long-lived请求或在 WebSockets 中。
更新 .maxAge 属性。 通常不需要调用,因为会话中间件会为您执行此操作。
以下演示通过销毁session的方式来退出登录
app.use('/login',function(req,res){//设置sessionreq.session.userinfo='张三';res.send("登陆成功!");
});app.use('/loginOut',function(req,res){//注销sessionreq.session.destroy(function(err){res.send("退出登录!"+err);});
});app.use('/',function(req,res){//获取sessionif(req.session.userinfo){res.send("hello "+req.session.userinfo+",welcome to index");}else{res.send("未登陆");}
});app.listen(8080);
当我们进入到主页时,未显示任何信息,进入login路由后,自动设置session,这是回到主页则显示session信息,之后进入loginOut路由已注销session信息,再回到首页显示为hello 张三, welcome to index。
每个会话都有一个与之关联的唯一 ID。 该属性是 req.sessionID 的别名,不能修改。 添加它是为了使会话 ID 可从会话对象访问。
每个会话都有一个唯一的 cookie 对象。 这允许您更改每个访问者的会话 cookie。 例如,我们可以将 req.session.cookie.expires 设置为 false 以使 cookie 仅在用户代理的持续时间内保留。
req.session.cookie.maxAge 将返回以毫秒为单位的剩余时间,我们也可以调整 req.session.cookie.expires 属性,expires是返回Date()对象。安全性上说使用maxAge更好,过期时间是服务器给的,倒计时一过自动就没了。而expires是写死的时间,很容易修改浏览器的时间达到骗过有效期的目的。
属性返回会话 cookie 的原始 maxAge,以毫秒为单位。
要获取已加载会话的 ID,请访问请求属性 req.sessionID。 这只是在加载/创建会话时设置的只读值。
前面提到服务器会保存session,那具体保存在哪里呢?在配置session选项中有个store,如果不指定的话,默认会使用new MemoryStore()保存在内存中。内存有个特点就是断电或服务器重启数据就没了,所以通常我们可以指定其他的store中间件来保存session,比如file-store,或是数据库redis等等。如果要查看默认的store的话,你可以提前先创建一个变量,当store有了名字,就可以后面使用store的api来调用了。
const store = new MemoryStore() // 创建个MemoryStore实例
app.use(session({...store
}))app.use((req, res, next) => {store.get(req.sessionId, (err, session) => {// 这里就可以操作内存中的store数据了。})
})
此可选方法用于将存储中的所有会话作为数组获取。 callback中第一个为error,第二个是sessions。
这个必需的方法用于在给定会话 ID (sid) 的情况下从存储中销毁/删除会话。 callback的对象为error。
此方法用于从存储中删除所有会话.callback的对象为error。
此方法用于获取商店中所有会话的数量。 callback中第一个为error,第二个是len。
这个方法第一个参数为会话 ID (sid) 。 callback中第一个为error,第二个是session。
找不到不会错误,而是在session返回null 或 undefined。
这个方法用于新建或修改session 保存在store中。 callback的对象为error。
这个方法用给定会话 ID (sid) 和会话 (session)来“touch”对应的session。callback的对象为error。
这主要用于当存储将自动删除空闲会话并且此方法用于向存储发出信号给定会话处于活动状态时,可能会重置空闲计时器。
token 的身份验证流程:
token——refresh tokenrefresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。
Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。JWT 认证流程:
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

相同:
区别:
传统Session认证的弊端
我们知道HTTP本身是一种无状态的协议,这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,认证通过后HTTP协议不会记录下认证后的状态,那么下一次请求时,用户还要再一次进行认证,因为根据HTTP协议,我们并不知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在用户首次登录成功后,在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这是传统的基于session认证的过程。

然而,传统的session认证有如下的问题:
JWT认证的优势
对比传统的session认证方式,JWT的优势是:
因为这些优势,目前无论单体应用还是分布式应用,都更加推荐用JWT token的方式进行用户认证
封装方法
//jsonwebtoken 封装
const jwt = require("jsonwebtoken")
const secret = "dselegent"const JWT = {//生成签名//expiresIn是过期时间,例'24h'//value是要传入的数据generate(value,expiresIn){return jwt.sign(value,secret,{expiresIn})},verify(token){try{return jwt.verify(token,secret)//返回的是解析后的token,原始数据+自带的数据构成的对象}catch(e){return false//通过上面按个方法会自动解出是否过期,可是会报错,所以用try-catch}}
}module.exports = JWT
router/login.js
async login(req, res, next) {const { username, password } = req.body;let data = await userService.login({ username, password });//存储数据库//因为存储成功返回的data对象并不是简单的对象,不能直接用,只能取出要用的值if (data) {const token = jwt.generate({id:data._id,username:data.username},"10s")res.header("Authorization",token)//将token设置到响应头res.send({ok: 1});}}
login.html
用户名:密码:
需要token才能进入的页面
用户名:密码:年龄:
名字 年龄
token处理中间件
//node中间件校验
app.use((req,res,next)=>{// 如果token有效 ,next() // 如果token过期了, 返回401错误if(req.url==="/login"){next()return;}//Authorization会变成authorization//链判断运算符如果?前面判断为真就会继续执行后面的,判断为假就不会执行后面//这里因为如果没有token,前面是undefined,去使用undefined是会报错的//如果有token就验证,没token就通过//(直接访问/能通过,但是有个那个页面自动获取数据的axios,在那里就会发送authorization请求头,进入token验证)const token = req.headers["authorization"]?.split(" ")[1]if(token){var payload = JWT.verify(token)//验证成功就生成一个新token重置有效时间,// 验证失败就返回错误码让前端跳到登录页if(payload){const newToken = JWT.generate({id:payload.id,username:payload.username},"1d")res.header("Authorization",newToken)next()}else{res.status(401).send({errCode:"-1",errorInfo:"token过期"})}}
})