393 lines
12 KiB
TypeScript
393 lines
12 KiB
TypeScript
/**
|
||
* 一个生成和验证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<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,这个是真实用户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<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);
|
||
}
|
||
}
|