feat: 优化token刷新逻辑,支持使用访问token刷新token,增强错误处理
This commit is contained in:
@@ -34,6 +34,9 @@ const usersTable = cfUser;
|
||||
const orgsTable = cfOrgs;
|
||||
const userSecretsTable = cfUserSecrets;
|
||||
|
||||
// 常量定义
|
||||
const JWKS_TOKEN_EXPIRY = 2 * 3600; // 2 hours in seconds
|
||||
|
||||
export const redis = useContextKey<Redis>('redis');
|
||||
|
||||
type TokenOptions = {
|
||||
@@ -77,6 +80,32 @@ export class User {
|
||||
* @param uid
|
||||
* @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 = {}) {
|
||||
const { id, username, type } = this;
|
||||
const oauthUser: OauthUser = {
|
||||
@@ -90,30 +119,12 @@ export class User {
|
||||
oauthUser.orgId = id;
|
||||
}
|
||||
if (loginType === 'jwks') {
|
||||
const expiresIn = opts?.expire ?? 2 * 3600; // 2 hours
|
||||
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
|
||||
};
|
||||
return await User.createJwksTokenResponse(this, opts);
|
||||
}
|
||||
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...opts });
|
||||
return {
|
||||
type: 'default',
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken,
|
||||
token: token.accessToken,
|
||||
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
|
||||
accessTokenExpiresIn: token.accessTokenExpiresIn,
|
||||
...token,
|
||||
};
|
||||
}
|
||||
/**
|
||||
@@ -129,40 +140,63 @@ export class User {
|
||||
* @param refreshToken
|
||||
* @returns
|
||||
*/
|
||||
static async refreshToken(refreshToken: string) {
|
||||
if (refreshToken?.includes?.('.')) {
|
||||
static async refreshToken(opts: { refreshToken?: string, accessToken?: string }) {
|
||||
const { refreshToken, accessToken } = opts;
|
||||
let jwsRefreshToken = accessToken || refreshToken;
|
||||
if (oauth.getTokenType(jwsRefreshToken) === 'jwks') {
|
||||
// 可能是 jwks token
|
||||
const jwksToken = await oauth.getJwksToken(refreshToken);
|
||||
const jwksToken = await oauth.getJwksToken(jwsRefreshToken);
|
||||
if (!jwksToken) {
|
||||
throw new CustomError('Invalid refresh token');
|
||||
}
|
||||
const decoded = await jwksManager.decode(refreshToken);
|
||||
const sub = decoded.sub;
|
||||
const username = decoded.name;
|
||||
const expiresIn = 2 * 3600; // 2 hours
|
||||
const newToken = await jwksManager.sign({
|
||||
sub,
|
||||
name: username,
|
||||
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
||||
const decoded = await jwksManager.decode(jwsRefreshToken);
|
||||
return await User.createJwksTokenResponse({
|
||||
id: decoded.sub.replace('user:', ''),
|
||||
username: decoded.name
|
||||
});
|
||||
oauth.setJwksToken(newToken, { id: sub.replace('user:', ''), expire: expiresIn });
|
||||
return {
|
||||
type: 'jwks',
|
||||
accessToken: newToken,
|
||||
refreshToken: newToken,
|
||||
token: newToken,
|
||||
refreshTokenExpiresIn: expiresIn,
|
||||
accessTokenExpiresIn: expiresIn,
|
||||
}
|
||||
if (!refreshToken && !accessToken) {
|
||||
throw new CustomError('Refresh Token or Access Token 必须提供一个');
|
||||
}
|
||||
if (accessToken) {
|
||||
try {
|
||||
const token = await User.refreshTokenByAccessToken(accessToken);
|
||||
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);
|
||||
return {
|
||||
type: 'default',
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken,
|
||||
token: token.accessToken,
|
||||
accessTokenExpiresIn: token.accessTokenExpiresIn,
|
||||
refreshTokenExpiresIn: token.refreshTokenExpiresIn,
|
||||
...token
|
||||
};
|
||||
}
|
||||
/**
|
||||
@@ -171,30 +205,17 @@ export class User {
|
||||
* @returns
|
||||
*/
|
||||
static async resetToken(refreshToken: string, expand?: Record<string, any>) {
|
||||
if (refreshToken?.includes?.('.')) {
|
||||
if (oauth.getTokenType(refreshToken) === 'jwks') {
|
||||
// 可能是 jwks token
|
||||
const jwksToken = await oauth.getJwksToken(refreshToken);
|
||||
if (!jwksToken) {
|
||||
throw new CustomError('Invalid refresh token');
|
||||
}
|
||||
const decoded = await jwksManager.decode(refreshToken);
|
||||
const sub = decoded.sub;
|
||||
const username = decoded.name;
|
||||
const expiresIn = 2 * 3600; // 2 hours
|
||||
const newToken = await jwksManager.sign({
|
||||
sub,
|
||||
name: username,
|
||||
exp: Math.floor(Date.now() / 1000) + expiresIn,
|
||||
return await User.createJwksTokenResponse({
|
||||
id: decoded.sub.replace('user:', ''),
|
||||
username: decoded.name
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -296,10 +317,9 @@ export class User {
|
||||
const users = await query.limit(1);
|
||||
return users.length > 0 ? new User(users[0]) : null;
|
||||
}
|
||||
static findByunionid() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*/
|
||||
static async createUser(username: string, password?: string, description?: string) {
|
||||
const user = await User.findOne({ username });
|
||||
if (user) {
|
||||
|
||||
Reference in New Issue
Block a user