前端认证授权那些事儿

认证授权在业界已经有很多成熟的方案,但对于前端开发来说,大部分情况都是调用服务端提供登录接口完成认证,后续请求带上对于 token 即可。相信不少公司还会开发 JS SDK,让各个业务项目都能快速接入认证授权,所以认证授权的细节流程我们可能并没有关注过。

本文将从前端视角介绍常见的认证授权方案,同时提供简单的实践来体验流程。

概念介绍

认证

认证 (Identification) 是验证当前用户的身份。

常见的认证技术:

  • 身份证
  • 用户名和密码
  • 用户手机:手机短信、手机二维码扫描、手势密码
  • 用户的电子邮箱
  • 用户的生物学特征:指纹、语音、眼睛虹膜

授权

授权 (Authorization) 指赋予用户系统的访问权限。认证完用户身份后,系统会授予用户部分或者全部权限。系统要是没有权限控制需求的话,一般认证后用户就有全部权限。

实现授权的方式有:

  • cookie
  • session
  • token
  • OAuth

鉴权

鉴权 (Authentication) 是指系统鉴定用户身份和权限。比如系统需要鉴定 session/cookie/token 的合法性和有效性。

认证、授权和鉴权关系

这三个概念的关系也是很清晰,就是一个前后依次发生的关系:认证 => 授权 => 鉴权。比如我们登录某个系统就完成了认证和授权,后续使用功能时就需要系统鉴权。

了解相关概念后,就可以开始介绍常见的认证授权方案。

认证授权方案

HTTP 基本认证

基本认证 (Basic 认证) 是 HTTP/1.0 就定义的认证方式,主要通过用户提供用户名和密码的方式,实现对用户身份的验证。

基本认证流程图

前端认证授权那些事儿

认证步骤解析

  1. 浏览器请求受保护的资源
  2. 服务器返回 401,同时响应头带上 WWW-Authenticate: Basic realm=protected_docs,realm 代表资源的安全域,我们资源可能权限不同,放在不同的安全域中
  3. 浏览器弹窗请求用户账号密码,用户输入后,浏览器 Base64 编码账号密码,再次请求,请求头带上 Authorization: Basic Base64(账号:密码)
  4. 服务器认证账号密码,认证成功返回对应资源,认证失败返回 403 forbidden

node 的简单实现

const express = require('express')
const app = express()

const protectedPath = '/protected_docs'
const realms = {
  [protectedPath]: {
    users: ['root'],
  },
}

app.get(protectedPath, (req, res, next) => {
  const realm = realms[req.path]
  const authorization = req.get('authorization')

  if (!authorization) {
    // 告知用户需要身份认证
    res.statusCode = 401
    res.set('WWW-Authenticate', 'Basic realm=' + encodeURIComponent(realm))
    res.end()
    return
  }

  const usernamePasswd = authorization.split(' ')[1] // Basic Y2h5aW5ncDoxMjM0NTY
  const [usrname, passwd] = Buffer.from(usernamePasswd, 'base64')
    .toString()
    .split(':')

  if (!realm.users.includes(usrname)) {
    // 用户不在realm里
    res.statusCode = 401
    res.set('WWW-Authenticate', 'Basic realm=' + encodeURIComponent(realm))
    res.end()
    return
  }
  const isValid = usrname === 'root' && passwd === '123456'

  if (!isValid) {
    // 用户账号、密码验证不通过
    res.statusCode = 403
    res.end()
    return
  }

  res.end(`welecom ${usrname}`)
})

app.listen(3000)

小结

优点:

  1. 简单,基本所有浏览器都支持

缺点:

  1. 不安全,HTTP 上传输,密码只是 Base64 编码,可以被解码。
  2. 无法主动注销,除非标签页或浏览器关闭、或用户清除历史记录,认证信息一直存在。

使用场景:

  1. 内部网络,或者对安全要求不是很高的网络。比如公司内网 wiki

这边提一下,HTTP1.1 针对基本认证缺点,提供了摘要认证(Digest 认证),原理简单来说就是服务端会给浏览器一个 nonce 随机数,浏览器会将账号、密码和 nonce 等参数进行 md5 加密后传给服务端(同时传账号数据,密码不传),服务端获取到账号后,从数据库拿密码同样进行 md5 加密,加密后值和浏览器传的一样就认为认证成功。

摘要认证不再明文传输密码、可以防重放和避免报文被篡改,但是需要和 Https 配合使用。

Session-Cookie

Session-Cookie 认证是利用服务端的 Session(会话)和浏览器(客户端)的 Cookie 来实现的前后端通信认证模式。

什么是 Cookie

HTTP 是无状态协议,服务端在接收到客户端首次请求后,设置对应的 Cookie,随后浏览器在请求带上 Cookie,服务端就可以知道当前客户端状态。

Cookie 的特点:

  • Cookie 存储在客户端,可能被修改,不安全
  • 有大小限制,一般是 4KB
  • Android 和 IOS 对 Cookie 支持不好
  • Cookie 有跨域限制

什么是 Session

Session 的抽象概念是会话,是无状态协议通信过程中,为了实现中断/继续操作,将用户和服务器之间的交互进行的一种抽象。

Session 一般流程是:服务端接收到客户端首次请求后,设置一个 Session 来跟踪用户的会话,同时会给客户端一个 Session ID,后续客户端请求时在带上 Session ID,服务端即可找到对应的 Session,此时双方通信就是有状态的。

Session 特点:

  • Session 数据保存在服务端,安全性高
  • Session 数据大小可以超过 4KB,存储数

Session-Cookie 流程

Session 流程中一般会设置 Session ID,通常 Session ID 会保存在 浏览器 Cookie 中,接下来看下整体流程。

前端认证授权那些事儿

认证步骤解析

  1. 浏览器发送登录请求
  2. 服务端校验账号密码,校验通过后生成 Session 和 Session ID,Session 保存在 Session 服务器中(一般保存在内存或者 Redis 服务器)。随后返回数据给浏览器,同时设置一个 Session ID 的 Cookie
  3. 浏览器请求资源,一般会自动带上 Session ID 的 Cookie
  4. 服务端获取到 Session ID,通过 Session 服务器校验 Session,Session 服务器校验成功,服务端处理数据逻辑
  5. 服务端返回数据给浏览器

简单代码示例

const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const port = 3000;
const session = require("express-session");

app.use(bodyParser());
app.use(
  session({
    key: 'SESSION_ID',
    secret: "your_secret_key",
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 1000 * 60 * 60 * 8, signed: true },
  })
);

app.get("/", async function (req, res) {
  res.send(req.session);
});

app.get("/login", async function (req, res) {
  const authInfo = {
    id: '1',
    username: 'user',
  }
  const isValid = true
  if (isValid) {
    req.session.authInfo = authInfo
    res.send({
      success: true,
      info: "登录成功",
    });
  } else {
    res.send({
      success: false,
      info: "登录失败",
    });
  }
});

app.listen(port, () => {
  console.log(`node listening at http://localhost:${port}`);
});

使用 node 起好服务器后,先访问 /login,在访问首页 /, 可以看到首页输出用户名。同时打开 F12,在 Cookie 中可以看到 SESSION_ID 的数据。

小结

优点:

  1. Cookie 简单易用
  2. Session 数据保存在服务端,安全
  3. 支持 Session 管理,能主动注销 Session
  4. 只需后端操作,前端无感

缺点:

  1. 依赖 Cookie,禁用 Cookie 情况下无法使用
  2. 因为使用了 Cookie ,所以可能有 CSRF 攻击
  3. Session 存在服务端,用户量大的情况下,服务端开销增加,性能会下降
  4. 对移动端的支持性不友好

使用场景:

  1. 一般中大型的网站都适用

Token

上述介绍中,我们知道了 Session-Cookie 的一些缺点,及 Session 的维护给服务端造成很大困扰,必须找地方存放它,又要考虑分布式的问题,所以 Token 方案就出来了。

什么是 Token

Token 是一个令牌,客户端访问服务器时,验证通过后服务端会为其签发一张令牌,之后,客户端就可以携带令牌访问服务器,服务端只需要验证令牌的有效性即可。

一般 Token 的组成:

uid(用户唯一的身份标识) + time(当前时间的时间戳) + sign(签名,Token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

Token 认证流程

前端认证授权那些事儿

认证步骤解析

  1. 客户端发送登录请求
  2. 服务端校验账号密码,生成 Token,并返回给客户端
  3. 收到 Token 以后需要把它存储起来,web 端一般会放在 localStorage 或 Cookie 中
  4. 客户端请求 API 资源的时候,将 Token 通过 HTTP 请求头 Authorization 字段或者其它方式发送给服务端
  5. 服务端拿到 Token,做解密和签名校验,通过校验返回数据,否则返回 401

Token的优缺点

优点:

  • 服务端无状态化、可扩展性好:Token 自身包含了其所标识用户的相关信息,这有利于在多个服务间共享用户状态
  • 安全性好,可以避免 CSRF 攻击
  • 支持跨域调用

缺点:

  • 性能问题,服务端需要对 Token 加解密等操作,所以会更耗性能
  • 有效期短:为了避免 Token 被盗用,一般 Token 的有效期会设置的较短,所以就有了 Refresh Token
  • 相比于 Session-Cookie,需要前后端配合处理

Refresh Token

业务接口用来鉴权的 Token,我们称之为 Access Token,为了安全性,Access Token 有效期一般设置的比较短。Access Token 过期后,需要用户重新登录,但是这种体验较差。

所以有了 Refresh Token, 可以用 Refresh Token 去获取 Access Token。

  • Access Token:用来访问业务接口,由于有效期足够短,盗用风险小。

  • Refresh Token:用来获取 Access Token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性。

Refresh Token 的使用流程是在服务器校验 Token, 发现过期后,客户端可以使用 Refresh Token 发起请求,获取新的 Access Token 和 Refresh Token。

JSON Web Token(JWT)

上述 Token 中,一般只有 uid 信息,需要更多登录信息和其他数据的话,这时就需要查询数据库。每次都需要查询数据库,就会带来一些性能消耗。所以业界常用的 JWT 方案就出来了。

JWT 是 Auth0 提出的通过对 JSON 进行加密签名来实现授权验证的方案, 它的特点是自包含的,用户信息和认证是在一起的,无需像 Cookie-Session 一样需要 Session 服务器,或者像 Token 一样访问数据库获取用户信息。

JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:

  • Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
  • Payload : 用来存放实际需要传递的数据,JWT 规定了 7 个官方字段,比如 iss、exp 等等,还可以自定义数据。
  • Signature(签名):服务器通过 Payload、Header 和一个密钥 (Secret) 使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

JWT 通常是这样的:xxxxx.yyyyy.zzzzz。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJuYW1lIjoieGlhb21pbmciLCJkYXRhIjoiPT09PT09PT09PT09PSIsImlhdCI6MTY3OTgwNDA1NywiZXhwIjoxNjc5ODA0MTE3fQ
.FdJ6UD4Ff5zOz83f4hRDh1C86kN5f8aO_KeEtIwt3cM

JWT 认证流程

前端认证授权那些事儿

其实 JWT 的认证流程与 Token 的认证流程差不多,只是不需要再单独去查询数据库查找用户信息。

JWT 实例

const { expressjwt: jwt } = require('express-jwt')
const express = require('express')
const app = express()
const jsonwebtoken = require('jsonwebtoken')

const secretOrPrivateKey = 'hello' //加密token 校验token时要使用
app.use(
  jwt({
    secret: secretOrPrivateKey,
    algorithms: ['HS256'],
  }).unless({
    path: ['/getToken'], //除了这个地址,其他的URL都需要验证
  })
)

app.use(function (err, req, res, next) {
  if (err.name === 'UnauthorizedError') {
    res.status(401).send('invalid token...')
  }
})

app.get('/getToken', function (req, res) {
  res.json({
    result: 'ok',
    token: jsonwebtoken.sign(
      {
        name: 'xiaoming',
        data: '=============',
      },
      secretOrPrivateKey,
      {
        expiresIn: 60 * 1,
      }
    ),
  })
})

app.get('/getData', function (req, res) {
  res.send('data')
})

app.listen(3000)

服务启动后,访问 /getToken 获取 JWT,然后在 Postman 中请求 /getData, Header 部分加上 Authorization: Bearer your jwt,比如 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb21pbmciLCJkYXRhIjoiPT09PT09PT09PT09PSIsImlhdCI6MTY3OTgwNjkyOCwiZXhwIjoxNjc5ODA2OTg4fQ.qFOT9IS_T1ZNsWWheRXP9MxYPh1l3SBGWLtp8ocnKAE

JWT 的优缺点

优点:

  • 不需要在服务端保存会话信息,所以易于应用的扩展
  • JWT 中的 Payload 负载可以存储常用信息,用于信息交换,有效地使用 JWT,可以降低服务端查询数据库的次数

缺点:

  • 请求头体积较大,Payload 数据量大的时候,请求头体积也会对应增大
  • JWT 默认是不加密,所以 Payload 一般不放敏感信息,不过也可以再次加密。
  • 到期问题,JWT 一旦签发后,除非到了过期时间,不然会一直有效,服务端无法主动注销掉。

使用场景:

  • 一般对安全要求不高,对负载要求高的场景都会可以使用 JWT,安全敏感的场景推荐使用 token + redis
  • 单点登录

Oauth 2.0

OAuth 这种方式登录相信大家都使用过,比如我们想登录某个网站时,通常会发现可以通过第三方的 QQ 或者微信登录,这个就是使用到了 OAuth。

OAuth 是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站。常见的提供 OAuth 认证服务的厂商:支付宝、QQ、微信、微博

OAuth 认证流程

前端认证授权那些事儿

认证步骤解析

  1. 客户端发起使用第三方登录,服务端携带 client_id 直接重定向到授权服务器登录页面,授权服务器要求客户端登录。
  2. 客户端完成第三方登录并同意授权后,授权服务器重定向到服务端,并携带授权码 code,服务器携带 client_id, client_secret, code 向授权服务器请求令牌。
  3. 授权服务器通过认证,并返回令牌
  4. 服务端用令牌向授权服务器请求用户基本信息,授权服务器认证令牌通过后返回数据
  5. 服务端返回用户信息给客户端

使用 github 登录示例

准备工作:

1、创建 OAuth App

前端认证授权那些事儿

2、填写基本信息

前端认证授权那些事儿

3、获取 client_idclient_secret

前端认证授权那些事儿

代码示例

const express = require('express')
const app = express()
const querystring = require('querystring')
const axios = require('axios')

// GitHub登录参数配置;配置授权应用生成的Client ID和Client Secret
const config = {
  client_id: 'xxx',
  client_secret: 'xxxxxx'
}

// 登录接口
app.get('/github/login', function (req, res) {
  // 重定向到GitHub认证接口,并配置参数
  let path = 'https://github.com/login/oauth/authorize?client_id=' + config.client_id
  // 转发到授权服务器
  res.redirect(path)
})

// GitHub授权登录成功回调,地址必须与GitHub配置的回调地址一致
app.get('/passport/github/callback', async function (req, res) {
  console.log('callback...')

  // 服务器认证成功,回调带回认证状态code
  const code = req.query.code
  const params = {
    client_id: config.client_id,
    client_secret: config.client_secret,
    code: code
  }

  // 申请令牌token
  let tokenRes = await axios.post('https://github.com/login/oauth/access_token', params)
  const access_token = querystring.parse(tokenRes.data).access_token

  // 根据token获取用户信息
  userRes = await axios.get(`https://api.github.com/user`, {
    headers: {
      'Authorization': 'token ' + access_token
    }
  })

  // 渲染页面
  res.end(`
    <h1>Hello ${userRes.data.login}</h1>
    <img src="${userRes.data.avatar_url}" alt="">
  `)
})

app.listen(7001, () => {
  console.log('listening port at 7001...')
})

服务启动后,访问 /github/login,后续会跳转 github 登录授权,完成后即可看到你的 github 用户名和头像。

总结

登录的认证授权方式有很多,上述讲到的都是一些常见的方案,当前方案的具体细节实施各个项目方还是会有差异的。

除了上述的内容后,认证授权这块还有单点登录(SSO)、扫描登录、手机号一键登录等等方式,后续有机会在讲。

本文正在参加「金石计划」

原文链接:https://juejin.cn/post/7214665617311875132 作者:晓得迷路了

(0)
上一篇 2023年3月26日 下午4:05
下一篇 2023年3月26日 下午4:16

相关推荐

发表回复

登录后才能评论