283 lines
8.6 KiB
TypeScript
283 lines
8.6 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'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
|
||
expire?: number; // 过期时间,单位为秒
|
||
hasRefreshToken?: boolean;
|
||
[key: string]: any;
|
||
};
|
||
interface Store<T> {
|
||
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>;
|
||
}
|
||
export class RedisTokenStore implements Store<OauthUser> {
|
||
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<T extends OauthUser> {
|
||
private store: Store<T>;
|
||
|
||
constructor(store: Store<T>) {
|
||
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<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);
|
||
}
|
||
}
|