From 9d1dfa3715ca6f1f7a058e93e6b3e1890c1280c9 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 20 Jun 2025 00:06:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=92=8C=E4=B8=AD=E9=97=B4=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6=EF=BC=8C=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- src/controllers/JobController.js | 2 +- src/controllers/Page/HtmxController.js | 16 +++-------- src/controllers/Page/PageController.js | 4 +-- src/controllers/StatusController.js | 2 +- src/controllers/userController.js | 16 ++++------- src/middlewares/Auth/auth.js | 45 ++++++++---------------------- src/middlewares/install.js | 18 ++++++------ src/utils/router.js | 26 ++++++++++++++---- src/utils/router/RouteAuth.js | 50 ++++++++++++++++++++++++++++++++++ 10 files changed, 105 insertions(+), 76 deletions(-) create mode 100644 src/utils/router/RouteAuth.js diff --git a/Dockerfile b/Dockerfile index fec5140..26e907f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # 使用官方 Bun 运行时的轻量级镜像 -FROM oven/bun:alpine as base +FROM oven/bun:alpine AS base WORKDIR /app diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index 3348cf8..9b51957 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -47,7 +47,7 @@ export const updateCron = async (ctx) => { // 路由注册示例 import Router from "utils/router.js" export function createRoutes() { - const router = new Router({ prefix: "/api/jobs" }) + const router = new Router({ prefix: "/api/jobs", auth: true }) router.get("/", list) router.post("/start/:id", start) router.post("/stop/:id", stop) diff --git a/src/controllers/Page/HtmxController.js b/src/controllers/Page/HtmxController.js index 1beabad..f0cdbd3 100644 --- a/src/controllers/Page/HtmxController.js +++ b/src/controllers/Page/HtmxController.js @@ -6,18 +6,10 @@ export const Page = (name, data) => async ctx => { return await ctx.render(name, data) } -import Router from "utils/router.js" +import Router from "utils/router" export function createRoutes() { - const router = new Router() - 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" }) - }) + const router = new Router({ auth: "try" }) + // 如有页面路由可继续添加 + // router.get("/htmx", Index) return router } diff --git a/src/controllers/Page/PageController.js b/src/controllers/Page/PageController.js index b461917..58f4f72 100644 --- a/src/controllers/Page/PageController.js +++ b/src/controllers/Page/PageController.js @@ -6,9 +6,9 @@ export const Page = (name, data) => async ctx => { return await ctx.render(name, data) } -import Router from "utils/router.js" +import Router from "utils/router" export function createRoutes() { - const router = new Router() + const router = new Router({ auth: "try" }) router.get("/", Index) return router } diff --git a/src/controllers/StatusController.js b/src/controllers/StatusController.js index 5239ec0..e3121ab 100644 --- a/src/controllers/StatusController.js +++ b/src/controllers/StatusController.js @@ -11,6 +11,6 @@ export function createRoutes() { ctx.set("X-API-Version", "v1") return next() }) - v1.get("/status", status) + v1.get("/status", { auth: "try" }, status) return v1 } diff --git a/src/controllers/userController.js b/src/controllers/userController.js index 9a9a537..5ee791e 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -26,15 +26,9 @@ 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 - }) + if (result && result.user) { + // 登录成功后写入 session,不再设置 jwt 到 cookie + ctx.session.user = result.user } ctx.body = formatResponse(true, result) } catch (err) { @@ -45,9 +39,9 @@ export const login = async (ctx) => { // 路由注册示例 import Router from "utils/router.js" export function createRoutes() { - const router = new Router({ prefix: "/api" }) + const router = new Router({ prefix: "/api", auth: false }) router.get("/hello", hello) - router.get("/user/:id", getUser) + router.get("/user/:id", { auth: true }, getUser) router.post("/register", register) router.post("/login", login) return router diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js index 76362ce..ff4f8f0 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 } } @@ -35,10 +30,12 @@ function verifyToken(ctx) { } } -export default function authMiddleware(options = { - whiteList: [], - blackList: [] -}) { +export default function authMiddleware( + options = { + whiteList: [], + blackList: [], + } +) { return async (ctx, next) => { // 黑名单优先生效 if (matchList(options.blackList, ctx.path).matched) { @@ -53,39 +50,21 @@ 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缺失或无效" } } await next() } diff --git a/src/middlewares/install.js b/src/middlewares/install.js index da1a44b..12f4d0d 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: "try" }, + // { pattern: "/**/*", auth: "try" }, ], blackList: [], }) @@ -36,7 +36,7 @@ export default app => { extension: "pug", options: { basedir: resolve(__dirname, "../views"), - } + }, }) ) autoRegisterControllers(app) diff --git a/src/utils/router.js b/src/utils/router.js index 1cabd04..33ec54c 100644 --- a/src/utils/router.js +++ b/src/utils/router.js @@ -1,16 +1,19 @@ 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.defaultAuth = options.auth !== undefined ? options.auth : true; } /** @@ -24,7 +27,7 @@ class Router { /** * 注册GET路由,支持中间件链 * @param {string} path - 路由路径 - * @param {...Function} handlers - 中间件和处理函数 + * @param {...Function|Object} handlers - 中间件和处理函数,支持最后一个参数为auth配置对象 */ get(path, ...handlers) { this._registerRoute('get', path, handlers); @@ -33,7 +36,7 @@ class Router { /** * 注册POST路由,支持中间件链 * @param {string} path - 路由路径 - * @param {...Function} handlers - 中间件和处理函数 + * @param {...Function|Object} handlers - 中间件和处理函数,支持最后一个参数为auth配置对象 */ post(path, ...handlers) { this._registerRoute('post', path, handlers); @@ -59,7 +62,7 @@ class Router { * @param {Function} callback - 组路由注册回调 */ group(prefix, callback) { - const groupRouter = new Router({ prefix: this.prefix + prefix }); + const groupRouter = new Router({ prefix: this.prefix + prefix, auth: this.defaultAuth }); callback(groupRouter); // 合并组路由到当前路由 Object.keys(groupRouter.routes).forEach(method => { @@ -77,14 +80,25 @@ class Router { const { method, path } = ctx; const route = this._matchRoute(method.toLowerCase(), path); - // 组合全局中间件、路由专属中间件和 handler + // 组合全局中间件、默认RouteAuth、路由专属中间件和 handler const middlewares = [...this.middlewares]; if (route) { ctx.params = route.params; + // 路由级auth优先,其次Router构造参数auth,最后默认true + let routeAuthConfig = this.defaultAuth; + if (route.handlers.length && typeof route.handlers[0] === 'object' && route.handlers[0].hasOwnProperty('auth')) { + routeAuthConfig = { ...route.handlers[0] }; + route.handlers = route.handlers.slice(1); + } else if (typeof routeAuthConfig !== 'object') { + routeAuthConfig = { auth: routeAuthConfig }; + } + middlewares.push(RouteAuth(routeAuthConfig)); middlewares.push(...route.handlers); + } else { + // 未命中路由也加默认RouteAuth + let defaultAuthConfig = typeof this.defaultAuth === 'object' ? this.defaultAuth : { auth: this.defaultAuth }; + middlewares.push(RouteAuth(defaultAuthConfig)); } - - // 用 koa-compose 组合 const composed = compose(middlewares); await composed(ctx, next); }; diff --git a/src/utils/router/RouteAuth.js b/src/utils/router/RouteAuth.js new file mode 100644 index 0000000..21f7c3a --- /dev/null +++ b/src/utils/router/RouteAuth.js @@ -0,0 +1,50 @@ +import jwt from "./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, roles } = 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; + } + if (roles && !roles.includes(ctx.state.user.role)) { + ctx.status = 403; + ctx.body = { success: false, error: "无权限" }; + return; + } + return next(); + } + + // 其他自定义模式 + return next(); + }; +} + +function getToken(ctx) { + // 只支持 Authorization: Bearer xxx + return ctx.headers["authorization"]?.replace(/^Bearer\s/i, ""); +}