session
常规的单页面部署模式下,除了服务端的 api 接口外,其他的请求 URL 全部返回 index.html,(www.baidu.com/login` )login 页面,其实返回的是 index.html
- 访问登录地址,输入账号密码后,登录成功后,服务端会给 cookie中种下一个 sessionId, (服务端签发的sessionID,直接存到cookie 中)
- 前端根据路由访问页面的时候,根据 cookie 的特性,会将所有访问的接口都给自动带上 sessionId, 通过 sessionID 服务端来判断有没有登录,服务端通过对比cookie 中的sessionID 和保存在服务端中的是否一致
- 如果用户未登录,接口返回一个错误码,在前端请求拦截器中,判断返回的错误码,跳转到登录页面
- 如果用户登陆,则直接放行
- 当退出登录的时候,调用服务端退出登录的接口,将 sessionId的 登录取消,然后前端跳转到登录页面
- 当用户重新登录的时候,服务端重新签发一个 sesssionID
这样会有一个问题,当有大量客户端的时候,服务器中要保存大量的 sessionID, 并且如果部署的是一个集群,则所有的机器都需要同步sessionID,而且,sessionId 存放在cookie 中,会有 CSRF 攻击
所有,提出了 token 的登录方式
- 用户输入账号密码后,点击登录 服务端创建一个 token
- 客户端拿到 token 后自由保存
- 请求页面的时候,将 token 传给服务端,服务端验证 token 是否有效
- 验证通过,登录成功
这里我们发现,服务端自己不会存 token, 而是只验证 token 是否有效,判断客户端带过来的 token 对不对, 所有的用户信息都存在 token 中
我们可以通过 jwt.io 这个网站来解析 token
SSO 单点登录
单点登录,就是同一个账号在多个平台应用系统中,用户只需要登录一次,就可以访问所有的平台,比如百度公司有百度,百度云,百度贴吧,用户不管在哪个平台下登录账号,只需要登录一次,就可以访问这三个平台
实现单点登录的方式可以利用 父域cookie 或者 认证中心的方式
- 父域cookie
利用cookie 的特性,父域的 cookie 可以被子域共享,将多个平台的域名都建立在一个主域名下面,这样的话,登录之后,将 cookie 保存在主域名之下就可以了,登录其他平台的时候,就可以使用主域名的cookie
- 认证中心
其实认证中心就是利用了 OAuth2.0授权协议
单点登录认证中心实现流程,我们还拿百度,百度云,百度贴吧 举例
-
- 用户访问百度页面,发现没有登录,然后百度页面跳转到认证中心,跳转认证中心的时候,带上回调的地址,这个地址就是百度页面地址,认证中心发现用户没有登录,返回登录页面
- 用户在 认证中心 输入账号密码后,认证中心登录成功,然后重定向到百度页面地址,并且重定向到时候,,带上了一个授权码 code, 而且认证中心会将认证中心的登录态写入到 认证中心的 cookie 中
- 在百度平台中,拿到 code 向认证中心 去验证是否合法,当 code 验证通过时候,百度将登录信息写入到cookie中
- 现在存了两个 cookie, 一个是百度页面的登录态cookie,一个是认证中心页面的登录态cookie
- 当百度平台访问其他页面的时候,因为已经有了 百度的登录 cookie, 所以直接访问
- 当访问 百度云 或者 百度贴吧的时候,跳转认证中心带上回调地址(百度云),这个时候,认证中心的cookie会被自动带上,发现 cookie 中已经存了 认证中心的 登录态,所以,不需要在认证中心重新输入登录密码, 而是认证中心直接带上 code 重定向到百度云, 后续流程与上述 c 流程一致
当退出登录的时候,比如百度云退出登录,则调用认证中心的退出登录api, 这个时候认证中心会拿到下发的所有平台的token,然后调用他们的api,实现退出登录
OAuth2
OAuth 即 Open Authorization, 是一种授权协议标准, OAuth 允许用户授权第三方应用访问他服务器存储的各种信息数据,而且不需要提供用户名和密码给第三方应用
借用一个图来表示实现第三方登录
看一个例子:比如接入支付宝的登录流程
- 先去支付宝开放平台注册一下,然后获取 APPID
- 在支付宝开放平台配置回调地址 ENCODED_URL (用于登录成功后,跳转的页面)
- 登录成功后,ENCODED_URL 的页面会在 URL 上面带有一个 authCode
- 监听 URL 参数, 将 authCode 通过调用服务端接口传给服务端
- 服务端拿到 authCode 去换取 token, 然后再拿着 token 去调用支付宝的 SDK 去获取支付宝的用户信息返回
// 监听路由,将 authCode 传给服务端
const updateCertification = async (auth_code: string) => {
const { code } = await update(auth_code);
};
useEffect(() => {
const { auth_code } = parse(location.search);
await updateCertification(auth_code);
}, [location]);
// 跳转登录支付宝
const randomString = uuidv4().slice(0, 7);
const buffer = Buffer.from(randomString);
const state = buffer.toString('base64');
const APPID = '2021***********58'; // 你的APPID
const ENCODED_URL =
'https://www.xxxx.com:8080/home'; // 你的实际的项目地址
// 为了每次打开都可以跳转到登录页面,所以修改为这个地址
const url = encodeURIComponent(
`https://openauth.alipay.com:443/oauth2/publicAppAuthorize.htm?app_id=${APPID}&scope=auth_user&redirect_uri=${ENCODED_URL}&state=${state}`,
);
history.push(`https://auth.alipay.com/login/index.htm?goto=${url}`);
KeyCloak 登录
KeyCloak 接入主要分为两种流程,非机密客户端 和 机密客户端
非机密客户端
整个登录流程不需要服务端参与,是透明的,服务端并没有用户信息,所有的用户信息维护在 keycloak 中,服务端不需要做账号权限管理
- 用户访问平台网址,如果没有登录,则跳转到 keyCloak 登录页面中去登录
- 登录成功后,keyCloak 通过内部的私钥,颁发一个token给浏览器
- 网站携带 token 去访问服务端地址,服务端解析token 可以对资源进行一个鉴权操作,KeyCloak 只做授权,token中告诉这个用户属于哪个组,有哪些权限,但是他不做鉴权,服务端拿到这些权限定义的字段后,自己鉴权
- 服务端拿到 keyCloak的公钥(公钥可以在服务端中写死),去验证token 是否正确
- 成功则用户正常访问
机密客户端
浏览器和服务端通过 cookie 维系,也叫全局会话,服务端和 keyCloak 通过 token 维系,也叫全局会话,服务端相当于把 KeyCloak当成了一个存储
- 浏览器登录,输出账号密码后直接请求服务端
- 服务端拿到用户账号密码再去 keyCloak 登录,然后 keyCloak 颁发 token 给服务端
- 服务端 还获取到一个 refresh token 用于在 Keycloak 中获取用户信息
- 服务端给浏览器中颁发一个 cookie (seesion)
- 用户有 cookie 直接登录
KeyCloak 也可以自定义登录页面 利用
github.com/keycloakify…
github.com/keycloakify…
下面是一个前端集成 使用 keycloak-js 例子,这里用的是非机密客户端的方法
import Keycloak from 'keycloak-js';
let token = ''
// 实例化
const keycloakInstance = new Keycloak({
url: 'xxx', // 你的地址
realm: 'xxx',
clientId: 'xxx',
});
// token 过期获取
keycloakInstance.onTokenExpired = () => {
keycloakInstance.updateToken(5).then((refreshed) => {
if (refreshed) {
token = keycloakInstance.token;
} else {
// 退出登录
keycloakInstance.logout()
}
}
};
await keycloakInstance.init({
onLoad: 'login-required',
pkceMethod: 'S256',
});
token = keycloakInstance.authenticated ? keycloakInstance.token : '';
之后,将获取到的 token 放到请求头中即可
扫码登录
- 服务端随机生成一个二维码,并且返回给前端
- 前端调用服务端接口轮询来查询二维码的状态
- 等待用户扫描二维码,当用户用手机扫描的时候,如果手机端没有登录,则会先跳转到登录页面,登录之后跳转到确认登录页面,用于授权登录pc端
- 当确认登录pc 后,手机端会发送一个请求,对二维码的状态进行修改 (二维码的 ID 可以从二维码中拿到)
- 当 pc 轮询发现是确认登录状态后,会生成一个 token 返回,然后浏览器自动登录用户
二维码五个状态
- 未扫描
- 已扫描,等待用户确认
- 已扫描,用户同意授权
- 已扫描,用户取消授权
- 已过期
常用加密方式
import md5 from 'crypto-js/md5';
import NodeRSA from 'node-rsa';
export const MD5 = (value: string) => {
return md5(value).toString();
};
// 利用公钥加密
export const RSA = (message: string, publicKey = PUBLICKEY) => {
const key = new NodeRSA(publicKey);
key.setOptions({ encryptionScheme: 'pkcs1' });
return key.encrypt(message, 'base64');
};
// 利用私钥解密
export const RSADecrypt = (message: string, privateKey: string) => {
const key = new NodeRSA(privateKey, 'pkcs8-private');
key.setOptions({ encryptionScheme: 'pkcs1' });
return key.decrypt(Buffer.from(message, 'base64'), 'utf8');
};
// 利用公钥解密
export const RSADecryptPublic = (message: string, publicKey = PUBLICKEY) => {
const key = new NodeRSA(publicKey);
key.setOptions({ encryptionScheme: 'pkcs1' });
return key.decryptPublic(Buffer.from(message, 'base64'), 'utf8');
};