Browse Source

feat: 更新路由中间件,添加全局认证配置,重构权限验证逻辑,尝试路由权限配置

route
npmrun 1 month ago
parent
commit
d079341238
  1. 9
      src/controllers/Page/HtmxController.js
  2. 2
      src/controllers/Page/PageController.js
  3. 10
      src/controllers/userController.js
  4. 36
      src/middlewares/Auth/auth.js
  5. 16
      src/middlewares/install.js
  6. 32
      src/utils/autoRegister.js
  7. 50
      src/utils/router.js
  8. 45
      src/utils/router/RouteAuth.js

9
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

2
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
}

10
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)

36
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()
}

16
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: [],
})

32
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()
}
})
})()
}

50
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 })
}
/**

45
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, "")
}
Loading…
Cancel
Save