/** * 一个生成和验证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'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year' expire?: number; // 过期时间,单位为秒 hasRefreshToken?: boolean; [key: string]: any; }; interface Store { 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; } export class RedisTokenStore implements Store { 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 { private store: Store; constructor(store: Store) { 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) { 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); } }