feat: 优化token刷新逻辑,支持使用访问token刷新token,增强错误处理
This commit is contained in:
@@ -34,6 +34,9 @@ const usersTable = cfUser;
|
|||||||
const orgsTable = cfOrgs;
|
const orgsTable = cfOrgs;
|
||||||
const userSecretsTable = cfUserSecrets;
|
const userSecretsTable = cfUserSecrets;
|
||||||
|
|
||||||
|
// 常量定义
|
||||||
|
const JWKS_TOKEN_EXPIRY = 2 * 3600; // 2 hours in seconds
|
||||||
|
|
||||||
export const redis = useContextKey<Redis>('redis');
|
export const redis = useContextKey<Redis>('redis');
|
||||||
|
|
||||||
type TokenOptions = {
|
type TokenOptions = {
|
||||||
@@ -77,6 +80,32 @@ export class User {
|
|||||||
* @param uid
|
* @param uid
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 创建JWKS token的通用方法
|
||||||
|
*/
|
||||||
|
private static async createJwksTokenResponse(user: { id: string; username: string }, opts: { expire?: number } = {}) {
|
||||||
|
const expiresIn = opts?.expire ?? JWKS_TOKEN_EXPIRY;
|
||||||
|
const accessToken = await jwksManager.sign({
|
||||||
|
sub: 'user:' + user.id,
|
||||||
|
name: user.username,
|
||||||
|
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
||||||
|
});
|
||||||
|
await oauth.setJwksToken(accessToken, { id: user.id, expire: expiresIn });
|
||||||
|
|
||||||
|
const token = {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: accessToken,
|
||||||
|
token: accessToken,
|
||||||
|
refreshTokenExpiresIn: expiresIn,
|
||||||
|
accessTokenExpiresIn: expiresIn,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'jwks',
|
||||||
|
...token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'jwks', opts: TokenOptions = {}) {
|
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'jwks', opts: TokenOptions = {}) {
|
||||||
const { id, username, type } = this;
|
const { id, username, type } = this;
|
||||||
const oauthUser: OauthUser = {
|
const oauthUser: OauthUser = {
|
||||||
@@ -90,30 +119,12 @@ export class User {
|
|||||||
oauthUser.orgId = id;
|
oauthUser.orgId = id;
|
||||||
}
|
}
|
||||||
if (loginType === 'jwks') {
|
if (loginType === 'jwks') {
|
||||||
const expiresIn = opts?.expire ?? 2 * 3600; // 2 hours
|
return await User.createJwksTokenResponse(this, opts);
|
||||||
const accessToken = await jwksManager.sign({
|
|
||||||
sub: 'user:' + this.id,
|
|
||||||
name: this.username,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
|
||||||
});
|
|
||||||
await oauth.setJwksToken(accessToken, { id: this.id, expire: expiresIn });
|
|
||||||
return {
|
|
||||||
type: 'jwks',
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: accessToken,
|
|
||||||
token: accessToken,
|
|
||||||
refreshTokenExpiresIn: expiresIn,
|
|
||||||
accessTokenExpiresIn: expiresIn
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...opts });
|
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...opts });
|
||||||
return {
|
return {
|
||||||
type: 'default',
|
type: 'default',
|
||||||
accessToken: token.accessToken,
|
...token,
|
||||||
refreshToken: token.refreshToken,
|
|
||||||
token: token.accessToken,
|
|
||||||
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
|
|
||||||
accessTokenExpiresIn: token.accessTokenExpiresIn,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -129,40 +140,63 @@ export class User {
|
|||||||
* @param refreshToken
|
* @param refreshToken
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async refreshToken(refreshToken: string) {
|
static async refreshToken(opts: { refreshToken?: string, accessToken?: string }) {
|
||||||
if (refreshToken?.includes?.('.')) {
|
const { refreshToken, accessToken } = opts;
|
||||||
|
let jwsRefreshToken = accessToken || refreshToken;
|
||||||
|
if (oauth.getTokenType(jwsRefreshToken) === 'jwks') {
|
||||||
// 可能是 jwks token
|
// 可能是 jwks token
|
||||||
const jwksToken = await oauth.getJwksToken(refreshToken);
|
const jwksToken = await oauth.getJwksToken(jwsRefreshToken);
|
||||||
if (!jwksToken) {
|
if (!jwksToken) {
|
||||||
throw new CustomError('Invalid refresh token');
|
throw new CustomError('Invalid refresh token');
|
||||||
}
|
}
|
||||||
const decoded = await jwksManager.decode(refreshToken);
|
const decoded = await jwksManager.decode(jwsRefreshToken);
|
||||||
const sub = decoded.sub;
|
return await User.createJwksTokenResponse({
|
||||||
const username = decoded.name;
|
id: decoded.sub.replace('user:', ''),
|
||||||
const expiresIn = 2 * 3600; // 2 hours
|
username: decoded.name
|
||||||
const newToken = await jwksManager.sign({
|
|
||||||
sub,
|
|
||||||
name: username,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
|
||||||
});
|
});
|
||||||
oauth.setJwksToken(newToken, { id: sub.replace('user:', ''), expire: expiresIn });
|
}
|
||||||
return {
|
if (!refreshToken && !accessToken) {
|
||||||
type: 'jwks',
|
throw new CustomError('Refresh Token or Access Token 必须提供一个');
|
||||||
accessToken: newToken,
|
}
|
||||||
refreshToken: newToken,
|
if (accessToken) {
|
||||||
token: newToken,
|
try {
|
||||||
refreshTokenExpiresIn: expiresIn,
|
const token = await User.refreshTokenByAccessToken(accessToken);
|
||||||
accessTokenExpiresIn: expiresIn,
|
return token;
|
||||||
|
} catch (e) {
|
||||||
|
// access token 无效,继续使用 refresh token 刷新
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const token = await User.refreshTokenByRefreshToken(refreshToken);
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
...token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static async refreshTokenByAccessToken(accessToken: string) {
|
||||||
|
const accessUser = await User.verifyToken(accessToken);
|
||||||
|
if (!accessUser) {
|
||||||
|
throw new CustomError('Invalid access token');
|
||||||
|
}
|
||||||
|
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
||||||
|
if (refreshToken) {
|
||||||
|
return await User.refreshTokenByRefreshToken(refreshToken);
|
||||||
|
} else {
|
||||||
|
await User.oauth.delToken(accessToken);
|
||||||
|
const token = await User.oauth.generateToken(accessUser, {
|
||||||
|
...accessUser.oauthExpand,
|
||||||
|
hasRefreshToken: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
...token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async refreshTokenByRefreshToken(refreshToken: string) {
|
||||||
const token = await oauth.refreshToken(refreshToken);
|
const token = await oauth.refreshToken(refreshToken);
|
||||||
return {
|
return {
|
||||||
type: 'default',
|
type: 'default',
|
||||||
accessToken: token.accessToken,
|
...token
|
||||||
refreshToken: token.refreshToken,
|
|
||||||
token: token.accessToken,
|
|
||||||
accessTokenExpiresIn: token.accessTokenExpiresIn,
|
|
||||||
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -171,30 +205,17 @@ export class User {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async resetToken(refreshToken: string, expand?: Record<string, any>) {
|
static async resetToken(refreshToken: string, expand?: Record<string, any>) {
|
||||||
if (refreshToken?.includes?.('.')) {
|
if (oauth.getTokenType(refreshToken) === 'jwks') {
|
||||||
// 可能是 jwks token
|
// 可能是 jwks token
|
||||||
const jwksToken = await oauth.getJwksToken(refreshToken);
|
const jwksToken = await oauth.getJwksToken(refreshToken);
|
||||||
if (!jwksToken) {
|
if (!jwksToken) {
|
||||||
throw new CustomError('Invalid refresh token');
|
throw new CustomError('Invalid refresh token');
|
||||||
}
|
}
|
||||||
const decoded = await jwksManager.decode(refreshToken);
|
const decoded = await jwksManager.decode(refreshToken);
|
||||||
const sub = decoded.sub;
|
return await User.createJwksTokenResponse({
|
||||||
const username = decoded.name;
|
id: decoded.sub.replace('user:', ''),
|
||||||
const expiresIn = 2 * 3600; // 2 hours
|
username: decoded.name
|
||||||
const newToken = await jwksManager.sign({
|
|
||||||
sub,
|
|
||||||
name: username,
|
|
||||||
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
|
||||||
});
|
});
|
||||||
oauth.setJwksToken(newToken, { id: sub.replace('user:', ''), expire: expiresIn });
|
|
||||||
return {
|
|
||||||
type: 'jwks',
|
|
||||||
accessToken: newToken,
|
|
||||||
refreshToken: newToken,
|
|
||||||
token: newToken,
|
|
||||||
refreshTokenExpiresIn: expiresIn,
|
|
||||||
accessTokenExpiresIn: expiresIn,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return await oauth.resetToken(refreshToken, expand);
|
return await oauth.resetToken(refreshToken, expand);
|
||||||
}
|
}
|
||||||
@@ -296,10 +317,9 @@ export class User {
|
|||||||
const users = await query.limit(1);
|
const users = await query.limit(1);
|
||||||
return users.length > 0 ? new User(users[0]) : null;
|
return users.length > 0 ? new User(users[0]) : null;
|
||||||
}
|
}
|
||||||
static findByunionid() {
|
/**
|
||||||
|
* 创建新用户
|
||||||
}
|
*/
|
||||||
|
|
||||||
static async createUser(username: string, password?: string, description?: string) {
|
static async createUser(username: string, password?: string, description?: string) {
|
||||||
const user = await User.findOne({ username });
|
const user = await User.findOne({ username });
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|||||||
@@ -298,6 +298,28 @@ export class OAuth<T extends OauthUser> {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取token类型:jwks, secretKey, accessToken, refreshToken
|
||||||
|
* @param token 要检查的token
|
||||||
|
* @returns token类型或null
|
||||||
|
*/
|
||||||
|
getTokenType(token: string) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (token.includes('.')) {
|
||||||
|
return 'jwks';
|
||||||
|
}
|
||||||
|
if (token.startsWith('sk_')) {
|
||||||
|
return 'secretKey';
|
||||||
|
}
|
||||||
|
if (token.startsWith('st_')) {
|
||||||
|
return 'accessToken';
|
||||||
|
}
|
||||||
|
if (token.startsWith('rk_')) {
|
||||||
|
return 'refreshToken';
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 刷新token
|
* 刷新token
|
||||||
* @param refreshToken
|
* @param refreshToken
|
||||||
@@ -406,18 +428,23 @@ export class OAuth<T extends OauthUser> {
|
|||||||
*/
|
*/
|
||||||
async setJwksToken(token: string, opts: { id: string; expire: number }) {
|
async setJwksToken(token: string, opts: { id: string; expire: number }) {
|
||||||
const expire = opts.expire ?? 2 * 3600; // 2 hours
|
const expire = opts.expire ?? 2 * 3600; // 2 hours
|
||||||
const id = opts.id || '';
|
const id = opts.id || '-';
|
||||||
// jwks token的过期时间比accessToken多3天,确保3天内可以用来refresh token
|
// jwks token的过期时间比accessToken多3天,确保3天内可以用来refresh token
|
||||||
const addExpire = 3 * 24 * 3600;
|
const addExpire = 3 * 24 * 3600;
|
||||||
await this.store.redis.set('user:jwks:' + token, id, 'EX', expire + addExpire);
|
await this.store.redis.set('user:jwks:' + token, id, 'EX', expire + addExpire);
|
||||||
}
|
}
|
||||||
async deleteJwsToken(token: string) {
|
async deleteJwksToken(token: string) {
|
||||||
await this.store.redis.expire('user:jwks:' + token, 0);
|
await this.store.redis.expire('user:jwks:' + token, 0);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取后就删除jwks token,确保token只能使用一次。
|
||||||
|
* @param token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async getJwksToken(token: string) {
|
async getJwksToken(token: string) {
|
||||||
const id = await this.store.redis.get('user:jwks:' + token);
|
const id = await this.store.redis.get('user:jwks:' + token);
|
||||||
if (id) {
|
if (id) {
|
||||||
this.deleteJwsToken(token);
|
this.deleteJwksToken(token);
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ app
|
|||||||
.route({
|
.route({
|
||||||
path: 'user',
|
path: 'user',
|
||||||
key: 'switchCheck',
|
key: 'switchCheck',
|
||||||
|
description: '切换用户或切换为用户组,获取切换后的token',
|
||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
@@ -263,23 +264,13 @@ app
|
|||||||
const { username, accessToken } = ctx.query.data || {};
|
const { username, accessToken } = ctx.query.data || {};
|
||||||
|
|
||||||
if (accessToken && username) {
|
if (accessToken && username) {
|
||||||
const accessUser = await User.verifyToken(accessToken);
|
const result = await User.refreshToken({ accessToken });
|
||||||
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
if (result.accessToken) {
|
||||||
if (refreshToken) {
|
console.log('refreshToken result', result);
|
||||||
const result = await User.refreshToken(refreshToken);
|
|
||||||
createCookie(token, ctx);
|
|
||||||
|
|
||||||
ctx.body = result;
|
|
||||||
return;
|
|
||||||
} else if (accessUser) {
|
|
||||||
await User.oauth.delToken(accessToken);
|
|
||||||
const result = await User.oauth.generateToken(accessUser, {
|
|
||||||
...accessUser.oauthExpand,
|
|
||||||
hasRefreshToken: true,
|
|
||||||
});
|
|
||||||
createCookie(result, ctx);
|
createCookie(result, ctx);
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
return;
|
} else {
|
||||||
|
ctx.throw(500, 'Refresh Token Failed, please login again');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await ctx.call(
|
const result = await ctx.call(
|
||||||
@@ -355,18 +346,19 @@ app
|
|||||||
args: {
|
args: {
|
||||||
data: z.object({
|
data: z.object({
|
||||||
refreshToken: z.string().describe('刷新token'),
|
refreshToken: z.string().describe('刷新token'),
|
||||||
|
accessToken: z.string().optional().describe('使用访问token去刷新token,如果提供了访问token,优先使用访问token去刷新token,刷新失败才会使用refreshToken去刷新'),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { refreshToken } = ctx.query.data || {};
|
const { refreshToken, accessToken } = ctx.query.data || {};
|
||||||
try {
|
try {
|
||||||
if (!refreshToken) {
|
if (!refreshToken && !accessToken) {
|
||||||
ctx.throw(400, 'Refresh Token is required');
|
ctx.throw(400, 'Refresh Token or Access Token 必须提供一个');
|
||||||
}
|
}
|
||||||
const result = await User.refreshToken(refreshToken);
|
const result = await User.refreshToken({ accessToken, refreshToken });
|
||||||
if (result) {
|
if (result.accessToken) {
|
||||||
console.log('refreshToken result', result);
|
console.log('refreshToken result', result);
|
||||||
createCookie(result, ctx);
|
createCookie(result, ctx);
|
||||||
ctx.body = result;
|
ctx.body = result;
|
||||||
|
|||||||
Reference in New Issue
Block a user