认证与鉴权
一、什么是认证、什么是授权
1、认证(Authentication)
用户认证,就是验证此用户的身份。解决的是‘我是谁’的问题。就是从用户请求信息中获取用户信息的过程,认证是一个过程。
举个栗子🌰:当你在登录时,输入用户名密码,系统判断你的用户名与密码是否在已有的用户内并给出结果反馈,这个过程就是用户认证。
用户名+密码登录,手机、邮箱验证码,第三方登录等都属于用户认证的一种。
2、授权(Authorization)
用户授权,就是授予用户权限,能够进行后续的某些访问和操作。解决的是“我能干那些事”的问题。就是从获取到用户信息到授予用户权限的过程。
举个栗子🌰:每天乘电梯刷卡,刷卡的过程就是授权,授予了我们访问某某层的权限。就相当于是某个人或者某个机构赋予某人干某事的权限的过程。
认证 | 授权 |
---|---|
身份验证确认您的身份以授予对系统的访问权限 | 授权确定您是否有权访问资源 |
这是验证用户凭据以获得用户访问权限的过程 | 这是验证是否允许访问的过程 |
它决定用户是否是他声称的用户 | 它确定用户可以访问和不访问的内容 |
身份认证通常需要用户名和密码 | 授权所需的身份验证因素可能有所不同,具体取决于安全级别 |
身份认证是授权的第一步,因此始终是第一步。 | 授权在认证成功后完成。 |
二、什么是Cookie、什么是Session
凭证
实现认证和授权的前提是需要一种**媒介(证书)**来标记访问者的身份,这个媒介(证书)就是凭证
在互联网应用中,常见的就是在登录之后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌。
1、Cookie
- HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。
- **cookie 存储在客户端:**cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
- **cookie 是不可跨域的:**每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)
2、Session
当我们使用Cookie时,实际就是服务端向访它他的客户端给与一个通行凭证,无论谁访问都必须携带自己通行凭证。这样服务器就能从通行凭证上确认客户身份了。这就是Cookie的工作原理
cookie 可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些Cookie,如果 Cookie 很多,这无形地增加了客户端与服务端的数据传输量,由此就诞生了Session来解决这样的问题,
Session的原理是当同一个客户端每次和服务端交互时,不需要每次都传回所有的 Cookie 值,而是只要传回一个 ID,这个 ID 是客户端第一次访问服务器的时候生成的, 而且每个客户端是唯一的。这样每个客户端就有了一个唯一的 ID,客户端只要传回这个 ID 就行。
当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端里的请求里是否已包含了一个session标识--sessionID,如果已经包含一个sessionID,则说明以前已经为此客户端创建过session,服务器就按照sessionID把这个session检索出来使用如果客户端请求不包含sessionID,则为此客户端创建一个session并且声称一个与此session相关联的sessionID,
sessionID的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串(服务器会自动创建),这个sessionID将被在本次响应中返回给客户端保存。
3、Cookie 和 Session 的区别
- **安全性:**Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
- 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
- **有效期不同:**Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
- **存储大小不同:**单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
三、什么是Token
token是在访问服务端资源时候需要携带的一种验证凭证
简单的token组成:userID(用于标识唯一的用户身份)+time(当前时间的时间戳)+sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
验证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
- 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
- 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
- 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
- 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
- token 完全由应用管理,所以它可以避开同源策略
Token 和 Session 的区别
- Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token使服务端无状态化,不会存储会话信息。
- Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
- 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
四、什么是JWT
JWT全称为:JSON Web Token。适合分布式的授权模式。是目前最流行的跨域认证解决方案
用户登录成功后,认证服务器会返回一个带有签名认证的JWT。JWT保存在客户端(如浏览器或APP),每次访问应用服务器时,将JWT放到http的header 的Authorization 字段里面。
Authorization: Bearer <token>
不同于Session的集中管理,JWT自身就包含了用户信息,每台应用服务器可以自己独自判断JWT是哪个用户、是否合法、是否过期,无需再向认证服务器确认,也没有Session同步的问题。JWT至少包含以下信息:
- sub: Subject,一般为用户ID。
- exp: Expiration Time。过期时间。
- signature: 签名。用于验证JWT是否合法。
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
使用JWT
1、在请求时在请求头的Authorization 带上token
2、可以在url上带上token字段
3、也可在跨域的时候放在Post请求的请求体内
五、实践是检验真理的唯一方式
1、cookie实践
设置格式:this.ctx.cookies.set(key,value,options)
获取格式: this.ctx.cookies.get(key)
如果在设置的时候加密了 需要 this.ctx.cookies.get(key,{encrypt: true})
清除格式:this.ctx.cookies.set(key,null)
// 设置
this.ctx.cookies.set('auth', 'test', {
maxAge: 24 * 3600 * 1000, // 过期时间 1天
httpOnly: true,
signed: true, // 对cookie进行签名,防止被修改
encrypt: true, // 对cookie进行加密, 如果加密,则在获取的时候需要对cookie进行解密
});
// 获取
this.ctx.cookies.get('auth',{encrypt: true}
// 清除
this.ctx.cookies.set('auth',null}
options参数 maxAge: 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。 expires: 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。 path: 设置键值对生效的 URL 路径,默认设置在根路径上(/),也就是当前域名下的所有 URL 都可以访问这个 Cookie。 domain: 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。 httpOnly: 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。 secure: 设置键值对只在 HTTPS 连接上传输,框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。 除了这些属性之外,框架另外扩展了 3 个参数的支持:
overwrite:设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。 signed:设置是否对 Cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。 encrypt:设置是否对 Cookie 进行加密,如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。
我们来使用node写三个接口
router.get('/api/user/setauth', controller.user.SetAuth);
router.get('/api/user/auth', _Cookie, controller.user.Auth);
router.get('/api/user/clearauth', _Cookie, controller.user.ClearAuth);
// _Cookie是一个校验cookies的中间件 如果请求不带cookie 就会被请求拦截
public async SetAuth() {
this.ctx.cookies.set('auth', 'test', {
maxAge: 24 * 3600 * 1000, // 过期时间 1天
httpOnly: true,
signed: true, // 对cookie进行签名,防止被修改
encrypt: true, // 对cookie进行加密, 如果加密,则在获取的时候需要对cookie进行解密
});
this.ctx.body = { message: '设置cookie成功', code: 200 };
}
public async Auth() {
// cookie
const cookie = this.ctx.cookies.get('auth', { encrypt: true });
this.ctx.body = { message: `刚才设置的cookie是: ${cookie}`, code: 200 };
}
public async ClearAuth() {
// cookie
this.ctx.cookies.set('auth', null);
this.ctx.body = { message: 'cookie清除成功', code: 200 };
}
我们先请求/api/user/setauth
来将cookie设置好
然后我们再请求·/api/uset/auth
来查看刚才设置的cookie
然后我们再请求/api/user/clearauth
来清除设置好的cookie
最后我们再次请求/api/user/auth
此次请求就会被中间件拦截
2、session实践
设置格式:this.ctx.session.[key]=value
获取格式: this.ctx.cookies.[key]
// 设置 session可以设置多种数据类型
this.ctx.session.auth = 'test';
// 获取
const sessionvalue = this.ctx.session.auth
//配置 session的很多配置与Cookie相似 可以参考cookie
config.session = {
// 设置session cookis里面的key
key: 'SESSION_KEY',
// 设置过期时间
maxAge: 24 * 3600 * 1000,
httpOnly: true,
// 设置是否加密
encrypt: true,
// 设置每次刷新页面的时候session是否都会被延期
renew: true,
};
我们依旧来使用node写两个接口
router.get('/api/user/setsession', controller.user.setSession);
router.get('/api/user/session', controller.user.getSession);
public async setSession() {
this.ctx.session.auth = 'test';
this.ctx.body = { message: '设置session成功', code: 200 };
}
public async getSession() {
const session = this.ctx.session.auth;
this.ctx.body = { message: `刚才设置的session是: ${session}`, code: 200 };
}
我们先来请求/api/user/setsession
可以看到session已经设置成功
接下来我们请求 /api/user/session
来获取刚才生成的session
此时可以看到,我们可以获取到设置的session值,并且请求的cookie里已经写入了SESSION_KEY
3、jwt实践
设置格式: app.jwt.sign(options,sectet)
获取内容格式:app.jwt.verify(token,sectet)
不同的框架 ,设置的方法有所不同
// 设置
const token = app.jwt.sign({
userid,
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // token 有效期为 24 小时
// exp: Math.floor(Date.now() / 1000) + 10, // 测试 --- token 有效期为 10秒
}, app.config.jwt.secret,);
// 解密获取
const tokenvalue = ctx.app.jwt.verify(token, secret);
我们依旧使用Node来写两个接口
router.post('/api/user/login', controller.user.Login);
router.post('/api/user/getUserInfo', _jwt, controller.user.GetUserInfo);
// _jwt是验证token的中间件
import { Context } from 'egg';
const consola = require('consola');
const JwtCheck = (secret: string) => {
return async (ctx: Context, next: any) => {
const token = ctx.request.header.authorization as string; // 拿到token
let decode = '';
if (token !== 'null' && token) {
try {
const formatToken = token.split(' ')[1];
// 解密 token
decode = ctx.app.jwt.verify(formatToken, secret);
ctx.decode = decode;
await next();
} catch (error) {
consola.error(error);
if (error.name === 'TokenExpiredError') {
consola.info('Token过期!');
ctx.body = {
msg: 'token已过期,请重新登录',
code: 401,
};
return;
}
ctx.body = {
msg: 'token已失效,请重新登录',
code: 401,
};
return;
}
} else {
ctx.body = {
code: 401,
msg: 'token不存在',
};
return;
}
};
};
export default JwtCheck;
public async Login() {
const { ctx, app } = this;
const { userid } = ctx.request.body;
consola.log('userid', userid);
// 生成 token
const token = app.jwt.sign({
userid,
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // token 有效期为 24 小时
// exp: Math.floor(Date.now() / 1000) + 10, // 测试 --- token 有效期为 10秒
}, app.config.jwt.secret);
ctx.body = { message: '登录成功!', code: 1, data: { token } };
}
// 获取用户信息
public async GetUserInfo() {
const ctx = this.ctx;
ctx.body = { message: `获取成功,登录的用户的用户ID为:${ctx.decode.userid}`, code: 1 };
}
首先我们通过调用/api/user/login
登录接口来设置token
然后我们来调用接口/api/user/getUserInfo
来判断token的有效性并获取userid
记得要在请求头的 Authorization字段加上 Bearer
就可以看到我们登录的userid 😀