Browse Source

feat: 更新路由和中间件,添加权限控制,重构认证逻辑

alpha
npmrun 2 months ago
parent
commit
9d1dfa3715
  1. 2
      Dockerfile
  2. 2
      src/controllers/JobController.js
  3. 16
      src/controllers/Page/HtmxController.js
  4. 4
      src/controllers/Page/PageController.js
  5. 2
      src/controllers/StatusController.js
  6. 16
      src/controllers/userController.js
  7. 45
      src/middlewares/Auth/auth.js
  8. 18
      src/middlewares/install.js
  9. 26
      src/utils/router.js
  10. 50
      src/utils/router/RouteAuth.js

2
Dockerfile

@ -1,5 +1,5 @@
# 使用官方 Bun 运行时的轻量级镜像
FROM oven/bun:alpine as base
FROM oven/bun:alpine AS base
WORKDIR /app

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

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

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

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

16
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

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

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

26
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);
};

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