2025-03-21 20:45:14 +08:00

283 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 一个生成和验证token的模块不使用jwt使用redis缓存
* token 分为两种一种是access_token一种是refresh_token
*
* access_token 用于验证用户是否登录过期时间为1小时
* refresh_token 用于刷新access_token过期时间为7天
*
* 生成token时会根据用户信息生成一个access_token和refresh_token并缓存到redis中
* 验证token时会根据token从redis中获取用户信息
* 刷新token时会根据refresh_token生成一个新的access_token和refresh_token并缓存到redis中
*
* 并删除旧的access_token和refresh_token
*
* 生成token的方法使用nanoid生成一个随机字符串
* 验证token的方法使用redis的get方法验证token是否存在
*
* 刷新token的方法使用redis的set方法刷新token
*
* 缓存和获取都可以不使用redis只是用可拓展的接口。store.get和store.set去实现。
*/
import { Redis } from 'ioredis';
import { customAlphabet } from 'nanoid';
export const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
export const randomId16 = customAlphabet(alphabet, 16);
export const randomId24 = customAlphabet(alphabet, 24);
export const randomId32 = customAlphabet(alphabet, 32);
export const randomId64 = customAlphabet(alphabet, 64);
export type OauthUser = {
/**
* 真实用户非org
*/
id: string;
/**
* 组织id非必须存在
*/
orgId?: string;
/**
* 必存在真实用户id
*/
userId: string;
/**
* 当前用户的id如果是org则uid为org的id
*/
uid?: string;
username: string;
type?: 'user' | 'org'; // 用户类型默认是usertoken类型是用于token的扩展
oauthType?: 'user' | 'token'; // 用户类型默认是usertoken类型是用于token的扩展
oauthExpand?: UserExpand;
};
export type UserExpand = {
createTime?: number;
accessToken?: string;
refreshToken?: string;
[key: string]: any;
} & StoreSetOpts;
type StoreSetOpts = {
loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
expire?: number; // 过期时间,单位为秒
hasRefreshToken?: boolean;
[key: string]: any;
};
interface Store<T> {
getObject: (key: string) => Promise<T>;
setObject: (key: string, value: T, opts?: StoreSetOpts) => Promise<void>;
expire: (key: string, ttl?: number) => Promise<void>;
delObject: (value?: T) => Promise<void>;
keys: (key?: string) => Promise<string[]>;
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>;
}
export class RedisTokenStore implements Store<OauthUser> {
private redis: Redis;
private prefix: string = 'oauth:';
constructor(redis: Redis, prefix?: string) {
this.redis = redis;
this.prefix = prefix || this.prefix;
}
async set(key: string, value: string, ttl?: number) {
await this.redis.set(this.prefix + key, value, 'EX', ttl);
}
async get(key: string) {
return await this.redis.get(this.prefix + key);
}
async expire(key: string, ttl?: number) {
await this.redis.expire(this.prefix + key, ttl);
}
async keys(key?: string) {
return await this.redis.keys(this.prefix + key);
}
async getObject(key: string) {
try {
const value = await this.get(key);
if (!value) {
return null;
}
return JSON.parse(value);
} catch (error) {
console.log('get key parse error', error);
return null;
}
}
async del(key: string) {
const number = await this.redis.del(this.prefix + key);
return number;
}
async setObject(key: string, value: OauthUser, opts?: StoreSetOpts) {
await this.set(key, JSON.stringify(value), opts?.expire);
}
async delObject(value?: OauthUser) {
const refreshToken = value?.oauthExpand?.refreshToken;
const accessToken = value?.oauthExpand?.accessToken;
// 清理userPerfix
let userPrefix = 'user:' + value?.id;
if (value?.orgId) {
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
}
if (refreshToken) {
await this.del(refreshToken);
await this.del(userPrefix + ':refreshToken:' + refreshToken);
}
if (accessToken) {
await this.del(accessToken);
await this.del(userPrefix + ':token:' + accessToken);
}
}
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts) {
const { accessToken, refreshToken, value } = data;
let userPrefix = 'user:' + value?.id;
if (value?.orgId) {
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
}
// 计算过期时间根据opts.expire 和 opts.loginType
// 如果expire存在则使用expire否则使用opts.loginType 进行计算;
let expire = opts?.expire;
if (!expire) {
switch (opts.loginType) {
case 'month':
expire = 30 * 24 * 60 * 60;
break;
case 'season':
expire = 90 * 24 * 60 * 60;
break;
default:
expire = 25 * 60 * 60; // 默认过期时间为25小时
}
} else {
expire = Math.min(expire, 60 * 60 * 24 * 30, 60 * 60 * 24 * 90); // 默认的过期时间最大为90天
}
await this.set(accessToken, JSON.stringify(value), expire);
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
if (refreshToken) {
let refreshTokenExpire = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
// 小于7天, 则设置为7天
if (refreshTokenExpire < 60 * 60 * 24 * 7) {
refreshTokenExpire = 60 * 60 * 24 * 7;
}
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpire);
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire);
}
}
}
export class OAuth<T extends OauthUser> {
private store: Store<T>;
constructor(store: Store<T>) {
this.store = store;
}
/**
* 生成token
* @param user
* @returns
*/
async generateToken(
user: T,
expandOpts?: StoreSetOpts,
): Promise<{
accessToken: string;
refreshToken?: string;
}> {
// 拥有refreshToken 为 true 时accessToken 为 st_ 开头refreshToken 为 rk_开头
// 意思是secretToken 和 secretKey的缩写
const accessToken = expandOpts?.hasRefreshToken ? 'st_' + randomId32() : 'sk_' + randomId64();
const refreshToken = expandOpts?.hasRefreshToken ? 'rk_' + randomId64() : null;
// 初始化 appExpand
user.oauthExpand = user.oauthExpand || {};
if (expandOpts) {
user.oauthExpand = {
...user.oauthExpand,
...expandOpts,
accessToken,
createTime: new Date().getTime(), //
};
if (expandOpts?.hasRefreshToken) {
user.oauthExpand.refreshToken = refreshToken;
}
}
await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
return { accessToken, refreshToken };
}
/**
* 验证token如果token不存在返回null
* @param token
* @returns
*/
async verifyToken(token: string) {
const res = await this.store.getObject(token);
return res;
}
/**
* 刷新token
* @param refreshToken
* @returns
*/
async refreshToken(refreshToken: string) {
const user = await this.store.getObject(refreshToken);
if (!user) {
// 过期
throw new Error('Refresh token not found');
}
// 删除旧的token
await this.store.delObject({ ...user });
const token = await this.generateToken(
{ ...user },
{
...user.oauthExpand,
hasRefreshToken: true,
},
);
console.log('resetToken token', await this.store.keys());
return token;
}
/**
* 刷新token的过期时间
* expand 为扩展参数可以扩展到user.oauthExpand中
* @param token
* @returns
*/
async resetToken(accessToken: string, expand?: Record<string, any>) {
const user = await this.store.getObject(accessToken);
if (!user) {
// 过期
throw new Error('token not found');
}
user.oauthExpand = user.oauthExpand || {};
const refreshToken = user.oauthExpand.refreshToken;
if (refreshToken) {
await this.store.delObject(user);
}
user.oauthExpand = {
...user.oauthExpand,
...expand,
};
const token = await this.generateToken(
{ ...user },
{
...user.oauthExpand,
hasRefreshToken: true,
},
);
return token;
}
/**
* 过期token
* @param token
*/
async delToken(token: string) {
const user = await this.store.getObject(token);
if (!user) {
// 过期
throw new Error('token not found');
}
this.store.delObject(user);
}
}