鉴权

作者: hellomyshadow | 来源:发表于2020-08-19 14:33 被阅读0次

常见的鉴权方式:Session/Cookie、Token(JWT)、OAuth、SSO

Session/Cookie

Session基于CookieSession存在服务端,Cookie存在客户端。

// 服务端 - Node
const http = require('http');
http.createServer((req, res) => {
    req.headers.cookie  // 获取客户端携带的Cookie
    res.setHeader('Set-Cookie', 'cookie1=abc;')  // 让客户端设置Cookie
    res.end()
}).listen(3000)

客户端收到的Response Headers:

   Set-Cookie: cookie1=abc;

客户端再次请求时,会在 Request Headers 中携带 Cookie

   Cookie: cookie1=abc

Cookie的容量有限,且容易被篡改,不安全,所以出现了Session
在客户端的Cookie中,存储一个随机的Session IdSession信息存储在服务端

// 模拟实现Cookie-Session模式
let session = {}, sessionKey = 'sid'
http.createServer((req, res) => {
    const cookie req.headers.cookie
    if(cookie && cookie.indexOf(sessionKey) !== -1) {
        res.end('Come Back!')
        // 存在,取出Session
        const pattern = new RegExp(`${sessionKey}=(^;]+);?\s*`)
        const sid = pattern.exec(cookie)[1]
        session[sid]  // Session
    } else {
        // 不存在Cookie-Session
        const sid = (Math.random() * 999999).toFixed()
        // 设置Cookie
        res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
        session[sid] = { name: 'Jack' }
        res.end('Hi, Jack')
    }
}).listen(3000)

Session会话机制是一种服务端机制,它使用类似哈希表的结果来保存信息。

在客户端首次访问服务器时创建Session,并为这个Session生成一个唯一的表示字符串sid,作为键。如果需要,还会通过密匙对 sid 进行签名处理,避免客户端篡改。把 sid 作为 Cookie 返回给客户端。客户端在随后的HTTP请求中会携带这个sid,服务端通过这个sid查询客户端的Session信息。

哈希 Hash

  1. 哈希 Hash 的特点
    • 把一个不定长摘要定长结果
    • 摘要 xialaoshi -> x4sdfdsafsdafl3s3 -- 按照某种规则进行摘要,防篡改
    • 雪崩效应:牵一发动全身,如 xialaoshizialaoshi 虽然只有一个字母不同,但摘要的结果完全不同,让人完全找不到规律。
  2. 常见哈希算法:MD5、SHA1、hmac
  3. 摘要(加密算法)
    • 对称DES
    • 非对称RSA
  4. koa-session 中启用签名
const Koa = require('koa'), session = require('koa-session')
const app = new Koa()
app.keys = ['some secret']  //用于对cookie签名的key
const SESS_CONFIG = {
    key: 'ply:sess',  // cookie的键名
    maxAge: 86400000,
    httpOnly: true,  // 仅允许服务器修改
    signed: true  // 启用签名
}
app.use(session(SESS_CONFIG, app))

Session通常存在于内存、Redis等介质中,但分布式部署无法共享内存,所以大多数情况下存储于Redis中。

Token 令牌

Token是一个独立的概念,是通过认证后发放的某个 凭证,这个凭证可以通过Cookie、自定义http请求头(如X-Token),URLquery进行传递。所以Token不存在跨域、跨平台的问题。

  • 常见加密形式(携带信息):用户Id + 过期时间 + 签名
  • Token需要服务端查库验证是否有效,通常存储于Redis中。

JWT - 服务端无状态化

JWT Json Web Token,是Token的子集,是一种规则,采用Json格式以及附带一些规范约束,将用户信息加密到 Token 中。服务器不保存任何用户状态信息(无需查库),而是通过保存的密匙校验 Token,也就是说 服务端是无状态的,即使服务重启也不会有影响。

JWT的加密过程(原理) 包括三部分:头部header,载荷payload,签名signature,用 . 分隔。

  • Header 用于描述元信息,通常包括Token类型和Signature的加密算法
    {
       "alg": "HS256",  // 默认的哈希(加密)算法
       "typ": "JWT"     // Token类型
    }
    
    经过 base64 编码,得到第一部分
  • Payload 载荷信息,包含一些声明Claim(实体描述,通常是一个用户信息,还有一些其他的元数据),即携带希望向服务器发送的信息。
    这些信息可以是官方字段如iss(Issuer)、sub(Subject)、exp(Expiration time),也可以是自定义字段如userId,data
    {
       "iss": "JWT Builder",  // 该JWT的签发者
       "sub": "aaa@example.com",     // 该JWT所面向的用户
       "exp": 1448333419,     // 过期时间
       "userId": "b08f86af-35da-48f2-8fab-cef3904660bd"  // 用户Id
    }
    
    经过base64编码,得到第二部分
  • Signature
    第一部分 + "." + 第二个部分 拼接成字符串,通过 Header 中声明的加密方式进行加盐secret组合加密,得到第三部分。
    加盐secret 也就是服务端提供的私钥,防篡改。解密Token时仅仅使用设定的 secret,无需查询数据库,所以secret一定不能泄露!!!
    一旦客户端得到这个secret,那就意味着客户端可以自我签发JWT了!

第一部分 + "." + 第二个部分 + "." + 第三个部分 = JWT

由此可知,JWT(Token) 并不是为了隐藏或保密数据,而是为了确保它是被授权的,所以其中不能放诸如用户密码等 敏感信息。

// 前端
axios.interceptors.request.use(config => {
    const token = window.localStorage.getItem('token')
    if(token) {
        // 通常约定 Bearer 是 JWT 的认证头部
        config.headers.common['Authorization'] = 'Bearer ' + token
    }
    return config
}, err => Promise.reject(err))

Node Koa 后端:

  • koa、koa-router、koa-bodyparser、koa-static
  • jsonwebtoken、koa-jwt、koa2-cors(跨域)

对比

  • Cookie-Session 模式会受跨域、分布式部署、服务重启的影响;
  • Token + Redis 模式需要每次都查询数据库,验证Token是否有效;
  • JWT 是一种认证授权机制,服务端无状态(无需存储),也就不能主动清除,不能踢离线。

OAuth

随着大量开放平台的出现,建立在开放平台之上的各种第三方应用也在大量冒出。出于对安全性和统一标准的要求,就有了OAuth协议。

简单来说,OAuth是一种开放的协议,一种授权机制,它能为第三方应用提供一种简单的标准方式去访问需要用户授权的API服务。而且在为第三方提供服务的过程中,能够保护用户账号的安全。

2007年OAuth1.0,存在严重漏洞。2009年6月OAuth1.0 Revision A,修复了前一版本的安全漏洞。
2010年4月OAuth2.0,与1.0互不兼容。OAuth 2.0也是目前最流行的授权机制,授权第三方应用,获取用户数据。

举个栗子:
我们想做一个关于新浪微博的第三方应用,那么就需要新浪微博提供一系列API,但前提是要去授权验证,这就用到OAuth了。作为第三方开发者,我们并没有得到用户实质性的私密信息,只是获取了一个短期的进入令牌(必须保密,不能泄露),确保了用户账号的安全。

在应用OAuth时,常常与OpenID作比较:
OAuth的关注点在于授权,OpenID的侧重于证明鉴定。简单来说,OAuth解决用户能(想)做什么,是 WHAT 的问题;OpenID则为我们验证用户是谁,解决WHO的问题

1.0 与 2.0 的授权方式不同
浏览器、目标服务器、第三方认证服务器

  • 2.0授权过程分3步:
    获取授权码Authorization Code --> 换取访问令牌access_token --> 访问授权资源
    具体过程:浏览器向目标服务器发起认证请求 --> 目标服务器通知浏览器重定向到第三方认证服务器 --> 浏览器输入第三方认证的用户名和密码并发送 --> 认证成功后回调给目的服务器授权码--> 目的服务器使用授权码向第三方认证服务器换取访问令牌 --> 第三方认证服务器返回令牌给目的服务器 --> 目的服务器通知浏览器刷新页面,登录授权成功,使用令牌访问得到授权的资源
OAuth2x之授权码
  • 1.0授权过程分4步
    1. 请求未授权的Request Token (比如显示新浪微博的授权登录界面)
    2. 请求授权的Request Token,用户确认授权,认证服务器返回授权的OAuth Token
    3. OAuth Token换取Access Token,获得访问令牌oauth_tokenoauth_token_secret
    4. 用访问令牌访问得到授权的资源
OAuth1x

虽然1.0协议每个Token都有加密,2.0则不需要,但2.0要求使用 https 协议,安全性高于1.0;而且2.0充分考虑了客户端的各种姿态,提供了多种途径获取访问令牌,包括授权码,客户端私有整数,资源拥有者密码证书,刷新令牌等等,且验证过程更加简洁。相比之下,1.0只有一个用户授权流程。

Github认证为例
需要先开通Github账号,主页头像 --> Settings --> Developer settings --> OAuth Apps --> New OAuth App 注册一个第三方OAuth认证的应用(应用名称、应用的主页URL、认证回调的URL),得到 Client ID,Client Secret

  • 前端页面提供第三方Github登录的入口:
<a href="/github/login">login with github</a>
  • 后端处理第三方认证:
const Koa = require('koa'),
    Router = require('@koa/router'),
    static = require('koa-static'),
    querystring = require('querystring'),
    axios = require('axios');
const config = {
    client_id: '76a2dacb24ead9fc4596',
    client_secret: '657124b0a8073d5538d12ad68f6d56114761283c'
}
// github 的认证接口
const OAuthURL = 'https://github.com/login/oauth/authorize';
// github 的Token接口
const TokenURL = 'https://github.com/login/oauth/access_token'
let thirdUser;  // 保存第三方认证用户的数据
const app = new Koa();
const router = new Router();
app.use(static(__dirname + '/'));

router.get('/github/login', async ctx => {
    // 重定向到Github认证接口
    const url = OAuthURL + '?client_id=' + config.client_id;
    ctx.redirect(url)
});

/**
Github上配置的认证回调接口:
    http://localhost:7000/auth/github/callback
*/ 
router.get('/auth/github/callback', async ctx => {
    // 获取到授权码Code
    const code = ctx.query.code;
    const params = Object.assign({ code }, config)
    // 根据授权码 换取令牌Token
    let resp = await axios.post(TokenURL, params)
    const access_token = querystring.parse(resp.data).access_token
    // 已经获取了Token,可以请求Github的接口了:获取用户数据
    thirdUser = await axios.get('https://api.github.com/user?access_token='+access_token)
    // 重定向到主页
    ctx.redirect('/home')
});
router.get('/home', async ctx => {
    const { login, avatar_url } = thirdUser.data
    ctx.body = `
        <h1>Hello ${login}</h1>
        <img src="${avatar_url}" />
    `
})

app.use(router.routes()).use(router.allowedMethods()).listen(7000);

SSO

单点登录:Single Sign On - SSO 是一个在多系统共存的环境下,用户登录其中一个系统,便可在其他所有系统中得到授权 而无需再次登录。包括 单点登录单点注销 两部分。

Web系统由单系统发展成多个系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体;也就是说,用户访问Web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。

单点登录

单点登录可分为同域和跨域两种场景

同域

同域单点登录的核心就是共享Cookie,所有子系统都使用同一个一级域名,通过不同的二级域名来区分。
举个栗子:公司有一个一级域名 zlt.com,三个子系统:门户系统sso.zlt.com,应用1app1.zlt.com,应用2app2.zlt.com

  1. 设置它们的Cookie域为一级域名(domain=zlt.com),这样就可以共享门户系统的Cookie给所有使用 *.zlt.com 作为域名的系统。
  2. 使用 Spring Session 等技术让所有系统共享Session。这样只要门户系统登录之后,无论跳转 应用1 还是应用2,都能通过门户系统Cookie中的sessionId读取到Session中的登录信息,实现单点登录。

然而,共享Cookie的方式存在众多局限。首先,应用群域名必须统一;其次,应用群各系统使用的技术(至少是Web服务器)要相同,否则Cookiekey值(TomcatJSESSIONID)不同,无法维持会话,即共享Cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,Cookie本身不安全。

跨域

单点登录之间的系统域名不同,无法共享Cookie,需要一个独立的 授权系统/认证中心。子系统本身不参与登录,而是通过认证中心验证身份,完成间接授权。当一个系统登录成功后,认证中心会颁发一个令牌给子系统,子系统拿着令牌去获取受保护的资源。为了减少频繁认证,各个子系统在被认证中心授权以后,会建立一个局部会话,在一定时间内无需再次向认证中心发起认证。

单点登录
  • 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至SSO认证中心,并将自己的地址作为参数
  • SSO认证中心发现用户未登录,将用户引导至登录页面
  • 用户输入用户名密码提交登录申请
  • SSO认证中心校验用户信息,创建用户与SSO认证中心之间的会话,称为全局会话,同时创建授权令牌
  • SSO认证中心带着令牌跳转回最初的请求地址(系统1)
  • 系统1拿到令牌,去SSO认证中心校验令牌是否有效(安全性,必须校验)
  • SSO认证中心校验令牌,返回有效,注册系统1
  • 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  • 用户访问系统2的受保护资源,系统2发现用户未登录,跳转至SSO认证中心,并将自己的地址作为参数
  • SSO认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  • 系统2拿到令牌,去SSO认证中心校验令牌是否有效,SSO认证中心返回有效,注册系统2
  • 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与SSO认证中心及各个子系统建立会话,用户与SSO认证中心建立的会话称为全局会话,与各个子系统建立的会话称为局部会话。局部会话建立之后,用户访问子系统受保护资源将不再通过SSO认证中心。

全局会话局部会话的约束关系:

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁

SSO系统登录后,带着令牌跳回原业务系统时,业务系统还要拿令牌再次访问SSO进行验证,这个步骤看似有点多余。如果SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?!
其实问题是很严重的,如果在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的!

单点注销

单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁;

单点注销

SSO认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

  • 用户向 系统1 发起注销请求;
  • 系统1 根据与用户建立的会话id拿到令牌,向SSO认证中心发起注销请求;
  • SSO认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址,向所有注册系统发起注销请求;
  • 各注册系统接收SSO认证中心的注销请求,销毁局部会话;
  • SSO认证中心引导用户至登录页面。

相关文章

  • 谈谈鉴权与授权

    目录 鉴权场景实现 授权场景实现 鉴权 鉴权(authentication): 你是谁 场景 实现 关于鉴权的示例...

  • 常见的鉴权方式,你真的不想知道吗

    主要内容 鉴权的作用 几种常见的鉴权 各个鉴权的适用场景 一、什么是鉴权 鉴权是指验证用户是否有权利访问系统的行为...

  • 云调用,小程序鉴权-方案

    目录:一、无处不在的鉴权 现实生活中的身份鉴权方法 简单的密码鉴权体系二、鉴权优化 频繁的鉴权场景下的优化方案 第...

  • JWT鉴权 Session鉴权

    JWT鉴权:image.png session鉴权:image.png

  • 鉴权与Web安全

    什么是鉴权 鉴权(authentication)是指验证用户是否拥有访问系统的权利。传统的鉴权是通过密码来验证的。...

  • 04-15动态路由的实现与优化

    Vue中后台鉴权的另一种思路 - 动态路由的实现与优化 鉴权-前端路由 VS 鉴权-动态路由 前端路由鉴权相信只要...

  • 鉴权和权限管理的探索

    传统鉴权方案是通过Session ID完成的,现在一般使用Token鉴权。 1.认证与鉴权 分布式session ...

  • 鉴权方式整理

    最近在做 aPaaS 相关的项目,用到各种用户鉴权的方式。所以对鉴权方式做个整理。 常见的鉴权方式: HTTPBa...

  • 15 Go 鉴权(一):鉴权机制概述

    一、系统鉴权概述 在现代web开发中,系统鉴权服务已是基本标配模块,有些开发框架甚至内置了鉴权模块的实现,或者提供...

  • 防人之心不可无:网站安全问题窥视

    鉴权和授权 鉴权,Authentication,指的是对于用户身份的鉴别; 登录 授权,Authorization...

网友评论

      本文标题:鉴权

      本文链接:https://www.haomeiwen.com/subject/cknrjktx.html