/** * 一个生成和验证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'; // 用户类型,默认是user,token类型是用于token的扩展 oauthType?: 'user' | 'token'; // 用户类型,默认是user,token类型是用于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 { redis?: Redis; getObject: (key: string) => Promise; setObject: (key: string, value: T, opts?: StoreSetOpts) => Promise; expire: (key: string, ttl?: number) => Promise; delObject: (value?: T) => Promise; keys: (key?: string) => Promise; setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise; delKeys: (keys: string[]) => Promise; } export class RedisTokenStore implements Store { 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 { private store: Store; constructor(store: Store) { 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,这个是真实用户id,id是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): 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) { 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); } }