remove mark

This commit is contained in:
2025-12-04 10:31:37 +08:00
parent 46aa293cce
commit 2a55f2d3ef
35 changed files with 1837 additions and 726 deletions

392
src/auth/oauth/oauth.ts Normal file
View File

@@ -0,0 +1,392 @@
/**
* 一个生成和验证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);
}
}