当前位置: 首页 > news >正文

GoFrame的Gtoken--基于电商项目以及GPT-o3的辅助解惑

在cmd.go中启动管理后台gtoken

// 启动管理后台gtokengfAdminToken, err := StartBackendGToken()if err != nil {return err}

管理后台gtoken结构

// 管理后台相关
func StartBackendGToken() (gfAdminToken *gtoken.GfToken, err error) {gfAdminToken = &gtoken.GfToken{CacheMode:        consts.CacheModeLocal,ServerName:       consts.BackendServerName,LoginPath:        "/login",LoginBeforeFunc:  loginFunc,//在用户请求 /login 接口时,被 最先 调用。//它做了“查库 + 密码哈希比对”的工作LoginAfterFunc:   loginAfterFunc,//在用户请求 /login 接口时,被 最后 调用。//它做了“设置token到cookie”的工作LogoutPath:       "/user/logout",AuthPaths:        g.SliceStr{"/backend/admin/info"},AuthExcludePaths: g.SliceStr{"/admin/user/info", "/admin/system/user/info"}, // 不拦截路径 /user/info,/system/user/info,/system/user,AuthAfterFunc:    authAfterFunc,MultiLogin:       consts.MultiLogin,}err = gfAdminToken.Start()return
}

学习过程

问1:这两段代码是什么意思,有什么作用:

LoginBeforeFunc:  loginFunc,
LoginAfterFunc:   loginAfterFunc,

gtoken.GfToken 结构体里有很多配置项,用来定义“登录、签发 Token、鉴权”这一整套流程,其中就包括

LoginBeforeFunc:  loginFunc,
LoginAfterFunc:   loginAfterFunc,

它们是一前一后的两个回调函数指针(函数名就是变量值),作用类似于“钩子”(hook):

  1. LoginBeforeFunc
    • 在用户请求 /login 接口时,被 最先 调用。
    • 典型任务:
    – 解析并校验账号、密码、验证码等;
    – 若校验失败,直接向客户端返回错误并 r.ExitAll()
    – 若成功,则返回 (userKey, userData)
    - userKey —— 当前用户的唯一标识(如 "admin_12"),gtoken 会把它写进 JWT 的 sub 字段,用来区分不同用户;
    - userData —— 你想放进 Token 或后续流程中的自定义数据(例如整条用户记录)。
    • 在你的代码里,这个函数叫 loginFunc,它做了“查库 + 密码哈希比对”的工作。

  2. LoginAfterFunc
    • 只有当 LoginBeforeFunc 返回成功并且 gtoken 已经为该用户生成完 JWT 之后才会被触发。
    • 典型任务:
    – 自定义登录成功时的 响应格式(把 token、过期时间、权限列表等组装好返回给前端);
    – 写登录日志、统计信息;
    – 还可以在这里给 Session、Redis 打标等。
    • 你的实现是 loginAfterFunc
    – 它从 respData 中拿到刚签发的 tokenuserKey
    – 再去数据库查出角色、权限信息;
    – 最后用统一的 response.JsonExit{token, expireIn, permissions…} 返回给前端。

为什么要分成“前”与“后”?

职责清晰
– Before 只管“验身份”并告诉框架“这是谁”;
– After 只管“返结果”,框架已经帮你生成好 Token,不用重复劳动。

灵活扩展
– 如果要加图形验证码,只需要改 loginFunc
– 如果要给登录响应再塞一张七日签到券,只用改 loginAfterFunc;其它鉴权逻辑完全不用动。

失败快速返回
– 在 Before 阶段就能拦截无效账号/密码,省下后续生成 Token 的开销。

一句话总结:
LoginBeforeFunc 是“登录前置校验钩子”,验证账号密码并返回用户标识;
LoginAfterFunc 是“登录后置处理钩子”,拿到已生成的 Token,自定义返回数据或做额外业务处理。

问2: 这两行代码什么意思,有什么作用,把我当成零基础来讲解

 name := r.Get("name").String()
password := r.Get("password").String()
func loginFunc(r *ghttp.Request) (string, interface{}) {name := r.Get("name").String()password := r.Get("password").String()ctx := context.TODO()if name == "" || password == "" {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()}//验证账号密码是否正确adminInfo := entity.AdminInfo{}err := dao.AdminInfo.Ctx(ctx).Where(dao.AdminInfo.Columns().Name, name).Scan(&adminInfo)if err != nil {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()}if utility.EncryptPassword(password, adminInfo.UserSalt) != adminInfo.Password {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()}// 唯一标识,扩展参数user datareturn consts.GTokenAdminPrefix + strconv.Itoa(adminInfo.Id), adminInfo
}

下面把这两行代码拆成最细的颗粒来讲,假设你完全没写过 Go,也没接触过 Web 框架。

name := r.Get("name").String()
password := r.Get("password").String()

一、整体场景
• 代码所在函数是登录接口的“前置校验”函数(loginFunc),用来读取客户端提交的账号和密码。
rGoFrame 提供的 *ghttp.Request 对象,代表一次 HTTP 请求。里面封装了 URL、Header、Body、表单、Query 等信息。

二、每个小部件含义

  1. name := … / password := …
    := 是 Go 的“短变量声明”语法——左边变量第一次出现即可直接赋值,无需先写类型。
    • 所以 namepassword 被声明成 字符串类型(后面 .String() 返回的就是字符串)。

  2. r.Get("name")
    Get(key) 是 GoFrame 的快捷方法,用一个键名到 请求参数 中去查值。
    • 查找顺序:① URL 查询串(/login?name=abc)→ ② POST 表单 → ③ JSON Body → ④ 路由参数。
    • 返回值类型是 *gvar.Var,一个万能容器,可承载任意类型。

  3. .String()
    *gvar.Var 提供大量类型转换方法,例如 Int(), Bool(), String()
    • 调 .String() 就把内部值安全转成 string。转换失败时返回空串 "",不会 panic。

三、执行过程(一步步)

  1. 浏览器或 APP 发出 POST /login,Body 里携带 name=tom&password=123456(或 JSON 同名字段)。
  2. 服务器进入 loginFunc
    • r.Get("name") 找到参数值 "tom"
    • .String() 转成 Go 的字符串类型;
    • 最终 name 变量值就是 "tom"
    • 同理,password 变量值就是 "123456"

四、为什么要这么写?

  1. 简洁:一行搞定“取值 + 类型转换”,省去繁琐的 ParseFormjson.Unmarshal 等步骤。
  2. 安全:即使请求里根本没带 name 字段,r.Get("name") 也不会报错,.String() 会得到空串 "",后面可以做空值判断。
  3. 统一:不管前端是用 URL Query、表单,还是 JSON,都能通过同一个 Get 方法拿到,代码不必区分 GET/POST/Content-Type。

五、零基础比喻
把一次 HTTP 请求想象成寄到快递站的一个包裹,里面装了许多袋子(Query 参数、表单字段、JSON 字段)。
r.Get("name") 就像对仓库管理员说:“帮我把标签写着 name 的袋子找出来。”
.String() 就像嘱咐管理员:“不管里面本来是什么格式,都请帮我转成纯文本给我。”
:= 则是在你的笔记本上临时记下这两条信息(变量)。

一句话总结
这两行代码的作用是——从本次 HTTP 请求里读取名为 namepassword 的参数,并把它们转成字符串保存到本地变量,为后续的账号密码校验做准备。

问3:ctx := context.TODO()

ctx := context.TODO()

把它拆成几个关键点,像刚才那样一步步讲解。

  1. context
    • 这是 Go 标准库里的包,名字就叫 context
    • 之前比喻过,它相当于“一份随身文件夹”,用来在一条调用链(多个函数、多个 goroutine)之间携带取消信号、超时时间和一些元数据

  2. context.TODO()
    TODO()context 包提供的一个辅助函数,返回值类型是 context.Context
    • 字面意思:“我暂时不知道该用什么 Context,先给我来一个占位的。”
    • 生成的 Context:
    – 没有超时、没有取消信号;
    – 里面也没有任何键值;
    – 但它能满足类型要求,后续可以继续传递或派生真正带超时/取消的子 context。

  3. ctx := …
    := 是短变量声明,创建一个名为 ctx 的变量,并把 context.TODO() 的返回值赋给它。
    • 从此以后,这个函数里就可以把 ctx 传给数据库查询、Redis 操作等需要 Context 的调用。

为什么要这样写?

  1. 占位
    在登陆校验这种场景,代码里确实需要一个 context.Context(因为 DAO 方法 dao.AdminInfo.Ctx(ctx) 要求),但手上没有现成的请求上下文,例如:

    • loginFunc 并不是 GoFrame 常规的 Controller,它接受的是 r *ghttp.Request,却没有直接拿到 r.Context()(标准库上下文)。
    • 或者作者觉得:这里的数据库操作非常短暂,也不需要额外的超时/取消控制。
  2. 简洁
    与其写 context.Background() 或重新从 r 派生,不如先用 context.TODO() 占个位置。如果以后要加超时或链路追踪,只要把这行替换掉就行。

  3. 符合库的签名
    GoFrame 的 DAO 调用是 dao.AdminInfo.Ctx(ctx),必须传 context.Context 实参。context.TODO() 正好满足类型要求。

context.Background() 的区别?

两者返回的 Context 功能几乎一样,主要是语义差异

函数语义
context.Background()“这是根 Context,用在 main、init 或测试的起点位置。”
context.TODO()“我现在不确定用哪种 Context,先 TODO 以后再说。”

在实际代码里,两者可以互换;但用 TODO() 可以提醒后续阅读代码的人:“这里可能还要补充更合适的 Context(例如带超时、带 Trace-ID)”。

一句话总结

ctx := context.TODO() 的作用就是临时创建一份最简单的 Context 作为占位符,让后面的数据库查询等函数满足签名要求;如果未来需要加超时或取消逻辑,再把 TODO() 换成更合适的 WithTimeoutWithCancel 等派生 Context。

问4:

if name == "" || password == "" {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()
}

代码片段

if name == "" || password == "" {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()
}

逐行拆解 & “小白”式说明

  1. if name == "" || password == "" {
    if:条件判断语句。
    name == "":检查变量 name 是否是空字符串——说明客户端没有提交账号。
    ||:逻辑“或”,只要左边或右边有一个成立就算整个条件为真。
    password == "":同理,检查密码是否为空。
    ➜ 只要 账号或密码其中一个没填,就进入大括号里的代码块。

  2. r.Response.WriteJson(... )
    r 是本次 HTTP 请求对象。
    Response 拿到响应写入器。
    WriteJson(xxx)xxx 转成 JSON 格式写回给前端浏览器 / App。
    gtoken.Fail(msg) 是 gtoken 提供的快捷构造函数,返回形如

    {"code": -1, "msg": "登录失败:账号或密码不能为空"}
    

    这里用到的 msg 常量就是 consts.ErrLoginFaulMsg(统一的错误提示)。

  3. r.ExitAll()
    • GoFrame 的“终止请求”方法。
    • 调用后:
    立刻结束后续所有中间件、后续 Handler 的执行;
    – 把刚刚写入的 JSON 直接发给客户端;
    – 相当于在这里就“短路”返回,不再往下跑数据库查询等逻辑。
    • 这样既省资源,又避免在账号密码缺失的情况下继续执行无意义的代码。

为什么要这么写?

  1. 参数快速校验
    登录最基本的前置条件就是“账号、密码不能为空”。这一步如果失败,完全没必要再连接数据库,直接告诉用户即可。

  2. 节约资源 & 统一返回
    • 通过 WriteJson + ExitAll 一步完成错误响应,省去后面各种 return 判断。
    • 使用项目统一的 gtoken.Failconsts.ErrLoginFaulMsg,保证前端拿到的错误格式、错误文案一致。

  3. 可读性好
    前端同学阅读接口文档或抓包时,可以立刻看到登录失败返回码与提示,不用猜测。

一句话总结

这段代码做了“空账号/空密码的兜底校验”:
如有任一为空→立刻返回统一格式的登录失败 JSON → 终止后续处理,既保证安全也节约服务器资源。

问4:adminInfo := entity.AdminInfo{}

代码行

adminInfo := entity.AdminInfo{}

把它拆成最基础的概念来说明:

  1. entity.AdminInfo 是什么?
    entity 包里放的是由 GoFrame 代码生成器根据数据库表生成的“实体结构体”。
    AdminInfo 对应数据库中的 管理员表(通常叫 admin_info 或类似)。
    • 结构体字段一般包含 Id、Name、Password、UserSalt、RoleIds、IsAdmin、CreateTime 等。

  2. :=(短变量声明)
    • Go 语法,第一次使用变量时无需显式写类型,编译器会根据右边推断。

  3. {}(复合字面量)
    • 用空大括号创建一个“全零值”的 AdminInfo 实例:

    • 数字字段默认 0
    • 字符串字段默认 ""
    • 指针 / 切片 / 时间字段默认 nil
  4. 结果
    • 在内存中开辟了一块空间,名为 adminInfo,类型是 entity.AdminInfo,内容全为空。
    • 变量作用域:从这一行开始,到当前函数 loginFunc 结束为止。

  5. 接下来会怎么用?
    • 下面紧跟的数据库查询:

    err := dao.AdminInfo.Ctx(ctx).Where(dao.AdminInfo.Columns().Name, name).Scan(&adminInfo)
    

    Scan(&adminInfo) 把查出的管理员记录 填充 进这块已经准备好的结构体。
    • 这样,查询成功后 adminInfo.PasswordadminInfo.UserSalt 等字段就有值,可继续做密码比对、权限处理。

一句话总结

这行代码声明并初始化了一个空的管理员实体 adminInfo,为后面把数据库查询结果扫描进来做准备。

问5:dao.AdminInfo.Ctx(ctx).Where(dao.AdminInfo.Columns().Name, name).Scan(&adminInfo)

代码行

err := dao.AdminInfo.Ctx(ctx).Where(dao.AdminInfo.Columns().Name, name).Scan(&adminInfo)

这是一次 “把账号为 name 的管理员信息从数据库查出来,填充到 adminInfo 变量” 的完整操作。
下面把它拆成 7 个小部件,逐一解释。


  1. dao.AdminInfo
    dao 包里封装了对每张表的“数据访问对象” (DAO)。
    AdminInfo 就是对应管理员表的 DAO,内部已经预设了表名、字段映射等。

  2. .Ctx(ctx)
    • 给 DAO 注入 context
    – 让这次数据库操作带上超时 / 取消信号;
    – 方便链路追踪。
    • 返回值依旧是一个可链式调用的 *gdb.Model。

  3. .Where(dao.AdminInfo.Columns().Name, name)
    • 构造 SQL 的 WHERE 条件
    dao.AdminInfo.Columns().Name 是一种更安全的写法:
    – Columns() 返回一个结构体,里面把所有字段名都做成常量,防止手写字符串出错。
    – 这里等价于 "name" 这一列。
    • 最终效果:WHERE name = ??name 变量安全绑定,防 SQL 注入)。

  4. .Scan(&adminInfo)
    执行查询语句,并把结果“扫描”进传入的结构体指针里。
    • 查询不到数据 → Scan 返回 sql.ErrNoRows(在 GoFrame 中会包装成 gdb.ErrNoRows);
    • 查询到数据 → adminInfo 的各字段被填充,如 Id, Password, UserSalt …

  5. 整行最前面的 err :=
    • 短变量声明,接收 Scan 返回的 error
    • 成功时 err == nil,失败时可根据 err 判断是“没查到”还是数据库异常。

  6. 换行的写法 .\n Where(...).\n Scan(...)
    • Go 支持在点 . 之后换行,方便阅读长链式调用。
    • 语义与一行写完完全相同。

  7. 最终结果
    • 若账号存在 → adminInfo 得到该管理员完整记录 → 代码后面可用它做密码比对。
    • 若账号不存在 / SQL 报错 → err != nil → 随后的逻辑会走错误分支,返回“登录失败”。


“小白”式类比

  • dao.AdminInfo 想成“去管理员档案室”;
  • .Where(...) 像告诉管理员:“帮我找 ‘name = tom’ 的那一份”;
  • .Scan(&adminInfo) 就是“把那份档案复印件放进我手里的文件夹 adminInfo”;
  • err 则像柜台小姐告诉你“找到了 / 没找到 / 档案室停电了”。

一句话总结
这行代码使用 GoFrame ORM:

  1. 带着 ctx 访问管理员表;
  2. 根据账号名做等值查询;
  3. 把查到的那一行写入 adminInfo 变量;
  4. 并把查询过程中的错误保存到 err 以便后续判断。

问6:这里的ctx不是没有超时取消信号吗,他不是刚开始定义的context.TODO()吗

对,你观察得很对:
ctx := context.TODO() 本身 没有 设置任何 Deadline,也没有 Cancel-Func,所以它不会像 context.WithTimeoutcontext.WithCancel 那样,在一定时间后把 <-ctx.Done() 关闭。

那为什么还要传它?有没有问题?下面分三点说明。

────────────────────────────────────────

  1. 数据库 API 的「类型刚需」

GoFrame DAO 的链式调用 dao.AdminInfo.Ctx(ctx) 要求第一个参数必须是 context.Context,否则编译通不过。
context.TODO() 至少能满足 类型 要求,让代码先跑起来;如果未来需要真正的超时或取消,再把这一行替换即可。

────────────────────────────────────────
2. 占位符语义:「我以后可能补充」

context.TODO() 的官方说明就是 “I’m not sure which context to use, but I’ll figure it out later.”
也就是说,作者可能在早期先写通流程,后续再考虑:

// 改进版——给数据库查询加 2 秒超时
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

// 改进版——沿用 HTTP 请求自带的 Context(客户端断开就自动取消)
ctx := r.Context()

────────────────────────────────────────
3. 有什么潜在风险?应该怎么优化?

  • 风险

    • 如果数据库卡住(网络闪断、慢查询),这次登录请求就会一直阻塞,没有人帮你中断。
    • 如果并发很高,僵尸查询会堆积占用连接池,可能拖垮服务。
  • 优化方案

    1. 直接用请求上下文

      ctx := r.Context()        // GoFrame 的 Request 实现了此方法
      

      一旦浏览器断开或网关超时,ctx.Done() 就会关闭,数据库操作会被 driver 取消。

    2. 再加一道超时保护

      ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
      defer cancel()
      

      即便客户端不断开,3 秒到了也强制结束查询。

    3. 监控 + 慢 SQL 日志
      配合 GoFrame 的日志中间件记录查询耗时,方便定位并逐步替换 TODO() 占位。

────────────────────────────────────────
一句话答案
context.TODO() 确实不带超时/取消,它只是一个 临时占位的 Context,用来满足 DAO 的参数类型;真正生产环境应替换为 r.Context()context.WithTimeout(...),否则慢查询或客户端中断都无法被及时取消。

问7:

if utility.EncryptPassword(password, adminInfo.UserSalt) != adminInfo.Password {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()}`

代码片段

if utility.EncryptPassword(password, adminInfo.UserSalt) != adminInfo.Password {r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))r.ExitAll()
}

逐字逐句拆解

  1. utility.EncryptPassword(password, adminInfo.UserSalt)
    utility.EncryptPassword 是项目自写的 加密(准确说是“加盐哈希”)工具函数
    • 传入两个参数:
    password → 刚刚从用户请求里取出的明文密码。
    adminInfo.UserSalt → 这位管理员在数据库中保存的专属 “盐值”。
    • 函数内部通常会先把 盐 + 明文密码 拼接,再做一次不可逆的哈希(如 SHA-256、bcrypt)。
    • 这样即使两个人密码同为 “123456”,加上的盐不同,最后存进数据库的密文也不同,防止彩虹表撞库。
    • 返回值就是“计算出的密码密文”。

  2. adminInfo.Password
    • 这是数据库里存的已经加盐哈希后的密码(当初注册或管理员创建账户时生成并保存)。
    • 类型也是字符串,内容看起来像一长串随机字符。

  3. !=
    • Go 的不等号。
    • 判断“用户刚提交的密码→加盐哈希后的密文”是否与“数据库里的密文”一致。

  4. 整个 if ... { ... }
    条件成立 ⇒ 说明密码 不匹配,账户/密码错误;需要立即返回登录失败。
    条件不成立 ⇒ 密码正确,流程继续往下走。

  5. r.Response.WriteJson(gtoken.Fail(consts.ErrLoginFaulMsg))
    r 是当前 HTTP 请求。
    Response.WriteJson() 把后面的数据结构序列化成 JSON 并写到响应体。
    gtoken.Fail(msg) 是 gtoken 框架提供的快捷函数,生成统一格式:

    {"code": -1, "message": "用户名或密码错误"}
    

    其中 "用户名或密码错误" 这段文字来自常量 consts.ErrLoginFaulMsg

  6. r.ExitAll()
    • GoFrame 的“终止本次请求”方法。
    • 调用后:
    – 立即停止执行后续代码、后续中间件;
    – 立刻把已经写好的 JSON 回给前端;
    – 相当于 return 整条 HTTP 流程。

小白级类比

• 想像你到银行取钱,先在柜台输入账号、密码。
• 银行系统会把你输入的密码 + 你的专属“盐”一起做加密,跟数据库里存的密文比对。
• 一旦对不上,就马上在屏幕上提示“密码错误”,并结束本次交易;不会再往下走“扣款-出钞”这些步骤。
这里的 WriteJson + ExitAll 就是那个“立刻提示并中断”的流程。

一句话总结

这段代码对【用户提交的密码】做 加盐哈希,与数据库中保存的密文进行比对:
– 若不一致 → 立即返回统一格式的“登录失败” JSON,并终止后续逻辑;
– 若一致 → 说明密码正确,函数继续执行下一步登录流程。

问:8return consts.GTokenAdminPrefix + strconv.Itoa(adminInfo.Id), adminInfo

代码行

return consts.GTokenAdminPrefix + strconv.Itoa(adminInfo.Id), adminInfo

它出现在 loginFunc 的最后一行——loginFunc 是 gtoken 的 LoginBeforeFunc,要求必须返回 两个值

  1. 用户唯一标识(string)
  2. 附带的用户数据(interface{},随便什么类型都行)

逐项拆开说明

  1. adminInfo.Id
    • 数据库里查出的管理员主键,类型是 int
    • 假设这位管理员的 Id 是 12

  2. strconv.Itoa(adminInfo.Id)
    strconv 是 Go 标准库里的“字符串转换”包。
    ItoaInteger To ASCII —— 把 int 转成十进制字符串。
    12"12"

  3. consts.GTokenAdminPrefix + ...
    consts.GTokenAdminPrefix 是你们项目定义的常量前缀,例如 "admin_"
    • 字符串拼接 +
    "admin_" + "12""admin_12"

  4. 第一个返回值
    • 整串 "admin_12" 就是 userKey —— gtoken 会把它写入生成的 JWT 的 sub(Subject) 字段,用来区分不同用户。
    • 之所以加前缀,是为了在同一套系统里把不同业务线区分开(前台用户可能用 "user_99",后台管理员用 "admin_12"),避免撞号。

  5. 第二个返回值 adminInfo
    • 整个管理员实体结构体,包含 Name、RoleIds、IsAdmin 等字段。
    • gtoken 会把它挂在此次“登录事件”的上下文中,供后续 loginAfterFuncauthAfterFunc 使用:
    loginAfterFunc 里再次查询权限名单;
    authAfterFunc 每次请求解 Token 后把用户信息写进 r.Context()

  6. 函数效果
    loginFunc 的工作完成:
    • 账号/密码校验成功;
    • 返回 "admin_12" + adminInfo 给 gtoken;
    • gtoken 接着生成真正的 JWT Token,并触发 loginAfterFunc

“小白”级比喻

  • 把后台管理员想象成机场乘客:
    "admin_12" = 机票上的旅客编号(带航司前缀 admin_);
    adminInfo = 旅客整份资料(姓名、护照号、舱位)。
  • 检票口(gtoken)拿到这两样东西后,给乘客盖一个通行章(JWT Token),并把资料传给下一关安检用。

一句话总结

该行代码把“管理员唯一标识”拼成 "admin_<Id>"(string),连同这位管理员的完整资料一起返回给 gtoken——用于后续生成 JWT 和在系统内部流转用户信息。

问10:生成gtoken的代码在哪里

在项目代码里,你不会直接看到一段 “手写 jwt.NewWithClaims(…).SignedString(key)” 的语句,因为签发 Token 的动作被 第三方中间件封装 了。

─────────────────────────────
后台 / 前台用的 gtoken
─────────────────────────────
代码入口

internal/cmd/loginAuth.go → StartBackendGToken()
gfAdminToken := &gtoken.GfToken{LoginPath:       "/login",LoginBeforeFunc: loginFunc,      // 账号密码校验成功后LoginAfterFunc:  loginAfterFunc, // gtoken 已经生成好 Token,并把结果传进来
}
gfAdminToken.Start()                // 在 /login 路由挂载内部 handler

• 当浏览器 POST /backend/login 时,框架流程是
① 调 loginFunc → 你返回 userKeyuserData
gtoken 内部 根据 userKey 调用它自己的 jwt.New() 创建 JWT
③ 生成完毕后,把 token 封装在 respData 结构体里,再触发你的 loginAfterFunc
④ 你在 loginAfterFunc 里通过
go token := respData.GetString("token")
拿到已签好的 Token 并返回给前端。

生成逻辑位于外部依赖

$GOMODCACHE/pkg/mod/github.com/goflyfox/gtoken@vX.X.X/gtoken/handler.go

里面的 login() 方法会调用 jwt-goSignedString 完成真正的签名。

总结一句
• 你只写了 “配置 + 回调”,真正的 Token 由 gtoken(后台/前台)或 gf-jwt(另一套登录接口)内部调用 jwt-go 生成;
• 想看细节,需要去 GOPATH / go mod 缓存里的第三方库源代码中查看对应方法(login()TokenGenerator() 等)。

自定义的登录之后的函数

// 自定义的登录之后的函数
func loginAfterFunc(r *ghttp.Request, respData gtoken.Resp) {if !respData.Success() {respData.Code = 0r.Response.WriteJson(respData)return} else {respData.Code = 1//获得登录用户iduserKey := respData.GetString("userKey")adminId := gstr.StrEx(userKey, consts.GTokenAdminPrefix)//根据id获得登录用户其他信息adminInfo := entity.AdminInfo{}err := dao.AdminInfo.Ctx(context.TODO()).WherePri(adminId).Scan(&adminInfo)if err != nil {return}//通过角色查询权限//先通过角色查询权限idvar rolePermissionInfos []entity.RolePermissionInfoerr = dao.RolePermissionInfo.Ctx(context.TODO()).WhereIn(dao.RolePermissionInfo.Columns().RoleId, g.Slice{adminInfo.RoleIds}).Scan(&rolePermissionInfos)if err != nil {return}permissionIds := g.Slice{}for _, info := range rolePermissionInfos {permissionIds = append(permissionIds, info.PermissionId)}var permissions []entity.PermissionInfoerr = dao.PermissionInfo.Ctx(context.TODO()).WhereIn(dao.PermissionInfo.Columns().Id, permissionIds).Scan(&permissions)if err != nil {return}data := &backend.LoginRes{Type:        consts.TokenType,Token:       respData.GetString("token"),ExpireIn:    consts.GTokenExpireIn, //单位秒,IsAdmin:     adminInfo.IsAdmin,RoleIds:     adminInfo.RoleIds,Permissions: permissions,}response.JsonExit(r, 0, "", data)}return
}

代码片段

if !respData.Success() {respData.Code = 0r.Response.WriteJson(respData)return
} else {// 登录成功时的后续处理…
}

逐行拆解

  1. if !respData.Success() {
    respData 是 gtoken 在“登录流程”中帮你组装好的响应结构体,类型 gtoken.Resp
    Success() 是它自带的方法,返回 true / false——表示这次登录流程(账号密码校验、Token 生成)是否成功。
    ! 是逻辑取反。
    ➜ 这行的意思:如果登录失败,就进入大括号里的处理逻辑;成功则跳过,执行 else { … }

  2. respData.Code = 0
    • 修改返回对象里的 Code 字段。
    • 项目约定:Code = 0 代表“业务失败”,>0 代表成功(而 gtoken 原生可能用 -1 之类)。
    • 这样可以让前端拿到统一的错误码格式。

  3. r.Response.WriteJson(respData)
    r 是当前 HTTP 请求。
    Response.WriteJson(x)respData 序列化成 JSON 并写到响应体。
    • 发送给客户端的典型内容:

    {"code": 0,"msg": "用户名或密码错误","token": "","data": null
    }
    
  4. return
    • 直接从 loginAfterFunc 返回,终止后续代码
    • 也就是说,只有登录成功才会继续走 else { … } 分支,去查询权限、组装成功响应。

  5. else { … }
    • 当 respData.Success()true(登录成功),才进入这里执行成功逻辑:
    – 解析 userKey 拿到用户 ID;
    – 查询角色、权限;
    – 组装成功响应并返回。

为什么要这么写?

  1. 失败快速返回:一旦检测到登录失败,就立即响应客户端,无须执行任何多余的数据库查询或业务逻辑,节省资源。
  2. 统一错误格式:把 Code 设为 0,保持与项目整体的 API 规范一致,前端拿到即可根据 code 判断成功与否。
  3. 代码可读性if !Success() { … return } else { … } 结构让“失败流程”与“成功流程”清晰分离,逻辑更直观。

一句话总结

这段 if 语句在登录后置回调里做了一个早期失败拦截
• 若 gtoken 告诉你“登录失败”,就把 code 改成项目约定的 0,直接将失败结果返回给前端并退出函数;
• 否则进入 else 分支继续处理登录成功后的业务逻辑。

respData:

respData 的类型是 gtoken.Resp 结构体,由 gtoken 框架在整个登录流程结束后自动生成;它是“登录结果”的一份总包装,里面会包含多种字段,例如:

type Resp struct {Code    int         `json:"code"`   // 1 成功,0 或 -1 失败Msg     string      `json:"msg"`    // 提示信息Token   string      `json:"token"`  // 刚签发的 JWTUserKey string      `json:"userKey"`// loginFunc 返回的 userKeyData    interface{} `json:"data"`   // loginFunc 返回的 userData(这里才可能是 adminInfo)
}

关系梳理

  1. loginFunc 返回 (userKey, userData)

    • userKey:如 "admin_12"
    • userData:第二个返回值,你传入了 adminInfo 结构体
  2. gtoken 处理流程

    • 根据 userKey 生成 JWT
    • userKeytoken、以及 userData(即 adminInfo)都塞进 respData
  3. loginAfterFunc 收到参数

    • 第 2 个实参 respData 就是上面描述的 整包结果
    • 你可以在里边通过
      adminInfoStr := respData.GetString("data") // JSON 形式
      
      var adminInfo entity.AdminInfo
      _ = gconv.Struct(respData.GetString("data"), &adminInfo)
      
      重新得到管理员信息。
    • respData 本身并不等同于 adminInfo,它只是把 adminInfo 作为 Data 字段的一部分保存。

一句话:
respData 是 gtoken 打包好的“登录响应对象”;其中的 Data 字段才可能存放 loginFunc 返回的 adminInfo,但二者并不是同一个类型或同一个变量。

代码行

adminId := gstr.StrEx(userKey, consts.GTokenAdminPrefix)

逐个元素拆解

  1. userKey
    • 在前面的代码里:

    userKey := respData.GetString("userKey")   // 例如 "admin_12"
    

    • 这是 loginFunc 返回的第 1 个值,格式固定为

    <前缀><数字ID>
    

    后台管理员约定的前缀存放在常量 consts.GTokenAdminPrefix,通常是 "admin_"

  2. consts.GTokenAdminPrefix
    • 项目常量,值如 "admin_"。用来与前台用户前缀("user_")做区分,防止冲突。

  3. gstr.StrEx(full, prefix) — GoFrame 字符串工具
    • 来自 github.com/gogf/gf/v2/text/gstr 包。
    • 功能:如果 fullprefix 开头,则去掉该前缀并返回剩余部分;否则原样返回。
    • 因此:

    gstr.StrEx("admin_12", "admin_")  // 结果 "12"
    gstr.StrEx("admin_12", "user_")   // 结果 "admin_12" (因为开头对不上)
    
  4. adminId := ...
    := 短变量声明,把函数结果赋值给 adminId(类型 string)。
    • 此时 adminId 的值就是 "12" —— 纯数字 ID 的字符串形式

为什么要这样做?

  1. 快捷提取数字 ID

    • userKey 带前缀便于区分不同身份;
    • 业务查询数据库时只需要裸 ID;
    • 通过 gstr.StrEx 去掉前缀,比手写 strings.TrimPrefix 简洁。
  2. 健壮性

    • 如果哪天改前缀,只需改常量;
    • StrEx 在前缀不匹配时会返回原串,不会 panic。

后续用途

紧跟这行代码的是:

adminInfo := entity.AdminInfo{}
err := dao.AdminInfo.Ctx(context.TODO()).WherePri(adminId).Scan(&adminInfo)

说明 adminId 主要被用来作为主键,去数据库查询这位管理员的完整信息(角色、权限等)。

一句话总结

这行代码把登录成功后拿到的 userKey(如 "admin_12")去掉固定前缀 "admin_",提取出纯数字部分 "12",存入 adminId,以便后续按主键查询管理员详细资料。

var rolePermissionInfos []entity.RolePermissionInfoerr = dao.RolePermissionInfo.Ctx(context.TODO()).WhereIn(dao.RolePermissionInfo.Columns().RoleId, g.Slice{adminInfo.RoleIds}).Scan(&rolePermissionInfos)

把这两行当成一句“去数据库把角色-权限对照表捞出来”的普通中文句子,再细分成小口喂给“零基础”同学。

原代码

var rolePermissionInfos []entity.RolePermissionInfo
err = dao.RolePermissionInfo.Ctx(context.TODO()).WhereIn(dao.RolePermissionInfo.Columns().RoleId, g.Slice{adminInfo.RoleIds}).Scan(&rolePermissionInfos)

一、先说“要干什么”

  1. 我要准备一个空篮子,用来装“角色-权限关系”这类数据。
  2. 去数据库里的“角色_权限关联表” 把符合条件的记录全部捞出来。
  3. 条件是什么? —— 记录里的 role_id 字段要落在(IN)当前管理员持有的角色 ID 里。
  4. 捞完后 把这些记录塞进刚才的空篮子;如果途中出错,把错误记到 err 变量。

二、逐词拆解
― 第 1 行

var rolePermissionInfos []entity.RolePermissionInfo

var:声明变量。
rolePermissionInfos:变量名,意思“角色权限信息列表”。
[]entity.RolePermissionInfo:类型是“切片(动态数组)”,里面的元素长得像数据库里一行“角色-权限”记录。
– 初始值是 nil,相当于一只空篮子。

― 第 2 行(为了排版分了四行写,其实是一条链式语句)

err = dao.RolePermissionInfo.Ctx(context.TODO()).WhereIn(dao.RolePermissionInfo.Columns().RoleId, g.Slice{adminInfo.RoleIds}).Scan(&rolePermissionInfos)
  1. dao.RolePermissionInfo
    • “去角色-权限关联表” 的入口dao 可以理解成“数据仓库服务台”。

  2. .Ctx(context.TODO())
    • 给这次“取货”行动附上一张空白工作单(Context 占位),目前并没写超时等要求。
    • 语法原因:很多库函数都必须带这张纸才能工作。

  3. .WhereIn(字段, 值列表)
    • 加条件:role_id 某个列表里。
    dao.RolePermissionInfo.Columns().RoleId 相当于告诉仓库员:“字段名叫 role_id”。
    g.Slice{adminInfo.RoleIds} 把管理员自己的角色 ID 放进一个“通用切片”。
    – 举例:如果 adminInfo.RoleIds = 3,那这里就是 [3]
    • 最终 SQL 类似

    SELECT * FROM role_permission_info WHERE role_id IN (3);
    
  4. .Scan(&rolePermissionInfos)
    • 让库去执行 SQL,把查到的每一行结果 填到 之前准备的篮子 rolePermissionInfos 里。
    & 表示把篮子的“地址”给它,好直接往里塞东西。

  5. err = ...
    • 如果查库过程中出现问题(比如网络断了、SQL 拼错),错误信息就会存到变量 err
    • 稍后会有 if err != nil { ... } 来判断要不要中断流程。

三、极简比喻

  • 你(代码)带着一张空表格(rolePermissionInfos)去仓库服务台(dao.RolePermissionInfo)。
  • 告诉服务员:“帮我把 role_id 属于 我这张单子里写的 ID 的记录都复印一下。”
  • 服务员按要求查询数据库,复印完塞回你的表格;如果复印机坏了,就把错误写在 err 贴给你。

一句话汇总
这两行代码=
“声明一个装角色-权限关系的空切片 → 用管理员的角色 ID 作为条件 → 从数据库的角色-权限表里把对应记录批量查出 → 填进切片;出错就把错误存到 err。”

permissionIds := g.Slice{}

把这行代码当成“拿一个空购物袋,准备一会儿往里装东西”来看,逐点解释:

  1. g.Slice{}
    g 是 GoFrame 的核心包。
    Slice{} 是它提供的一个快捷别名,等价于原生的 []interface{}{} ——
    也就是“可以放任何类型元素的空切片(动态数组)”。
    • 这里没有放任何元素,所以初始容量和长度都为 0。

  2. :=(短变量声明)
    • 在 Go 里第一次使用变量可以直接用 :=,编译器会从右侧推断类型。
    • 因此 permissionIds 的类型被推断为 g.Slice → 本质就是 []interface{}

  3. permissionIds 的命名
    • 字面意思“权限 ID 列表”。
    • 这个空切片稍后会在 for 循环里不断 append,把每条角色-权限关系中的 PermissionId 丢进去。

  4. 为什么不用 []int{}
    • 因为后面要调用 GoFrame ORM 的 WhereIn 方法,参数类型要求是 []interface{}
    • 用 g.Slice{} 可以省去类型转换,写法更短。

  5. 后续典型使用

    for _, info := range rolePermissionInfos {permissionIds = append(permissionIds, info.PermissionId)
    }
    // 现在 permissionIds 里就装满了权限ID
    dao.PermissionInfo.WhereIn("id", permissionIds).Scan(&permissions)
    

一句话总结
这行代码只是声明并初始化一个能动态存放“权限 ID”的空切片,方便接下来把所有权限 ID 逐个塞进去,再用于下一步查询。

小白式拆解

var permissions []entity.PermissionInfo
err = dao.PermissionInfo.                   // 1. 找到“权限表” 的 DAOCtx(context.TODO()).                // 2. 带上一张“空白 context 工作单”WhereIn(dao.PermissionInfo.Columns().Id, permissionIds). // 3. where id IN (若干权限ID)Scan(&permissions)                  // 4. 把查询到的行装进切片

逐点讲:

  1. var permissions []entity.PermissionInfo

    • 先准备一个空篮子,篮子里要装的东西类型叫 PermissionInfo(对应数据库的权限表,每条记录描述一个按钮/接口的权限)。
  2. dao.PermissionInfo

    • 相当于“去权限表窗口”取资料的服务台。
    • 后面所有操作(加条件、查询)都是针对这张表。
  3. .Ctx(context.TODO())

    • 同之前一样,给查询附上一张“工作单”(context),目前仅占位,没有超时/取消功能。
  4. .WhereIn(dao.PermissionInfo.Columns().Id, permissionIds)

    • 加 SQL 条件:id IN (...)
    • dao.PermissionInfo.Columns().Id 取出列名 "id",防止写错。
    • permissionIds 是上一步从角色-权限关系表里收集到的 一串权限ID,例如 [8, 15, 23]
    • 最终生成的 SQL 片段形似:
      SELECT * FROM permission_info WHERE id IN (8,15,23);
      
  5. .Scan(&permissions)

    • 执行查询,把查到的每一行权限记录“扫描”进之前的篮子 permissions
    • & 取地址,告诉 ORM “你可以往这块内存写数据”。
    • 如果顺利,permissions 现在就变成了
      []entity.PermissionInfo{{Id:8,  Name:"/order/list",  ...},{Id:15, Name:"/user/add",    ...},{Id:23, Name:"/goods/edit",  ...},
      }
      
  6. err = ...

    • 若数据库出问题 (err != nil),后面的逻辑会进入错误分支;
    • err == nil,说明查询成功,可以把 permissions 打包进登录成功响应返回给前端。

一句话总结
这两行代码=“拿着刚刚整理出的权限 ID 列表,到权限表里批量把对应权限记录捞出来,放进 permissions 切片;若查询出错就写到 err”。

一行一行“保姆级”讲解

data := &backend.LoginRes{Type:        consts.TokenType,Token:       respData.GetString("token"),ExpireIn:    consts.GTokenExpireIn, // 单位:秒IsAdmin:     adminInfo.IsAdmin,RoleIds:     adminInfo.RoleIds,Permissions: permissions,
}
response.JsonExit(r, 0, "", data)

────────────────────────────────────────
1️⃣ data := &backend.LoginRes{ ... }

  • backend.LoginRes前后端约定的“后台登录成功响应结构体”。
  • &{} 取地址,创建一个指针类型,省内存又方便传递。
  • 大括号里按字段一一赋值(见 2️⃣–7️⃣)。

────────────────────────────────────────
2️⃣ Type: consts.TokenType,

  • Type 字段告诉前端 “返回的令牌属于哪种类型”,例如 "Bearer"
  • 统一写在常量里,避免散落魔法字符串。

3️⃣ Token: respData.GetString("token"),

  • 从刚才 gtoken 生成的 respData 中读取真正的 JWT 字符串
  • GetString("token") ⤵ 把它塞进 Token 字段,前端后续请求要携带它做认证。

4️⃣ ExpireIn: consts.GTokenExpireIn,

  • Token 多少秒后过期(比如 3600 秒 = 1 小时),前端可以用它做倒计时、自动刷新。

5️⃣ IsAdmin: adminInfo.IsAdmin,

  • 当前登录人是否“超级管理员”。
  • 前端可据此决定 UI:普通管理员隐藏某些按钮。

6️⃣ RoleIds: adminInfo.RoleIds,

  • 这位管理员拥有的角色 ID 列表(或逗号串)。
  • 有些项目权限逻辑在前端就要用到角色信息。

7️⃣ Permissions: permissions,

  • 之前查出的 权限详情数组(接口/按钮标识等)。
  • 前端根据它决定菜单树、按钮显隐。

────────────────────────────────────────
8️⃣ response.JsonExit(r, 0, "", data)

  • 项目自封装的快捷输出函数。
  • 参数含义:(Request对象, code, message, data)
    • 0 → 业务成功码(项目约定)。
    • "" → 暂无额外提示文字。
    • data → 刚刚拼好的 LoginRes 结构体。
  • 效果
    1. 自动把 {code:0,msg:"",data:{...}} 转成 JSON;
    2. 写进 HTTP 响应;
    3. r.ExitAll() 立即结束后续流程。

返回给前端的最终 JSON 示意

{"code": 0,"msg": "","data": {"type": "Bearer","token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....","expireIn": 3600,"isAdmin": 1,"roleIds": "3,5","permissions": [{"id":8,"name":"/order/list", ...},{"id":15,"name":"/user/add",  ...}]}
}

一句话总结
这段代码组装后台登录成功的响应体:把生成的 JWT、过期时间、角色/权限等信息打包进 LoginRes,然后用统一的 JsonExit 函数返回给前端并终止请求处理。

代码位置(StartBackendGToken() 的配置清单里)

AuthAfterFunc:    authAfterFunc,
// 登录鉴权中间件for后台
func authAfterFunc(r *ghttp.Request, respData gtoken.Resp) {var adminInfo entity.AdminInfoerr := gconv.Struct(respData.GetString("data"), &adminInfo)if err != nil {response.Auth(r)return}//todo 这里可以写账号前置校验、是否被拉黑、有无权限等逻辑r.SetCtxVar(consts.CtxAdminId, adminInfo.Id)r.SetCtxVar(consts.CtxAdminName, adminInfo.Name)r.SetCtxVar(consts.CtxAdminIsAdmin, adminInfo.IsAdmin)r.SetCtxVar(consts.CtxAdminRoleIds, adminInfo.RoleIds)r.Middleware.Next()
}

“零基础”拆解

  1. AuthAfterFunc 是什么字段?

    • gtoken.GfToken 结构体里的一个可选回调钩子。
    • 名字里的 “Auth” 指 鉴权(验证 Token);“AfterFunc” 指 “验证通过 之后 要再执行的函数”。
  2. 什么时候会触发?

    • 当一个请求命中了需要鉴权的路径(AuthPaths),gtoken 中间件会:
      ① 先检查请求头里的 Authorization: Bearer <token>
      ② 如果 Token 无效或过期 → 直接 401,不会触发 AuthAfterFunc
      ③ 如果 Token 合法且未过期 → 鉴权 通过,这时 立刻调用 AuthAfterFunc,然后才把请求继续往你的业务 Handler 传递。
  3. 这一行代码在做什么?

    • 把你自己写好的函数 authAfterFunc 注册给 gtoken,告诉框架:
      “每次鉴权成功后,请执行我这段自定义逻辑。”
  4. 你的 authAfterFunc 里干了什么?

    • 拿到 gtoken 解析好的 respData → 还原出管理员信息(adminInfo)。
    • r.SetCtxVar(...) 把管理员的 Id、Name、RoleIds、IsAdmin 塞进本次请求的上下文。
    • r.Middleware.Next() 让请求继续流向真正业务路由。
    • 这样业务层就可以随手 r.GetCtxVar("adminId") 获得已登录用户信息,而不必再自行解 Token 或查 Redis。
  5. 为什么要这样设计?

    • 解耦:把“解 Token → 放上下文”的动作集中在一个地方,业务 Handler 专注于业务本身。
    • 复用:如果以后想给所有请求统一加日志或黑名单过滤,只需改或换一版 authAfterFunc
    • 安全:保证只有鉴权成功的请求,才能拿到上下文里的用户信息,避免越权访问。

小白比喻

想像进公司大楼:

  1. 门禁闸机(gtoken 中间件)先刷卡(验 Token)。
  2. 刷卡成功后,保安把你写在宾客登记簿里(authAfterFunc 把用户信息写进上下文),然后才放你进楼梯口(业务代码)。
  3. 如果卡无效,连登记簿都不会写,直接请你走人(返回 401)。

一句话总结

AuthAfterFunc: authAfterFunc 就是给 gtoken 指定:“当某个受保护接口的 Token 校验通过后,请先执行我的 authAfterFunc,把当前管理员的信息写入上下文等,再继续处理业务逻辑。”

“保姆级”逐行拆解

r.SetCtxVar(consts.CtxAdminId,      adminInfo.Id)
r.SetCtxVar(consts.CtxAdminName,    adminInfo.Name)
r.SetCtxVar(consts.CtxAdminIsAdmin, adminInfo.IsAdmin)
r.SetCtxVar(consts.CtxAdminRoleIds, adminInfo.RoleIds)
r.Middleware.Next()
  1. r 是什么

    • r 代表 当前这一次 HTTP 请求,类型 *ghttp.Request
    • GoFrame 允许在 r 里塞“上下文变量”(键值对),供后面同一次请求的代码读取。
  2. SetCtxVar(key, value) 做了什么

    • 把一条键值对写进请求的内部 context.Context
    • 作用域 仅限本次请求;请求结束后自动销毁,线程安全。
    • 以后在控制器或 Service 里,可以通过
      id := r.GetCtxVar(consts.CtxAdminId).Int()
      
      拿到同样的值,无需再解析 Token 或查数据库。
  3. 四行 SetCtxVar 分别存了哪些东西

行号键(Key)常量存的值(Value)用途示例
CtxAdminIdadminInfo.Id主键 ID,日志、权限校验用
CtxAdminNameadminInfo.Name管理员姓名,操作日志展示
CtxAdminIsAdminadminInfo.IsAdmin0/1 标记:是否超级管理员
CtxAdminRoleIdsadminInfo.RoleIds角色 ID 列表,细粒度权限判断

常量放在 internal/consts 包里,避免魔法字符串、拼写错误。

  1. r.Middleware.Next()

    • 告诉 GoFrame “本中间件逻辑已经处理完,继续执行后面的中间件 / 业务路由”
    • 如果不调用 Next(),请求会在这里被截断,后面的控制器就不会执行。
    • 结合上下文:authAfterFunc 本身是 gtoken 的“鉴权成功钩子”;完成写入上下文变量后,需要 Next() 把请求交还给真正的业务处理链路。
  2. 为什么要放在上下文,而不是全局变量?

    • 线程安全:每次请求一份独立副本,不会交叉污染。
    • 易获取:任何后续函数只要有 *ghttp.Requestcontext.Context 都能拿到,避免层层传参。
    • 生命周期正确:请求结束即释放,内存占用可控。

一句话总结
这几行代码把“当前已通过鉴权的管理员”的关键信息(ID、姓名、是否超管、角色 ID 列表)写入本次 HTTP 请求的上下文中,然后调用 Next() 让请求流继续;这样后续业务代码就能随时读取这些信息来做日志记录或权限判断,而不必再次解析 Token 或查询数据库。

相关文章:

  • 如何轻松地将联系人从 iPhone 转移到 iPhone?
  • window显示驱动开发—输出合并器阶段
  • django FileSystemStorage is located outside of the base path component
  • 【前端隐蔽 Bug 深度剖析:SVG 组件复用中的 ID 冲突陷阱】
  • QT的一些介绍
  • Pinia在多步骤表单中的实践应用
  • DDoS防护体系构建——从基础限速到智能调度
  • C++容器之 forward_list (单向链表)使用说明
  • 层级冲突的处理,弹窗生成遮罩问题
  • http通信测试,模拟客户端
  • 力扣-169.多数元素
  • 三种经典算法无人机三维路径规划对比(SMA、HHO、GWO三种算法),Matlab代码实现
  • 医学图像处理期末复习
  • Ruoyi(若依)整合websocket实现信息推送功能(消息铃铛)
  • Logback示例解析
  • 云徙科技 OMS:让订单管理变得轻松又高效
  • python高校工作室管理系统
  • 【服务器R环境架构】基于 micromamba下载 R 库包
  • linux的基本运维
  • 【网络安全】文件上传型XSS攻击解析
  • 党内法规制度建设网站/百度关键词怎么做
  • 不用安装即可玩的游戏/seo黑帽教学网
  • 怎么做局域网网站/免费留电话号码的广告
  • 免费自助网站建站/什么软件能搜索关键词能快速找到
  • 开装潢公司做网站/目前好的推广平台
  • 基础型网站套餐/福州seo网站推广优化