2025-06-19 19:20:27 +08:00

393 lines
12 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' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
expire?: number; // 过期时间,单位为秒
hasRefreshToken?: boolean;
[key: string]: any;
};
interface Store<T> {
redis?: Redis;
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>;
delKeys: (keys: string[]) => Promise<number>;
}
export class RedisTokenStore implements Store<OauthUser> {
redis: Redis;
private prefix: string = 'oauth:';
constructor(redis?: Redis, prefix?: string) {
this.redis = redis;
this.prefix = prefix || this.prefix;
}
async setRedis(redis: Redis) {
this.redis = redis;
}
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 'day':
expire = 24 * 60 * 60;
break;
case 'week':
expire = 7 * 24 * 60 * 60;
break;
case 'month':
expire = 30 * 24 * 60 * 60;
break;
case 'season':
expire = 90 * 24 * 60 * 60;
break;
default:
expire = 7 * 24 * 60 * 60; // 默认过期时间为7天
}
} 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);
}
}
async delKeys(keys: string[]) {
const prefix = this.prefix;
const number = await this.redis.del(keys.map((key) => prefix + key));
return number;
}
}
export class OAuth<T extends OauthUser> {
private store: Store<T>;
constructor(store: Store<T>) {
this.store = store;
}
generateSecretKey(sk = true) {
if (sk) {
return 'sk_' + randomId64();
}
return 'st_' + randomId32();
}
/**
* 生成token
* @param user
* @param user.id 访问者id
* @param user.uid 如果是org这个是真实用户idid是orgId
* @param user.userId 真实用户id
* @param user.orgId 组织id可选
* @param user.username
* @param user.type
* @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 };
}
async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) {
// 生成一个secretKey
// 设置到store中
oauthUser.oauthExpand = {
...oauthUser.oauthExpand,
accessToken: secretKey,
description: 'secretKey',
createTime: new Date().getTime(), // 创建时间
};
await this.store.setToken(
{ accessToken: secretKey, refreshToken: '', value: oauthUser },
{
...opts,
hasRefreshToken: false,
},
);
return secretKey;
}
getOauthUser({ id, uid, username, type }: Partial<T>): OauthUser {
const oauthUser: OauthUser = {
id,
username,
uid,
userId: uid || id, // 必存在真实用户id
type: type as 'user' | 'org',
};
if (uid) {
oauthUser.orgId = id;
}
return oauthUser;
}
/**
* 验证token如果token不存在返回null
* @param token
* @returns
*/
async verifyToken(token: string) {
const res = await this.store.getObject(token);
return res;
}
/**
* 验证token是否是accessToken, sk 开头的为secretKey没有refreshToken
* @param token
* @returns
*/
isSecretKey(token: string) {
if (!token) {
return false;
}
// 如果是sk_开头则是secretKey
if (token.startsWith('sk_')) {
return true;
}
return false;
}
/**
* 刷新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);
}
/**
* 获取某一个用户的所有token
* @param userId
* @returns
*/
async getUserTokens(userId: string, orgId?: string) {
const userPrefix = orgId ? `org:${orgId}:user:${userId}` : `user:${userId}`;
const tokens = await this.store.keys(`${userPrefix}:token:*`);
return tokens;
}
/**
* 过期某一个用户的所有token
* @param userId
* @param orgId
*/
async expireUserTokens(userId: string, type: 'user' | 'org' = 'user') {
const userPrefix = type === 'org' ? `org:${userId}:user:*:` : `user:${userId}`;
const tokensKeys = await this.store.keys(`${userPrefix}:token:*`);
for (const tokenKey of tokensKeys) {
try {
const token = await this.store.redis.get(tokenKey);
const user = await this.store.getObject(token);
this.store.delObject(user);
} catch (error) {
console.error('expireUserTokens error', userId, type, error);
}
}
}
/**
* 过期所有用户的token 然后重启服务
*/
async expireAllTokens() {
const tokens = await this.store.keys('*');
await this.store.delKeys(tokens);
}
}