身份认证 ( Session、JWT )

身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
 

认证 / 授权

认证(Authentication):验证目标对象身份。比如,通过用户名和密码登录某个系统就是认证。

授权(Authorization):给予通过验证的目标对象操作权限。

 
不同开发模式下的身份认证
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
① 服务端渲染推荐使用 Session 认证机制
② 前后端分离推荐使用 JWT 认证机制

JWT应用场景:一次性验证

 

Session 认证机制
1. HTTP 协议的无状态性
HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会
主动保留每次 HTTP 请求的状态。
 
2. Cookie
Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用
于控制 Cookie 有效期、安全性、使用范围的可选属性组成。
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。
Cookie的几大特性:
① 自动发送
② 域名独立
③ 过期时限
④ 4KB 限制
 
3. Cookie 在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动
将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给
服务器,服务器即可验明客户端的身份。
 
4. Cookie 不具有安全性
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全
性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
 
Cookie存储在浏览器中,很容易被伪造,不具有安全性.
千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等
 
6. Session 的工作原理

首先客户端发起请求,服务端验证请求的合法性,把请求的信息保存到服务器上,并以响应头的形式向浏览器发送cookie,用以保存sessionId;

客户端下次发起请求时自动携带着cookie中的sessionId来验证身份,在服务器上核对身份之后对请求作出响应。

 

session需要把id以cookie的形式放在客户端,所以session依赖于cookie

 

Express使用session


首先安装express-session中间件:npm install express-session

// 1.配置 Session 中间件
const session = require('express-session');
app.use(session({
  //secret属性的值可以为任意字符串   secret:
'keyboard',
  //固定写法     resave:
false,
  //固定写法   saveUninitialized: true
, }) ) // 保存session信息 req.session.user = req.body // 用户的信息

// 读取session信息 req.session.user.username
// 清空 Session 信息 req.session.destroy()

 

Session 认证的局限性
Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接
口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
 
⚫ 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
⚫ 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
 

session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,

通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,

这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

 

因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

 

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息

。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,

另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了

Access-Control-Allow-Origin: *

 
JWT 认证机制( Json web token )
 
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案
工作原理:
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。
 
三者之间使用英文的“.”分隔
 
格式:
 
 
JWT 字符串的示例:
 
JWT 的三个部分各自代表的含义
JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:
⚫ Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
⚫ Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性
 
 

header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
  • 完整的头部就像下面这样的JSON:
    {
      'typ': 'JWT',
      'alg': 'HS256'
    }

    然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

     

playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "123456789",
  "name": "eee",
  "admin": true
}

 

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

 

signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.

连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

 

将这三部分用.连接成一个完整的字符串,构成了最终的jwt

  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
 
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证所以,
它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
 
 
JWT 的使用方式
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP
请求头的 Authorization 字段中,
格式: 
Authorization:bearer <token>
 
在 Express 中使用 JWT
1. 安装 JWT 相关的包
运行如下命令,安装如下两个 JWT 相关的包:
 
npm install jsonwebtoken express-jwt
其中:
⚫ jsonwebtoken 用于生成 JWT 字符串
⚫ express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
 
2. 导入 JWT 相关的包
使用 require() 函数,分别导入 JWT 相关的两个包:

分别是 jsonwebtoken 和 express-jwt

const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

例:

// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()

//1:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

// 允许跨域资源共享
const cors = require('cors')
app.use(cors())

// 解析 post 表单数据的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))

//2:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'itheima No1 ^_^'

// 4:注册将 JWT 字符串解析还原成 JSON 对象的中间件
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))

// 登录接口
app.post('/api/login', function (req, res) {
  // 将 req.body 请求体中的数据,转存为 userinfo 常量
  const userinfo = req.body
  // 登录失败
  if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
    return res.send({
      status: 400,
      message: '登录失败!',
    })
  }
  // 登录成功
  // 3:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
  // 参数1:用户的信息对象
  // 参数2:加密的秘钥
  // 参数3:配置对象,可以配置当前 token 的有效期
  // 记住:千万不要把密码加密到 token 字符中
  const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
  res.send({
    status: 200,
    message: '登录成功!',
    token: tokenStr, // 要发送给客户端的 token 字符串
  })
})

// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
  // 5:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
  console.log(req.user)
  res.send({
    status: 200,
    message: '获取用户信息成功!',
    data: req.user, // 要发送给客户端的用户信息
  })
})

//6:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
  // 这次错误是由 token 解析失败导致的
  if (err.name === 'UnauthorizedError') {
    return res.send({
      status: 401,
      message: '无效的token',
    })
  }
  res.send({
    status: 500,
    message: '未知的错误',
  })
})

// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(8888, function () {
  console.log('Express server running at http://127.0.0.1:8888')
})

 

优点

  • 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
  • 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
  • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
  • 它不需要在服务端保存会话信息, 所以它易于应用的扩展

安全相关

  • 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
  • 保护好secret私钥,该私钥非常重要。
  • 如果可以,请使用https协议
 
缺点
 

 无法满足注销场景

  传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。

但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验有效性。

没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。

 

 无法满足修改密码场景

  修改密码则略微有些不同,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret)之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,

所以仅仅清空 cookie 自然是不够的,这时,需要强制性的修改 secret。

 

 无法满足token续签场景

  我们知道微信只要你每天使用是不需要重新登录的,因为有token续签,因为传统的 cookie 续签方案一般都是框架自带的,

session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。

但是 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,

而 jwt 想延期这个 exp 就有点身不由己了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签

 

 
posted @ 2020-09-02 15:34  ℳℓ马温柔  阅读(1025)  评论(0编辑  收藏  举报