前端认证登录鉴权--Session 和 JWT简介

Session 工作原理

在这里插入图片描述
当客户端通过 username 和密码请求服务端的时候,发出了一个 POST 请求.
服务端生成身份认证相关的 session 数据,比如查询一下数据库中该用户名下有何其他信息等, 生成一份 session 数据.

生成的 session 数据可能保存在内存里, 也可能保存在数据库里,redis 里等等,并将 生成的 session-id返回给客户端. 通过 HTTP 一个响应头 Set-Cookie: session = xxx把 session设置成session-id.
客户端则把该 session-id 存在 cookie 中, 因为 Set-cookie 响应头就是一个命令, 把它存在 cookie 中.

之后,所有的请求都附带该 session-id, 比如发了一个 GET 请求,请求了 api/user, 请求里就带了一个 cookie 头, session = 刚才的 session-id
服务器接收到这个请求就会通过这个 session-id寻找session数据, 并解析 session 数据.
这样就完成了登录态. 如果通过校验的话就正常走流程, 如果没有就要重新登录等等

客户端手动清除 cookie 可以实现退出登录, 后端如果希望强制前端重新登录, 可以在服务端把 session 清除

**Tip: 注意区分上面的 session 和 session-id **

Session 的优势

1.相比 JWT, session最大的优势在于可以主动清除 session 了(因为 session 服务端有保存,所以可以主动清除). JWT 则是以 token 的形式保存在客户端, 只要没过期, 客户端可以一直拿着 token 进行认证与授权
2. session 保存在服务器端, 相对较为安全(因为用户可以轻易修改客户端数据,比如 js 修改 cookie, 但是不容易通过严格安全规定的服务器端来修改)
3. 结合 cookie 使用, 比较灵活, 兼容性好

灵活: 客户端和服务端都可以控制登录态. 比如客户端可以登出, 服务端也可以清楚 session.
或者说整个把 session 加密,把整个 session 保存在 cookie 里, 这样不用把 session 保存在内存数据库
兼容性: 指的大多数浏览器支持 session


Session 的劣势

  1. cookie + session 在跨域场景表现不好

因为 cookie 有不可跨域性. 设置 cookie 的时候不光设置 sessionid, 还有一个 domain 变量, 表示 cookie 生效的域名, 只有在这个域名下才生效. 因此跨域表现不好. 可以通过其他方式来进行跨域

  1. 如果是分布式部署, 需要做多机共享 session 机制. session 多数情况下保存在内存里, 一台服务器没问题, 只要不重启, 但是分布部署多服务器就要保证在多台服务器共享和一致, 就增加了成本
  2. 基于 cookie 的机制很容易被 CSRF(跨站请求伪造)

假如某用户登陆过银行网站,保存了 cookie, 如果点击了钓鱼连接, 有安全问题

  1. 查询 session 信息可能会有数据库查询操作

保存在客户端可能仅仅是 sessionid, 而完整 session 信息保存在数据库.


Session 相关概念

session: 主要放在服务器端, 相对安全
cookie: 主要放在客户端, 并不是很安全
sessionStorage: 仅在当前会话下有效, 关闭页面或者浏览器就清除掉了
localstorage: 在客户端存储变量的, 但是除非被清除, 否则永久保存(JWT 通常用 localstorage 来保存生成的 token)


什么是 JWT?

  • JSON Web Token 是一个开放标准(详细内容记录在 RFC 7519 文件中)
  • 定义了一种紧凑且独立的方式, 可以将各方信息作为 JSON 对象进行安全传输
  • 该信息可以验证和信任, 因为经过数字签名的

JWT 的构成

  1. 头部( Header )

Header 本质是个 json, json 里面有两个字段
第一个字段 typ: 代表 token 的类型, 这里固定为 JWT
第二个字段 alg: 使用的 hash 算法, 例如 HMAC SHA256 或者 RSA 等等

  1. 有效载荷( payload )

存储需要传递的信息, 如用户 id, 用户名等等. 还包含元数据, 如过期时间,发布人等等
与 Header 不同, payload 可以加密

  1. 签名(signature)

对头部和载荷部分签名
保证 token 在传输过程中没有被篡改或者损坏
为了加密多了一个秘钥字段


JWT 工作原理

在这里插入图片描述
浏览器端通过 POST 请求传递用户名和密码给服务端, 服务端校验后, 如果成功将用户 id 等其他信息作为 jwt 的有效载荷和头部进行 base64 编码之后形成了一个 jwt, 这段 jwt 就像以点分割的乱码字符串.

然后后端将这个字符串作为登录成功的返回结果和 200 状态码返回给前端, 前端将其保存在 localstorage 和 sessionstorage 中, 每次请求都把这个 jwt 字符串作为 http 头里的 Authorization 加上 Bearer 和 jwt 字符串, 发送给后端, 后端检查是否存在和有效性(例如检查签名正确与否, 令牌过期与否等), 验证通过后后端使用 jwt 中包含的用户信息, 这段用户信息保存在 jwt 的有效载荷中, 验证解密后就拿到用户信息, 进行其他业务逻辑, 返回结果

退出登录的时候就删除这段 jwt字符串就 ok


JWT vs. Session

  • 可拓展性

随着业务增加, 水平和纵向拓展增加必不可少. session 多以redis,数据库等保存在服务器中, 水平拓展方案下需要创建一个独立中心的存储, 否则难以共享. 所以 jwt 要比 session 要好一点, 因为 jwt 可以无缝接入拓展, 因为基于token 令牌的验证是无状态的, 所以不需要在 session 中存储用户信息, 可以使用 token 从不同服务器中访问资源, 不用担心用户是否真的登录到某台服务器上, 不需要专门的 redis 等服务器来存储, 仅是通过令牌来验证. 节约成本. 应用程序方便拓展.

  • 安全性
  1. 对于XSS跨站脚本攻击. js 可以修改 JWT, 因为 JWT 通常放在 localstorage 或 cookie 中, js 可以修改这些变量, 所以也可以修改 JWT, 此时就会出现 xss 攻击, 比如坏人把代码注入页面中.
    如何防范呢?
    可以通过签名, 加密两种方式. 还有不要把敏感信息放在 jwt 中, 防止密钥泄露导致信息泄露
  2. 对于 CSRF, 不管是 jwt 还是 session 的 id, 都可能存在 cookie 里, 只要放 cookie 中, 就很容易受到 csrf. 因此不管是 jwt 还是 session, 都要确保必要的 csrf 保护
  3. 对于重放攻击, 要尽量jwt 和 session-id 的过期时间要尽量的短.
  4. 对于中间人攻击. 使用 https 可以防范,因为在传输期间也是加密的
  • RESTful API 方面

RESTful 架构要求程序应该是无状态的.因此 session 这种有状态的认证方式, 显然违反了 RESTful 的基本限制. 所以要用 jwt

  • 性能

jwt 性能不太好.
因为在客户端向服务端发出请求的时候, 可能有大量的用户信息在 jwt 中, 那么每个 http 请求都产生大量的开销, 而 session 只用少量的开销, 因为 session-id 比较小. jwt 是json, 而且包含了完整的信息, 所以可能是它的好几倍大. jwt 是空间换时间
而 session-id 的缺点是每一个请求都要在服务器上查找 session, 因为拿到的是 id, 没有完整的信息, 要用 id 查完整信息. 所以要消耗性能. 而 jwt 完整信息都在字符串里, 不需要查询

  • 时效性

jwt 时效性差一些
因为 jwt 只有等到过期时间才可以销毁, 而 session 可以在服务端主动销毁.
如果 jwt 存储权限相关信息, 某账号权限被降级, 但是因为 jwt 无法实时更新, 必须要等过期才行. 所以没有办法立即生效.
如果用户发现账号异地登录, 但是由于 jwt 没过期, 异地账号依然可以操作包括修改密码


Nodejs 中使用 jwt

安装 jsonwebtoken
使用这个 npm 包提供的方法:
1. 对 json 对象进行签名生成 token
  - jwt = require('jsonwebtoken');   
  - jwt.sign({
    
    name:'jack'},'mima')   //sign 方法第一个参数是一个 json 对象, 
//第二个参数是个密钥
//然后生成一串两个点分割的字符串. 这就是 token, 可以把它传到客户端.以后客户端的每个请求都
//拿着这个 token 传给服务端. 

2. 逆向操作把 token 进行验证
//然后服务端要进行验证, 使用 verify 方法, 第二个参数是密钥
jwt.verify(token, 'mima')

实现用户注册

  • 再设计用户 Schema
const userSchema = new Schema({
    
    
  name:{
    
    type: String, required: true},
  password:{
    
    type; String, require: true, select: false}
})
新加一个密码规则, 并让其默认在数据库导出不显示
  • 编写保证唯一性的逻辑
创建用户的逻辑中添加逻辑

//获取请求体中的用户名, 用它进行查询
const {
    
     name } = ctx.request.body;
//find 查询出来的是个列表. 而查唯一需要用 findOne 方法
const repeatedUser = await User.findOne({
    
    name});
//然后判断
if(repeatedUser) {
    
    
  ctx.throw(409,'用户已存在');  //409代表冲突
}
//如果用手机,邮箱等判断, 思路一致

实现登录并获取 Token

  • 登录接口设计
登录接口不属于增删改查的一种,可以借鉴 github 设计, 动词加

//先写登录的控制器,并注册
 router.post('/login',login);
 
//实现接口
//做登录首先要把用户名和密码传进来,并校验
async login(ctx) {
    
    
  ctx.verifyParams({
    
    
    name: {
    
    type:'string', required: true},
    password: {
    
    type:'string', require:true}
  })
}

 
  • 用 jsonwebtoken 生成 token
//继续往规则添加逻辑
async login(ctx) {
    
    
  ctx.verifyParams({
    
    
    name: {
    
    type:'string', required: true},
    password: {
    
    type:'string', require:true}
  })
  const user = await User.findOne(ctx.request.body);
  if(!user) {
    
    ctx.throw(401, '用户名或密码不正确');}
  // 拿到 user 以后拿出 id 和 name
  const {
    
    _id, name} = user;
  // 使用 jsonwebtoken 包提供的签名方法,并设置过期时间
  const token = jsonwebtoken.sign({
    
    _id, name}, secret, {
    
    expiresIn: '1d'})
  // 把 token 放在 body 里
  ctx.body = {
    
    token}
}

现在就基本完成了登录接口

手动编写 koa 中间件实现用户认证与授权

  • 认证: 验证 token, 并获取用户信息

这段代码写到中间件里. 因为避免重复代码, 如果写在每个接口控制器里会重复

// 一个中间件就是一个函数
const auth = async(ctx, next) => {
    
    
  // 认证其实就是解析 token,并从中获取用户信息
  // 有很多种传递数据到服务端的方式,比如请求头/请求体/路由参数/querystring, 
  // 在 jwt 认证方式里, 最标准的做法是把 token 放在请求头里, 并且前面加 Bearer

  // 客户端是通过请求头的 authorization 字段把 token 传给服务端
  // 因此获取 token 可以如下方式:
  const {
    
    authorization = ''} = ctx.request.header;
  // 去掉 Bearer 和空格
  const token = authorization.replace('Bearer','');

  try {
    
    
    // 通过 jsonwebtoken 包的方法验证 token
    const user = jsonwebtoken.verify(token, secret);
    // 放置用户信息
    ctx.state.user = user
  } catch (err) {
    
    
    ctx.throw(401,err.message);
  }
  // 正常情况向下执行
  await next()
}

//把 auth 中间件添加到响应接口中, 比如
router.patch('/:id', auth, update)

以后登陆成功后才能请求这些接口
  • 授权: 使用中间件保护接口

每个用户权限不同

现在要完成授权功能, 也就是指定用户能干什么

假设删除接口, 肯定只有自己能删除, 不能删除别人的账号
所以要加授权逻辑
授权建立在认证基础上, 先告诉服务器是谁, 再被授权

写一个中间件检查是否是自己, 把该控制器添加到响应接口
为了提升代码复用性, 这个中间件单独抽离出来


async checkOwner(ctx,next) {
    
    
  // 如果要修改的用户 id !== 当前用户id
  if(ctx.param.id !== ctx.state.user._id) {
    
    ctx.throw(403, '没有权限')}
  await next();
}

//把 授权 中间件添加到响应接口中, 比如
router.patch('/:id', auth, checkOwner, update)

使用 koa-jwt 中间件实现用户认证与授权

动态控制用户信息放哪, 函数动态生成密码等更多功能

  • 安装 koa-jwt
npm i koa-jwt --save
  • 使用中间件保护接口
//引入
const jwt = require('koa-jwt');
//一行搞定 auth 中间件
const auth = jwt({
    
     secret })
//响应接口中添加
router.patch('/:id', auth, update)
  • 使用中间件获取用户信息
这个中间件用户信息也是在 state.user 里

猜你喜欢

转载自blog.csdn.net/m0_48446542/article/details/109126596