diff --git a/src/controllers/Page/HtmxController.js b/src/controllers/Page/HtmxController.js index 1beabad..edf3cbc 100644 --- a/src/controllers/Page/HtmxController.js +++ b/src/controllers/Page/HtmxController.js @@ -8,15 +8,8 @@ export const Page = (name, data) => async ctx => { import Router from "utils/router.js" export function createRoutes() { - const router = new Router() + const router = new Router({ auth: "try" }) router.post("/clicked", async ctx => { - ctx.cookies.set("token", "sadas", { - httpOnly: true, - // Setting httpOnly to false allows JavaScript to access the cookie - // This enables browsers to automatically include the cookie in requests - sameSite: "lax", - // maxAge: 86400000, // Optional: cookie expiration in milliseconds (e.g., 24 hours) - }) return await ctx.render("htmx/fuck", { title: "HTMX Clicked" }) }) return router diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js index b461917..b20d1ee 100644 --- a/src/controllers/Page/PageController.js +++ b/src/controllers/Page/PageController.js @@ -8,7 +8,7 @@ export const Page = (name, data) => async ctx => { import Router from "utils/router.js" export function createRoutes() { - const router = new Router() + const router = new Router({ auth: "try" }) router.get("/", Index) return router } diff --git a/src/controllers/userController.js b/src/controllers/userController.js index 9a9a537..b00549d 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -26,16 +26,6 @@ export const login = async (ctx) => { try { const { username, email, password } = ctx.request.body const result = await userService.login({ username, email, password }) - if (result && result.token) { - ctx.cookies.set("token", result.token, { - httpOnly: true, - // Setting httpOnly to false allows JavaScript to access the cookie - // This enables browsers to automatically include the cookie in requests - sameSite: "lax", - secure: process.env.NODE_ENV === "production", // Use secure cookies in production - maxAge: 2 * 60 * 60 * 1000, // 2 hours - }) - } ctx.body = formatResponse(true, result) } catch (err) { ctx.body = formatResponse(false, null, err.message) diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js index 76362ce..8fe12a8 100644 --- a/src/middlewares/Auth/auth.js +++ b/src/middlewares/Auth/auth.js @@ -17,12 +17,7 @@ function matchList(list, path) { } function verifyToken(ctx) { - // 优先从 headers 获取 token let token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") - // 如果 headers 没有,则从 cookies 获取 - if (!token) { - token = ctx.cookies.get("authorization") - } if (!token) { return { ok: false, status: -1 } } @@ -53,39 +48,22 @@ export default function authMiddleware(options = { return await next() } if (white.auth === "try") { - const data = verifyToken(ctx) - if (!data.ok && data.status !== -1) { - ctx.cookies.set("authorization", null, { httpOnly: true }); - if (ctx.accepts('html')) { - ctx.redirect(ctx.path+ '?redirectType=1'); - return; - } - } + verifyToken(ctx) return await next() } // true 或其他情况,必须有token if (!verifyToken(ctx).ok) { - if (ctx.accepts('html')) { - ctx.cookies.set("authorization", null, { httpOnly: true }); - return ctx.redirect('/login?redirectType=1'); - } else { - ctx.status = 401 - ctx.body = { success: false, error: "未登录或token缺失或无效" } - return - } + ctx.status = 401 + ctx.body = { success: false, error: "未登录或token缺失或无效" } + return } return await next() } // 非白名单,必须有token if (!verifyToken(ctx).ok) { - if (ctx.accepts('html')) { - ctx.cookies.set("authorization", null, { httpOnly: true }); - return ctx.redirect('/login?redirectType=1'); - } else { - ctx.status = 401 - ctx.body = { success: false, error: "未登录或token缺失或无效" } - return - } + ctx.status = 401 + ctx.body = { success: false, error: "未登录或token缺失或无效" } + return } await next() } diff --git a/src/middlewares/install.js b/src/middlewares/install.js index da1a44b..69ca2d6 100644 --- a/src/middlewares/install.js +++ b/src/middlewares/install.js @@ -18,14 +18,14 @@ export default app => { app.use( auth({ whiteList: [ - // API接口访问 - "/api/login", - "/api/register", - { pattern: "/api/v1/status", auth: "try" }, - { pattern: "/api/**/*", auth: true }, - // 静态资源访问 - { pattern: "/", auth: "try" }, - { pattern: "/**/*", auth: "try" }, + // // API接口访问 + // "/api/login", + // "/api/register", + // { pattern: "/api/v1/status", auth: "try" }, + // { pattern: "/api/**/*", auth: true }, + // // 静态资源访问 + { pattern: "/", auth: false }, + { pattern: "/**/*", auth: false }, ], blackList: [], }) diff --git a/src/utils/autoRegister.js b/src/utils/autoRegister.js index 64b7b30..9ddfd93 100644 --- a/src/utils/autoRegister.js +++ b/src/utils/autoRegister.js @@ -27,7 +27,6 @@ export function autoRegisterControllers(app, controllersDir = path.resolve(__dir controller = (await import(fullPath)).default } const routes = controller.createRoutes || controller.default?.createRoutes || controller.default || controller - // 判断 routes 方法参数个数,支持自动适配 if (typeof routes === "function") { allRouter.push(routes()) } @@ -37,8 +36,35 @@ export function autoRegisterControllers(app, controllersDir = path.resolve(__dir ;(async () => { await scan(controllersDir) - allRouter.forEach(router => { - app.use(router.middleware()) + // TODO: 存在问题:每个Controller都是有顺序的,如果其中一个Controller没有next方法,可能会导致后续的Controller无法执行 + // allRouter.forEach(router => { + // app.use(router.middleware()) + // }) + // 聚合中间件:只分发到匹配的router + app.use(async (ctx, next) => { + let matched = false + for (const router of allRouter) { + // router._matchRoute 只在 router.js 内部,需暴露或用 middleware 包一层 + if (typeof router._matchRoute === "function") { + const route = router._matchRoute(ctx.method.toLowerCase(), ctx.path) + if (route) { + matched = true + await router.middleware()(ctx, next) + break // 命中一个即停止 + } + } else { + // fallback: 直接尝试middleware,若未命中会自动next + const before = ctx.status + await router.middleware()(ctx, next) + if (ctx.status !== before) { + matched = true + break + } + } + } + if (!matched) { + await next() + } }) })() } diff --git a/src/utils/router.js b/src/utils/router.js index 1cabd04..82f8b80 100644 --- a/src/utils/router.js +++ b/src/utils/router.js @@ -1,16 +1,23 @@ import { match } from 'path-to-regexp'; import compose from 'koa-compose'; +import RouteAuth from './router/RouteAuth.js'; class Router { /** * 初始化路由实例 * @param {Object} options - 路由配置 * @param {string} options.prefix - 全局路由前缀 + * @param {Object} options.auth - 全局默认auth配置(可选,优先级低于路由级) */ constructor(options = {}) { - this.prefix = options.prefix || ''; this.routes = { get: [], post: [], put: [], delete: [] }; this.middlewares = []; + this.options = Object.assign({}, this.options, options); + } + + options = { + prefix: '', + auth: true, } /** @@ -24,33 +31,35 @@ class Router { /** * 注册GET路由,支持中间件链 * @param {string} path - 路由路径 - * @param {...Function} handlers - 中间件和处理函数 + * @param {Function} handler - 中间件和处理函数 + * @param {...Object} others - 其他参数(可选) */ - get(path, ...handlers) { - this._registerRoute('get', path, handlers); + get(path, handler, ...others) { + this._registerRoute("get", path, handler, others) } /** * 注册POST路由,支持中间件链 * @param {string} path - 路由路径 - * @param {...Function} handlers - 中间件和处理函数 + * @param {Function} handler - 中间件和处理函数 + * @param {...Object} others - 其他参数(可选) */ - post(path, ...handlers) { - this._registerRoute('post', path, handlers); + post(path, handler, ...others) { + this._registerRoute("post", path, handler, others) } /** * 注册PUT路由,支持中间件链 */ - put(path, ...handlers) { - this._registerRoute('put', path, handlers); + put(path, handler, ...others) { + this._registerRoute("put", path, handler, others) } /** * 注册DELETE路由,支持中间件链 */ - delete(path, ...handlers) { - this._registerRoute('delete', path, handlers); + delete(path, handler, ...others) { + this._registerRoute("delete", path, handler, others) } /** @@ -59,7 +68,7 @@ class Router { * @param {Function} callback - 组路由注册回调 */ group(prefix, callback) { - const groupRouter = new Router({ prefix: this.prefix + prefix }); + const groupRouter = new Router({ prefix: this.options.prefix + prefix }) callback(groupRouter); // 合并组路由到当前路由 Object.keys(groupRouter.routes).forEach(method => { @@ -81,7 +90,16 @@ class Router { const middlewares = [...this.middlewares]; if (route) { ctx.params = route.params; - middlewares.push(...route.handlers); + + let isAuth = this.options.auth; + if (route.meta && route.meta.auth !== undefined) { + isAuth = route.meta.auth; + } + + middlewares.push(RouteAuth({ auth: isAuth })); + middlewares.push(route.handler) + } else { + middlewares.push(RouteAuth({ auth: this.options.auth })); } // 用 koa-compose 组合 @@ -94,11 +112,11 @@ class Router { * 内部路由注册方法,支持中间件链 * @private */ - _registerRoute(method, path, handlers) { - const fullPath = this.prefix + path; + _registerRoute(method, path, handler, others) { + const fullPath = this.options.prefix + path const keys = []; const matcher = match(fullPath, { decode: decodeURIComponent }); - this.routes[method].push({ path: fullPath, matcher, keys, handlers }); + this.routes[method].push({ path: fullPath, matcher, keys, handler, meta: others }) } /** diff --git a/src/utils/router/RouteAuth.js b/src/utils/router/RouteAuth.js new file mode 100644 index 0000000..0c2675f --- /dev/null +++ b/src/utils/router/RouteAuth.js @@ -0,0 +1,45 @@ +import jwt from "@/middlewares/Auth/jwt.js" +import { JWT_SECRET } from "@/middlewares/Auth/auth.js" + +/** + * 路由级权限中间件 + * 支持:auth: false/try/true/roles + * 用法:router.get('/api/user', RouteAuth({ auth: true }), handler) + */ +export default function RouteAuth(options = {}) { + const { auth = true } = options + return async (ctx, next) => { + if (auth === false) return next() + + // 统一用户解析逻辑 + if (!ctx.state.user) { + const token = getToken(ctx) + if (token) { + try { + ctx.state.user = jwt.verify(token, JWT_SECRET) + } catch {} + } + } + + if (auth === "try") { + return next() + } + + if (auth === true) { + if (!ctx.state.user) { + ctx.status = 401 + ctx.body = { success: false, error: "未登录或Token无效" } + return + } + return next() + } + + // 其他自定义模式 + return next() + } +} + +function getToken(ctx) { + // 只支持 Authorization: Bearer xxx + return ctx.headers["authorization"]?.replace(/^Bearer\s/i, "") +}