为什么需要登录凭证呢?
Web开发中,我们使用最多的协议就是http,但是http是一个无状态的协议
http的每次请求对于服务器来说都是一个单独的请求,和之前请求过什么没有关系,也就是说服务器不知道你上一步做了什么,但如果有一个操作需要证明我们登陆过,那应该怎么办呢?登录凭证就是这个证明我们登录过的东西,常见的登录凭证有两种:
cookie+sessionToken令牌
认识Cookie
Cookie是某些网站为了辨别用户身份而存储在用户本地终端上的数据
- 浏览器会在特定的情况下携带上cookie来发送请求,我们可以通过cookie来获取一些信息
Cookie总是保存在客户端,按其在客户端中的存储位置,Cookie可以分为内存Cookie和硬盘Cookie
- 内存Cookie由浏览器维护,保存在内存中,浏览器关闭时Cookie就会消失,其存在时间是短暂的
- 硬盘Cookie保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时才会被清理
如何判断一个cookie是内存cookie还是硬盘cookie呢?
- 没有设置过期时间,默认情况下cookie是内存cookie,在浏览器关闭时会自动删除
- 有设置过期时间,并且过期时间不为0或者是负数的cookie,是硬盘cookie,需要手动或者到期时,才会自动删除
Cookie常见的属性
cookie的生命周期:
默认情况下(没有设置过期时间时)的cookie是内存cookie,也称之为会话cookie,也就是在浏览器关闭时会自动被删除
我们可以通过给cookie设置expires或者max-age属性来设置过期时间
- expires:设置的是
Date.toUTCString(),设置格式是:expires-date-in-GMTString-format - max-age:设置过期的秒钟:
max-age=max-age-in-seconds,我们设置的时间的单位是秒而不是毫秒(例如一年为60 * 60 * 24 * 365)
cookie的作用域(允许cookie发送给哪些URL)
-
Domain:指定哪些主机可以接受cookie- 如果不指定,那么默认是
origin,不包括子域名。比如我在www.baidu.com这个域名下面添加了cookie,那么如果没有指定Domain,那么用户去到其子域名页面,比如music.baidu.com或者vedio.baidu.com的时候,发送请求时就不会自动携带上cookie - 如果指定了Domain,则子域名的页面请求时也会携带上cookie。例如Domain设置为
mozilla.org,则cookie也包含在子域名中(如developer.mozilla.org等)
- 如果不指定,那么默认是
-
Path:指定主机下哪些路径可以接受cookie- 例如,设置
Path='/docs',则以下地址都会匹配: /docs/docs/Web//docs/Web/HTTP
- 例如,设置
设置Cookie
客户端
必须要是线上环境才可以设置cookie,所以本地测试的时候要用127.0.0.1打开html页面;下面我就是在客户端中设置了一个key为name,value为lilei的cookie,并设置其10s中之后过期,注意:max-age设置的是秒而不是毫秒
document.cookie = 'name=lilei;max-age=10;'
服务端
在koa框架中,我们可以通过上下文ctx对象的cookies属性,通过set方法即可给浏览器设置cookie,浏览器接受到我们的响应之后就会自动将我们所设置的cookie存储到浏览器中
在koa框架中,http请求携带的cookie会被放到ctx.cookies对象中,我们可以调用该对象的get方法获取到客户端在请求时所携带的cookie
注意:在服务器端设置maxAge属性时单位为毫秒
authRouter.get('/', (ctx, next) => {
// 服务端为客户端设置cookie
ctx.cookies.set('name', 'lileid', {
maxAge: 10 * 1000
})
ctx.body = 'haha'
})
authRouter.get('/test', (ctx, next) => {
// 读取http请求中的cookie
const value = ctx.cookies.get('name')
ctx.body = `你的name是${value}`
})
Session
在koa中,我们可以借助koa-session来实现session认证,而session其实是依赖cookie实现的
const app = new Koa()
const session = koaSession({
key: 'sessionid', // 作为cookie的key
maxAge: 60 * 60 * 60 * 1000, // 过期时间,单位和客户端存cookie不同,其单位是ms
httpOnly: true, // 不允许客户端通过js获取cookie
rolling: true, // 每次响应时,刷新session的有效期
signed: true // 默认值就为true,是否使用signed签名认证,防止数据被客户端篡改或伪造
}, app)
app.keys = ['secret'] // 如果需要使用签名,则app.keys不能省略,但里面的内容可以随便填,其作用是将 cookie 的内容通过密钥进行加密
// 注册了session中间件之后,其会自动解析请求中的cookie,把相关信息添加到ctx.session中去
app.use(session)
app.use((ctx, next) => {
// 设置session,客户端会通过cookie的方式存储我们设置的数据(存储前进行了base64编码)
ctx.session.user = {
id: 110,
name: '小明'
}
ctx.body = 'session设置成功~'
})
app.use((ctx, next) => {
const user = ctx.session.user
console.log(user); // 如果验证通过则就是我们传递到客户端的值{ id: 110, name: 'why' },如果不通过则拿到的是undefined
ctx.body = user
})
在浏览器的cookie中,sessionid代表的就是我们传递过去的数据,而sessionid.sig就是我们对应的签名,如果客户端修改了对应的数据,服务端那边校验不通过就拿不到对应的结果,自然就不让其访问对应的资源
认识Token
cookie和session方法的缺点:
Cookie会被附加在每个HTTP请求中,所以无形中增加了流量(事实上某些请求是不需要的)- Cookie是明文传递的,所以存在安全性的问题
- Cookie的大小限制是
4KB,对于复杂的需求来说是不够的 - 对于浏览器外的其他客户端(比如
IOS、Android),必须手动的设置cookie和session - 对于分布式系统和服务器集群中还需要考虑如何保证其他系统或服务器中也可以正确的解析session
所以,在目前的前后端分离的开发过程中,使用token来进行身份验证是最多的情况
- token可以翻译为令牌
- 也就是在验证了用户账号和密码正确的情况后给用户办法的一个令牌
- 这个令牌作为后续用户访问一些接口或者资源的凭证
- 我们可以根据这个凭证来判断用户是否有权限来访问对应的资源
所以token的使用应该分成两个重要的步骤:
- 生成token:登录的时候颁发token
- 验证token:访问某些资源或者接口时验证token
服务器集群
当你开发的项目受众比较多,或者是包含了即时通讯的项目,也就是说服务器可能需要接收高并发的请求,其承受的压力就会非常大
服务器集群就是我们可以使用多台服务器,每一台服务器上面都部署类似代码的项目,然后再通过nginx等方式做一个反向代理,请求的是nginx的服务器,但是其会将请求转发到服务器资源较为空闲的服务器中去,相当于其做了一个分发操作,这样就可以减少某一台服务器的压力,从而可以承受更多的并发量
但假如你的session是服务器1分发的,如果下次访问的是服务器2,还需要想办法如何去正确解析服务器分发的session
分布式系统
一台服务器里面不仅有一个系统,比如用户管理系统、商品信息系统、用户订单系统等等,如果受众比较广,一般的企业是不会将所有的系统放到一个服务器里面的
分布式相当于就是对系统进行拆分并放到不同的服务器中,如果我们的session是用户管理系统颁发的,那么在其它的系统中也需要想办法如何正确解析这个session
JWT实现Token机制
header
alg:采用的加密算法,默认是HMAC SHA256(HS256),这是一种对称加密,其采用同一个密钥进行加密和解密typ:JWT,固定值,通常都写成JWT即可- 会通过
base64Url算法进行编码
payload
- 携带的数据,比如我们可以将用户的
id和name放到payload中 - 默认也会携带
iat(issued at),令牌的签发时间 - 我们也可以设置过期时间:
exp(expiration time) - 会通过
base64Url算法进行编码
signature
- 设置一个
secretKey,通过将前两个的结合合并后进行HMACSHA256的算法 HMACSHA256(base64Url(header) + . + base64Url(payload), secretKey)- 但是如果
secretKey暴露是一件非常危险的事情,因为之后就可以模拟颁发token,也可以解密token
非对称加密
前面我们说过,使用HS256加密算法进行token颁发和解密的话,一旦密钥暴露就是非常危险的事情
- 比如在分布式系统中,每一个子系统都需要获取到密钥
- 那么拿到这个密钥后这个子系统均可以发布密钥,也可以验证令牌
- 但是对于一些资源服务器来说,他们只需要有验证令牌的能力就可以了
这个时候我们可以使用非对称加密算法:RS256:
- 私钥(
private key):用于发布令牌 - 公钥(
public key):用于验证令牌
私钥和公钥可以通过openssl这个工具生成,而且公钥是要通过私钥生成的,在mac操作系统上是自带这个工具的,但是在windows操作系统上,是不可以通过cmd进入到openssl的命令行的,但是我们可以使用git bash直接使用openssl
相对路径问题补充
在项目中的任何一个地方的相对路径,并不是相对于当前文件,而是相对于全局对象process下的cwd函数的返回值,其对应的就是你启动项目时所在的文件路径
比如说下面的文件是想要到项目根目录下寻找keys文件夹的,但由于相对路径是相对于项目启动的文件夹也就是coderhub,所以找的是coderhub的上两层目录下的keys文件夹中的private.key文件,自然是找不到的。所以项目开发的时候要特别注意,最好在找文件的时候使用绝对路径
// codehub/src/index.js
const PRIVATE_KEY = fs.readFileSync('../../keys/private.key')