diff --git a/src/auth/models/user.ts b/src/auth/models/user.ts index 07e34b6..033953e 100644 --- a/src/auth/models/user.ts +++ b/src/auth/models/user.ts @@ -38,6 +38,11 @@ export const redis = useContextKey('redis'); type TokenOptions = { expire?: number; // 过期时间,单位秒 + ip?: string; // 用户ID,默认为当前用户ID + browser?: string; // 浏览器信息 + host?: string; // 主机信息 + wx?: any; + loginWith?: string; // 登录方式,如 'cli', 'web', 'plugin' 等 } /** * 用户模型,使用 Drizzle ORM @@ -85,21 +90,25 @@ 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, }); - const expiresIn = opts?.expire ?? 2 * 3600; // 2 hours + await oauth.setJwksToken(accessToken, { id: this.id, expire: expiresIn }); return { + type: 'jwks', accessToken: accessToken, - refreshToken: null, + refreshToken: accessToken, token: accessToken, - refreshTokenExpiresIn: null, + refreshTokenExpiresIn: expiresIn, accessTokenExpiresIn: expiresIn }; } const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...opts }); return { + type: 'default', accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken, @@ -121,8 +130,73 @@ export class User { * @returns */ static async refreshToken(refreshToken: string) { + if (refreshToken?.includes?.('.')) { + // 可能是 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, + }); + oauth.setJwksToken(newToken, { id: sub.replace('user:', ''), expire: expiresIn }); + return { + type: 'jwks', + accessToken: newToken, + refreshToken: newToken, + token: newToken, + refreshTokenExpiresIn: expiresIn, + accessTokenExpiresIn: expiresIn, + } + } const token = await oauth.refreshToken(refreshToken); - return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken }; + return { + type: 'default', + accessToken: token.accessToken, + refreshToken: token.refreshToken, + token: token.accessToken, + accessTokenExpiresIn: token.accessTokenExpiresIn, + refreshTokenExpiresIn: token.refreshTokenExpiresIn, + }; + } + /** + * 重置token,立即过期token + * @param token + * @returns + */ + static async resetToken(refreshToken: string, expand?: Record) { + if (refreshToken?.includes?.('.')) { + // 可能是 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, + }); + 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); } static async getOauthUser(token: string) { return await UserSecret.verifyToken(token); diff --git a/src/auth/oauth/oauth.ts b/src/auth/oauth/oauth.ts index 5880dd9..5511d7e 100644 --- a/src/auth/oauth/oauth.ts +++ b/src/auth/oauth/oauth.ts @@ -74,7 +74,7 @@ interface Store { delKeys: (keys: string[]) => Promise; } -type TokenData = { +export type TokenData = { accessToken: string; accessTokenExpiresIn?: number; refreshToken?: string; @@ -401,4 +401,24 @@ export class OAuth { const tokens = await this.store.keys('*'); await this.store.delKeys(tokens); } + /** + * 设置 jwks token, 用于jwt的验证, 过期时间为2小时 + */ + async setJwksToken(token: string, opts: { id: string; expire: number }) { + const expire = opts.expire ?? 2 * 3600; // 2 hours + const id = opts.id || ''; + // jwks token的过期时间比accessToken多3天,确保3天内可以用来refresh token + const addExpire = 3 * 24 * 3600; + await this.store.redis.set('user:jwks:' + token, id, 'EX', expire + addExpire); + } + async deleteJwsToken(token: string) { + await this.store.redis.expire('user:jwks:' + token, 0); + } + async getJwksToken(token: string) { + const id = await this.store.redis.get('user:jwks:' + token); + if (id) { + this.deleteJwsToken(token); + } + return id; + } } diff --git a/src/modules/domain.ts b/src/modules/domain.ts index 41bdcdc..42e78ef 100644 --- a/src/modules/domain.ts +++ b/src/modules/domain.ts @@ -5,3 +5,5 @@ import { useKey } from "@kevisual/use-config"; * 用来放cookie的域名 */ export const proxyDomain = useKey('PROXY_DOMAIN') || ''; // 请在这里填写你的域名 + +export const baseProxyUrl = proxyDomain ? `https://${proxyDomain}` : 'https://kevisual.cn'; diff --git a/src/modules/v1-ws-proxy/proxy.ts b/src/modules/v1-ws-proxy/proxy.ts index 823f3e6..b4b37b7 100644 --- a/src/modules/v1-ws-proxy/proxy.ts +++ b/src/modules/v1-ws-proxy/proxy.ts @@ -6,6 +6,7 @@ import { logger } from '../logger.ts'; import { getLoginUser } from '@/modules/auth.ts'; import { createStudioAppListHtml } from '../html/studio-app-list/index.ts'; import { omit } from 'es-toolkit'; +import { baseProxyUrl, proxyDomain } from '../domain.ts'; type ProxyOptions = { createNotFoundPage: (msg?: string) => any; @@ -70,7 +71,7 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt const path = searchParams.get('path'); if (!path) { // 显示前端页面 - const html = fetch('https://kevisual.cn/root/router-studio/index.html').then(res => res.text()); + const html = fetch(`${baseProxyUrl}/root/router-studio/index.html`).then(res => res.text()); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(await html); return true; diff --git a/src/routes/user/me.ts b/src/routes/user/me.ts index 1e2f53c..d08559f 100644 --- a/src/routes/user/me.ts +++ b/src/routes/user/me.ts @@ -9,7 +9,7 @@ import z from 'zod'; * @param ctx * @returns */ -export const createCookie = (token: { accessToken?: string; token?: string }, ctx: any) => { +export const createCookie = (token: { accessToken?: string; token?: string, type?: string; }, ctx: any) => { if (!domain) { return; } @@ -17,6 +17,11 @@ export const createCookie = (token: { accessToken?: string; token?: string }, ct logger.debug('登陆用户没有请求对象,不需要创建cookie'); return } + // if (!token.type || token.type === 'jwks') { + // // 如果是jwks类型的token,不创建cookie, + // // 因为jwks类型的token自己就能检测是否过期了,不需要依赖cookie了 + // return; + // } //TODO, 获取访问的 hostname, 如果访问的和 domain 的不一致,也创建cookie const browser = ctx?.req?.headers['user-agent']; const isBrowser = browser.includes('Mozilla'); // 浏览器 @@ -139,7 +144,7 @@ app } if (tokenUser.id === user.id) { // 自己刷新自己的token - const token = await User.oauth.resetToken(oldToken, { + const token = await User.resetToken(oldToken, { ...tokenUser.oauthExpand, }); createCookie(token, ctx); @@ -156,9 +161,7 @@ app browser: someInfo['user-agent'], host: someInfo.host, }); - createCookie({ - token: token.accessToken - }, ctx); + createCookie(token, ctx); ctx.body = token; }) .addTo(app); @@ -263,10 +266,8 @@ app const accessUser = await User.verifyToken(accessToken); const refreshToken = accessUser.oauthExpand?.refreshToken; if (refreshToken) { - const result = await User.oauth.refreshToken(refreshToken); - createCookie({ - token: result.accessToken - }, ctx); + const result = await User.refreshToken(refreshToken); + createCookie(token, ctx); ctx.body = result; return; @@ -276,9 +277,7 @@ app ...accessUser.oauthExpand, hasRefreshToken: true, }); - createCookie({ - token: result.accessToken - }, ctx); + createCookie(result, ctx); ctx.body = result; return; } @@ -332,18 +331,13 @@ app const orgsList = [tokenUser.username, user.username, , ...orgs]; if (orgsList.includes(username)) { if (tokenUsername === username) { - const result = await User.oauth.resetToken(token); - createCookie({ - token: result.accessToken, - }, ctx); - await User.oauth.delToken(token); + const result = await User.resetToken(token); + createCookie(result, ctx); ctx.body = result; } else { const user = await User.findOne({ username }); const result = await user.createToken(userId, 'default'); - createCookie({ - token: result.accessToken, - }, ctx); + createCookie(result, ctx); ctx.body = result; } } else { @@ -371,12 +365,10 @@ app if (!refreshToken) { ctx.throw(400, 'Refresh Token is required'); } - const result = await User.oauth.refreshToken(refreshToken); + const result = await User.refreshToken(refreshToken); if (result) { console.log('refreshToken result', result); - createCookie({ - token: result.accessToken, - }, ctx); + createCookie(result, ctx); ctx.body = result; } else { ctx.throw(500, 'Refresh Token Failed, please login again'); diff --git a/src/routes/user/web-login.ts b/src/routes/user/web-login.ts index c5d4b30..f4b46c5 100644 --- a/src/routes/user/web-login.ts +++ b/src/routes/user/web-login.ts @@ -119,9 +119,7 @@ app const token = JSON.parse(data); if (token.accessToken) { ctx.body = token; - createCookie({ - token: token.accessToken, - }, ctx); + createCookie(token, ctx); } else { ctx.throw(500, 'Checked error Failed, login failed, please login again'); }