Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d015a53a12 | ||
|
|
26afe86e5a | ||
|
|
178478190f | ||
|
|
c3d2a40c1f | ||
|
|
e21e65d898 | ||
|
|
746397dd1e | ||
|
|
95c3da0f71 | ||
|
|
86182ce5d7 | ||
|
|
cde8281523 | ||
|
|
5263eb3a5f | ||
|
|
f40efea159 | ||
|
|
722a5b13e0 | ||
|
|
a2dbd51f35 | ||
|
|
241a7c43da | ||
|
|
735a339deb | ||
|
|
b6063cb9f9 | ||
|
|
ef18635509 | ||
|
|
3819ae9d94 | ||
|
|
e486d47e2a | ||
|
|
e6fe90a7c0 | ||
|
|
e00245ea66 | ||
|
|
f7f5d379a9 | ||
|
|
be711334fb |
@@ -1,8 +0,0 @@
|
|||||||
# 统一行尾为 LF:Windows / WSL 共享同一仓库时防止行尾伪差异
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
# 二进制文件不做任何转换
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.gif binary
|
|
||||||
*.ico binary
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Casbin v3 笔记更新方案
|
|
||||||
|
|
||||||
## 范围
|
|
||||||
|
|
||||||
- 仅更新 `note/随手记/casbin学习笔记.md`,不修改项目 Go 依赖。
|
|
||||||
- 本文件记录方案;笔记实施前等待用户确认。
|
|
||||||
|
|
||||||
## 已核实依据
|
|
||||||
|
|
||||||
- GitHub 最新正式 release 为 v3.11.0(2026-08-20)。
|
|
||||||
- v3.11.0 的 module 仍为 `github.com/casbin/casbin/v3`,尽管源码仓库已迁移至 apache/casbin。
|
|
||||||
- 本次查询 Go proxy 的旧模块路径 `@latest` 返回 v3.10.0;不能将 GitHub 最新发布与代理解析结果混为一谈。
|
|
||||||
- 来源:https://github.com/apache/casbin/releases/tag/v3.11.0
|
|
||||||
- 来源:https://raw.githubusercontent.com/apache/casbin/v3.11.0/go.mod
|
|
||||||
|
|
||||||
## 实施顺序
|
|
||||||
|
|
||||||
1. 保留原有学习笔记结构,新增版本基线、v2 到 v3 的迁移注意事项与官方来源。
|
|
||||||
2. 修改示例 import;纠正 CSV 加载错误处理、上下文生命周期、Watcher/自动重载、延迟加载、ABAC 能力边界等过时或绝对化描述。不要把既有能力或新增示例误写成 v3 新能力。
|
|
||||||
3. 完善最小 RBAC 示例错误处理;检查差异、代码块和引用,条件允许时在临时目录验证示例,不修改仓库依赖。
|
|
||||||
|
|
||||||
## 职责与验收
|
|
||||||
|
|
||||||
- librarian:只读核实官方事实(已完成)。
|
|
||||||
- fixer:用户确认后仅实施目标笔记,编辑前确认编码和换行,保持原有中文笔记风格。
|
|
||||||
- 主会话:核对最终 diff 和关键事实,说明实际验证情况及外部依赖限制。
|
|
||||||
|
|
||||||
## 完成状态
|
|
||||||
|
|
||||||
- [x] 已更新 `note/随手记/casbin学习笔记.md`(2026-09-18):头部版本基线(v3.11.0、module path、官方来源、release 与 proxy `@latest` 区别)、三元组只是典型模型、g 的角色继承表述、SyncedEnforcer 并发与 matcher 编译缓存、adapter 表结构非核心标准、LoadPolicyArray/Line 校验责任、只读 adapter、ctx 生命周期与 ContextAdapter/ContextEnforcer、Watcher/Dispatcher/StartAutoLoadPolicy、快照时序保护与去性能硬承诺、最小示例改 `/v3` 并完整处理 error、ABAC 能力边界、govaluate 来源与 GetPolicy 签名、延迟加载顺序。
|
|
||||||
- [x] 示例已抽取到 `/tmp/opencode/casbin-v3-verify` 独立 module(`github.com/casbin/casbin/v3 v3.11.0`)验证:`go run .` 输出 `true false false`;`NewSyncedEnforcer(m)` → `SetAdapter(a)` → `LoadPolicy()` 顺序 `go run ./delayed` 输出 `delayed-load: true`。
|
|
||||||
- 未改动仓库 Go 依赖,未提交 git;最终 diff 与事实由主会话复核。
|
|
||||||
|
|
||||||
## 精简方案(2026-09-18 二次修订)
|
|
||||||
|
|
||||||
- 目标:从 213 行压到约 60–85 行(原长度约 1/3),定位为中文概念速记,不是教程;不写/少写具体 API 使用方法。
|
|
||||||
- 保留:简短版本基线与 import 路径;“授权而非认证”一句话;PERM 四段简洁解释 + role_definition 扩展;核心组件 Model/Policy/Enforcer/Adapter/RoleManager/Watcher 职责表;一条判定链与 `alice/admin/report/read` 极短策略示意(无完整 Go 代码);关键提醒(deny/error 均 fail-closed、并发优先 SyncedEnforcer、内存策略不自动跨实例同步、ABAC 不自动 SQL 过滤、迁移核对 adapter `/v3` 依赖);2–3 条官方来源。
|
|
||||||
- 删除:加载入口对比、SQL 表结构、接口签名、CSV/context 细节、完整程序、只读消费与快照架构、MaxStale/性能讨论、proxy `@latest` 偏差等非核心展开及其历史修订细节。
|
|
||||||
- 约束:避免过度简化造成事实错误;不改代码与依赖;不提交;改动仅限本方案与目标笔记。
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package linkedlist
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node 是单向链表的一个节点,持有数据与指向下一个节点的指针。
|
|
||||||
type Node[T comparable] struct {
|
|
||||||
Data T
|
|
||||||
Next *Node[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
// LinkedList 是持有头指针的单向链表,零值即为空表。
|
|
||||||
type LinkedList[T comparable] struct {
|
|
||||||
head *Node[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertHead 将新节点插入表头。
|
|
||||||
func (l *LinkedList[T]) InsertHead(data T) {
|
|
||||||
l.head = &Node[T]{Data: data, Next: l.head}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertTail 将新节点追加到表尾;空表时新节点直接成为表头。
|
|
||||||
func (l *LinkedList[T]) InsertTail(data T) {
|
|
||||||
newNode := &Node[T]{Data: data}
|
|
||||||
if l.head == nil {
|
|
||||||
l.head = newNode
|
|
||||||
return
|
|
||||||
}
|
|
||||||
current := l.head
|
|
||||||
for current.Next != nil {
|
|
||||||
current = current.Next
|
|
||||||
}
|
|
||||||
current.Next = newNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteHead 删除表头节点;空表时无操作。
|
|
||||||
func (l *LinkedList[T]) DeleteHead() {
|
|
||||||
if l.head != nil {
|
|
||||||
l.head = l.head.Next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTail 删除表尾节点;空表时无操作,仅剩一个节点时表变空。
|
|
||||||
func (l *LinkedList[T]) DeleteTail() {
|
|
||||||
switch {
|
|
||||||
case l.head == nil:
|
|
||||||
case l.head.Next == nil:
|
|
||||||
l.head = nil
|
|
||||||
default:
|
|
||||||
current := l.head
|
|
||||||
for current.Next.Next != nil {
|
|
||||||
current = current.Next
|
|
||||||
}
|
|
||||||
current.Next = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search 返回首个 Data 等于 data 的节点;不存在时返回 nil。
|
|
||||||
func (l *LinkedList[T]) Search(data T) *Node[T] {
|
|
||||||
for current := l.head; current != nil; current = current.Next {
|
|
||||||
if current.Data == data {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len 返回链表的节点个数。
|
|
||||||
func (l *LinkedList[T]) Len() int {
|
|
||||||
count := 0
|
|
||||||
for current := l.head; current != nil; current = current.Next {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse 原地反转链表。
|
|
||||||
func (l *LinkedList[T]) Reverse() {
|
|
||||||
var prev, current *Node[T]
|
|
||||||
current = l.head
|
|
||||||
for current != nil {
|
|
||||||
next := current.Next
|
|
||||||
current.Next = prev
|
|
||||||
prev = current
|
|
||||||
current = next
|
|
||||||
}
|
|
||||||
l.head = prev
|
|
||||||
}
|
|
||||||
|
|
||||||
// String 以 "1 -> 2 -> nil" 的格式输出整条链,便于打印与调试。
|
|
||||||
func (l LinkedList[T]) String() string {
|
|
||||||
var sb strings.Builder
|
|
||||||
for current := l.head; current != nil; current = current.Next {
|
|
||||||
fmt.Fprintf(&sb, "%v -> ", current.Data)
|
|
||||||
}
|
|
||||||
sb.WriteString("nil")
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"lab/algorithms/linkedlist"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var list linkedlist.LinkedList[int] // 零值即为空表
|
|
||||||
list.InsertTail(3)
|
|
||||||
list.InsertTail(5)
|
|
||||||
list.InsertHead(1)
|
|
||||||
list.InsertHead(0)
|
|
||||||
fmt.Println(&list) // 0 -> 1 -> 3 -> 5 -> nil
|
|
||||||
|
|
||||||
list.DeleteHead()
|
|
||||||
list.DeleteTail()
|
|
||||||
fmt.Println(&list) // 1 -> 3 -> nil
|
|
||||||
|
|
||||||
found := list.Search(3)
|
|
||||||
if found != nil {
|
|
||||||
fmt.Printf("找到节点,Data = %d\n", found.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Reverse()
|
|
||||||
fmt.Println(&list) // 3 -> 1 -> nil
|
|
||||||
fmt.Printf("链表长度: %d\n", list.Len())
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
_ "net/http/pprof"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 来自文章 https://www.cnblogs.com/hwnd/p/20682814
|
|
||||||
func simulateRequest() {
|
|
||||||
timeout := 500 * time.Millisecond
|
|
||||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout))
|
|
||||||
defer cancel()
|
|
||||||
resultCh := make(chan string) // 无缓冲!
|
|
||||||
|
|
||||||
// 启动 worker goroutine 处理"请求"
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Duration(rand.Intn(500)+300) * time.Millisecond)
|
|
||||||
select {
|
|
||||||
case <-time.After(timeout):
|
|
||||||
fmt.Println("worker 超时,放弃处理")
|
|
||||||
case <-ctx.Done():
|
|
||||||
case resultCh <- fmt.Sprintf("处理完成,耗时 %dms", rand.Intn(500)+300):
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// TODO: 模拟请求取消或超时的情况(比如 500ms 后调用方不再关心结果)
|
|
||||||
// 如果 worker 还没完成,它就 leak 了
|
|
||||||
select {
|
|
||||||
case resultCh := <-resultCh:
|
|
||||||
fmt.Println(resultCh)
|
|
||||||
case <-ctx.Done():
|
|
||||||
fmt.Println("请求方取消了请求")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
simulateRequest()
|
|
||||||
fmt.Printf("第 %d 个请求处理完毕\n\n", i+1)
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
}
|
|
||||||
fmt.Println("所有请求处理完毕")
|
|
||||||
// 运行后观察:goroutine 数量只增不减
|
|
||||||
http.ListenAndServe(":6060", nil)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
_ "net/http/pprof" // 注册 pprof 相关 handler
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// memoryBlocks 保留一部分内存块,方便内存剖析观察。
|
|
||||||
// 这个全局变量会一直存活,便于分析堆上的对象。
|
|
||||||
var memoryBlocks [64][]byte
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 启动 pprof HTTP 服务
|
|
||||||
go func() {
|
|
||||||
log.Println("pprof server started at http://localhost:6060/debug/pprof/")
|
|
||||||
if err := http.ListenAndServe("localhost:6060", nil); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 模拟 CPU 密集型业务
|
|
||||||
go cpuIntensiveTask()
|
|
||||||
|
|
||||||
// 模拟内存分配型业务
|
|
||||||
go memoryIntensiveTask()
|
|
||||||
|
|
||||||
// 阻塞主协程,让程序持续运行
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fib 递归计算斐波那契数列,非常耗费 CPU
|
|
||||||
func fib(n int) int {
|
|
||||||
if n <= 1 {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
return fib(n-1) + fib(n-2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cpuIntensiveTask 循环执行 CPU 密集型计算
|
|
||||||
func cpuIntensiveTask() {
|
|
||||||
for {
|
|
||||||
_ = fib(30)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// memoryIntensiveTask 循环分配内存并保留在全局切片中
|
|
||||||
func memoryIntensiveTask() {
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
idx := i % len(memoryBlocks)
|
|
||||||
// 每次分配 1MB,替换掉旧的内存块
|
|
||||||
memoryBlocks[idx] = make([]byte, 1<<20) // 1 << 20 = 1MB
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mockUsers = map[string]string{
|
|
||||||
"zhangsan": "123456",
|
|
||||||
"lisi": "654321",
|
|
||||||
}
|
|
||||||
// secretKey = "your-secret-key"
|
|
||||||
hmacSecretKey = "5KiinhGcvoXFmgOttvgMWd9ugadE0KVP"
|
|
||||||
blockSecretKey = "OLZmfTWHGyjCfJoH1Y8SjbSY"
|
|
||||||
)
|
|
||||||
|
|
||||||
const sessionTTL = 60 * 5
|
|
||||||
|
|
||||||
func sessionOptions(maxAge int) sessions.Options {
|
|
||||||
return sessions.Options{
|
|
||||||
Path: "/",
|
|
||||||
Domain: "", // 设置为 "" 表示当前域名
|
|
||||||
MaxAge: maxAge, // 单位为秒。0 表示不写Max-Age(浏览器会话cookie); -1 表示删除 cookie
|
|
||||||
Secure: false, // 生产走 HTTPS 时改 true
|
|
||||||
HttpOnly: true, // true 表示禁止 JS 读取 cookie
|
|
||||||
SameSite: http.SameSiteStrictMode, // 严格模式,仅在同源请求中发送 cookie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 代码来自文章 https://www.cnblogs.com/XY-Heruo/p/22864951
|
|
||||||
func main() {
|
|
||||||
r := gin.Default()
|
|
||||||
|
|
||||||
// Create cookie-based session store with a secret key
|
|
||||||
// 传单个密钥时, 只做签名认证,不加密cookie内容. 认证密钥建议 32 或 64 字节
|
|
||||||
// 传一对密钥时,第一个用于认证(HMAC),第二个用于加密(AES-128, AES-192, AES-256)。加密密钥必须是16、24或32字节长度,对应 AES-128/192/256。
|
|
||||||
// 多对密钥用于密钥轮换,第一对用于当前加密和认证,后续的用于解密和认证旧的cookie。Example: cookie.NewStore([]byte("newAuth"), []byte("newEnc"), []byte("oldAuth"), []byte("oldEnc"))
|
|
||||||
store := cookie.NewStore([]byte(hmacSecretKey), []byte(blockSecretKey))
|
|
||||||
store.Options(sessionOptions(sessionTTL)) // 全局设置 cookie 过期时间为 5 分钟
|
|
||||||
r.Use(sessions.Sessions("mysession", store))
|
|
||||||
|
|
||||||
r.POST("/login", login)
|
|
||||||
|
|
||||||
auth := r.Group("/", authRequired())
|
|
||||||
|
|
||||||
auth.GET("/profile", profile)
|
|
||||||
auth.POST("/logout", logout)
|
|
||||||
|
|
||||||
r.Run(":8080")
|
|
||||||
}
|
|
||||||
|
|
||||||
func login(c *gin.Context) {
|
|
||||||
session := sessions.Default(c)
|
|
||||||
username := c.PostForm("username")
|
|
||||||
password := c.PostForm("password")
|
|
||||||
|
|
||||||
if username == "" || password == "" {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is empty"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if mockUsers[username] != password {
|
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Set("user", username)
|
|
||||||
session.Set("expire_at", time.Now().Add(time.Second*sessionTTL).Unix()) // 设置过期时间
|
|
||||||
if err := session.Save(); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "save session failed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "logged in"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func logout(c *gin.Context) {
|
|
||||||
session := sessions.Default(c)
|
|
||||||
session.Clear()
|
|
||||||
session.Options(sessionOptions(-1)) // 让浏览器删除 cookie
|
|
||||||
if err := session.Save(); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "save session failed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func profile(c *gin.Context) {
|
|
||||||
session := sessions.Default(c)
|
|
||||||
user := session.Get("user").(string)
|
|
||||||
c.JSON(http.StatusOK, gin.H{"user": user})
|
|
||||||
}
|
|
||||||
|
|
||||||
func authRequired() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
session := sessions.Default(c)
|
|
||||||
if session.Get("user") == nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if expireAt, ok := session.Get("expire_at").(int64); !ok || time.Now().Unix() > expireAt {
|
|
||||||
session.Clear()
|
|
||||||
session.Options(sessionOptions(-1))
|
|
||||||
session.Save()
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "expired session"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,15 @@
|
|||||||
module lab
|
module lab
|
||||||
|
|
||||||
go 1.27.0
|
go 1.26.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/sessions v1.1.2
|
github.com/richardlehane/mscfb v1.0.7 // indirect
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||||
golang.org/x/crypto v0.56.0
|
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||||
)
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
|
github.com/xuri/excelize/v2 v2.11.0 // indirect
|
||||||
require (
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
github.com/bytedance/sonic v1.15.2 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
|
||||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.30.3 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.6 // indirect
|
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.23 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.60.0 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
|
||||||
go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect
|
|
||||||
golang.org/x/arch v0.29.0 // indirect
|
|
||||||
golang.org/x/net v0.57.0 // indirect
|
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
|
||||||
golang.org/x/text v0.41.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,100 +1,18 @@
|
|||||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
|
||||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||||
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||||
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||||
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
github.com/gin-contrib/sessions v1.1.2 h1:utWRqOHT/kbqxxqF/mtutV9wfGLi1+uu5YuvWUhj/Ow=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
github.com/gin-contrib/sessions v1.1.2/go.mod h1:qRxYwA/TYJw0wv28oFzs4gY/8G8z69id862SNgX9Gmw=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
|
||||||
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
|
||||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
|
||||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
|
||||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
|
||||||
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
|
||||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
|
||||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
|
||||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
|
||||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
|
||||||
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
|
||||||
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
|
||||||
golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho=
|
|
||||||
golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
|
||||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
|
||||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
|
||||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
|
||||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
|
||||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidPassword = errors.New("invalid password")
|
|
||||||
ErrInvalidHash = errors.New("invalid password hash")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Hasher struct {
|
|
||||||
cost int // cost 可以理解成 bcrypt 的“计算难度”参数,cost 越高,计算 bcrypt hash 就越慢,攻击者暴力破解密码也越贵
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHasher(cost int) (*Hasher, error) {
|
|
||||||
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
|
|
||||||
return nil, errors.New("invalid bcrypt cost")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Hasher{
|
|
||||||
cost: cost,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash 用于注册、重置密码、修改密码。
|
|
||||||
func (h *Hasher) Hash(password string) (string, error) {
|
|
||||||
if len(password) == 0 {
|
|
||||||
return "", ErrInvalidPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
// bcrypt 只使用密码前 72 bytes。
|
|
||||||
// 生产环境建议显式拒绝,而不是静默截断。
|
|
||||||
if len([]byte(password)) > 72 {
|
|
||||||
return "", ErrInvalidPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
hash, err := bcrypt.GenerateFromPassword(
|
|
||||||
[]byte(password),
|
|
||||||
h.cost,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(hash), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify 用于登录时验证密码。
|
|
||||||
func (h *Hasher) Verify(password, encodedHash string) (bool, error) {
|
|
||||||
if len(password) == 0 || len([]byte(password)) > 72 {
|
|
||||||
return false, ErrInvalidPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
if encodedHash == "" {
|
|
||||||
return false, ErrInvalidHash
|
|
||||||
}
|
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword(
|
|
||||||
[]byte(encodedHash),
|
|
||||||
[]byte(password),
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return true, nil
|
|
||||||
|
|
||||||
case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
|
|
||||||
return false, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
// 例如数据库里的 hash 已损坏或格式不合法。
|
|
||||||
return false, ErrInvalidHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NeedsRehash 判断数据库中的 hash 是否使用了较旧的 cost。
|
|
||||||
func (h *Hasher) NeedsRehash(encodedHash string) bool {
|
|
||||||
cost, err := bcrypt.Cost([]byte(encodedHash))
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return cost != h.cost
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBcrypt(t *testing.T) {
|
|
||||||
oldHasher, err := NewHasher(10)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
newHasher, err := NewHasher(12)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
password := "password"
|
|
||||||
oldHash, err := oldHasher.Hash(password)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Log(oldHash)
|
|
||||||
newHash, err := newHasher.Hash(password)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Log(newHash)
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(oldHash), []byte(password))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHashPassword(t *testing.T) {
|
|
||||||
hasher, err := NewHasher(12)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
password := "password"
|
|
||||||
hash, err := hasher.Hash(password)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Log(hash)
|
|
||||||
if ok, err := hasher.Verify(password, hash); err != nil || !ok {
|
|
||||||
t.Fatal("verify failed")
|
|
||||||
}
|
|
||||||
if ok := hasher.NeedsRehash(hash); !ok {
|
|
||||||
t.Fatal("needs rehash failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateTime(t *testing.T) {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
_, err := bcrypt.GenerateFromPassword(
|
|
||||||
[]byte("TestPassword123!"),
|
|
||||||
14,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Log(time.Since(start), err)
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
// 引入两个包进行对比
|
|
||||||
jsonV1 "encoding/json"
|
|
||||||
jsonV2 "encoding/json/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestV2Null(t *testing.T) {
|
|
||||||
type Product struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
}
|
|
||||||
p := Product{
|
|
||||||
Name: "product1",
|
|
||||||
}
|
|
||||||
data1, _ := jsonV1.Marshal(p)
|
|
||||||
data2, _ := jsonV2.Marshal(p)
|
|
||||||
t.Log(string(data1))
|
|
||||||
t.Log(string(data2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOmitZeroVsEmpty(t *testing.T) {
|
|
||||||
// ===== V1 结构体:使用 omitempty =====
|
|
||||||
type EventV1 struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
CreatedAt time.Time `json:"created_at,omitempty"` // ⚠️ omitempty 对 time.Time 无效
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== V2 结构体:使用 omitzero =====
|
|
||||||
type EventV2 struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
CreatedAt time.Time `json:"created_at,omitzero"` // ✅ omitzero 能识别 IsZero()
|
|
||||||
}
|
|
||||||
// 都不给 CreatedAt 赋值,保持 Go 的零值 time.Time{}
|
|
||||||
|
|
||||||
// 1. 测试 v1 + omitempty
|
|
||||||
e1 := EventV1{Title: "Go Meetup"}
|
|
||||||
data1, _ := jsonV1.Marshal(e1)
|
|
||||||
fmt.Println("v1 + omitempty 输出:", string(data1))
|
|
||||||
|
|
||||||
// 2. 测试 v2 + omitzero
|
|
||||||
e2 := EventV2{Title: "Go Meetup"}
|
|
||||||
data2, _ := jsonV2.Marshal(e2)
|
|
||||||
fmt.Println("v2 + omitzero 输出:", string(data2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiStyle(t *testing.T) {
|
|
||||||
type MultiStyleUser struct {
|
|
||||||
// 1. 严格匹配:JSON key 必须精确是 "user_id"
|
|
||||||
ID string `json:"user_id,case:strict"`
|
|
||||||
|
|
||||||
// 2. 忽略大小写:JSON key 是 "Name"、"NAME"、"name" 都能成功赋值
|
|
||||||
Name string `json:"name,case:ignore"`
|
|
||||||
}
|
|
||||||
// 假设第三方传来的 JSON 风格混杂
|
|
||||||
jsonData := []byte(`{
|
|
||||||
"user_id": "1001",
|
|
||||||
"NAME": "Gopher"
|
|
||||||
}`)
|
|
||||||
var m MultiStyleUser
|
|
||||||
jsonV2.Unmarshal(jsonData, &m)
|
|
||||||
t.Log(m)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package testing
|
package testing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -47,63 +46,3 @@ func TestChannel(t *testing.T) {
|
|||||||
var c chan bool // 一个零值nil通道
|
var c chan bool // 一个零值nil通道
|
||||||
<-c // 永久阻塞在此
|
<-c // 永久阻塞在此
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFeature1(t *testing.T) {
|
|
||||||
type Person struct {
|
|
||||||
Sex string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
type Student struct {
|
|
||||||
StudentID int
|
|
||||||
Person
|
|
||||||
}
|
|
||||||
|
|
||||||
s1 := Student{
|
|
||||||
StudentID: 1,
|
|
||||||
Sex: "male",
|
|
||||||
Name: "张三",
|
|
||||||
}
|
|
||||||
t.Log(s1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFeature2(t *testing.T) {
|
|
||||||
// Go 1.27 之前:必须手动写出类型实参 [int],否则编译报错
|
|
||||||
fn1 := IntToString(FormatToString[int])
|
|
||||||
fn2 := IntToString(FormatToString)
|
|
||||||
|
|
||||||
oldFormat := []IntToString{FormatToString[int]}
|
|
||||||
newFormat := []IntToString{FormatToString}
|
|
||||||
|
|
||||||
t.Log(fn1(1))
|
|
||||||
t.Log(fn2(2))
|
|
||||||
for i, fn := range oldFormat {
|
|
||||||
t.Log(fn(i))
|
|
||||||
}
|
|
||||||
for i, fn := range newFormat {
|
|
||||||
t.Log(fn(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一个泛型格式化函数
|
|
||||||
func FormatToString[T any](v T) string {
|
|
||||||
return fmt.Sprintf("value: %v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 具名函数类型:接收一个 int,返回 string
|
|
||||||
type IntToString func(int) string
|
|
||||||
|
|
||||||
func TestSort(t *testing.T) {
|
|
||||||
slice := []int{8, 2, 4, 4}
|
|
||||||
Sort1(slice)
|
|
||||||
t.Log(slice)
|
|
||||||
}
|
|
||||||
func Sort1(slice []int) {
|
|
||||||
for i := 0; i < len(slice)-1; i++ {
|
|
||||||
for j := 0; j < len(slice)-1-i; j++ {
|
|
||||||
if slice[j] > slice[j+1] {
|
|
||||||
slice[j], slice[j+1] = slice[j+1], slice[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
# MySQL 空表 JOIN 导致 ORDER BY 索引失效——实验笔记
|
|
||||||
|
|
||||||
> 环境:MySQL 8.0.18+(hash join 自 8.0.18 引入,实测 8.0.28)
|
|
||||||
> 现象:一条 `ORDER BY ... LIMIT` 查询原本走主表复合索引(免排序),LEFT JOIN 一张**空表**后,执行计划退化为全量读取 + `Using temporary; Using filesort`,主表索引"失效"。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、一句话结论
|
|
||||||
|
|
||||||
**不是空表禁用了别的表的索引,而是空表让优化器为该 JOIN 选择了 hash join;hash join 不保序,导致"借主表索引序完成 ORDER BY"的整个计划作废**,主表随之退化为全量读取 + 临时表排序。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、原理
|
|
||||||
|
|
||||||
### 1. 为什么嵌套循环(Nested Loop Join)能"免排序"
|
|
||||||
|
|
||||||
```
|
|
||||||
按 idx(created_at, id) 倒序逐行读主表 → 每行对被 join 表做索引点查(eq_ref)→ 输出
|
|
||||||
```
|
|
||||||
|
|
||||||
- 嵌套循环的输出顺序 = 驱动表(主表)的访问顺序
|
|
||||||
- 主表走 `(created_at, id)` 复合索引倒序扫描,输出天然满足 `ORDER BY created_at DESC, id DESC`
|
|
||||||
- `LIMIT 10` 可以提前终止:读到 10 行即停,`ORDER BY` 零成本(EXPLAIN 中体现为 `type=index` + `Backward index scan`,无 temporary/filesort)
|
|
||||||
|
|
||||||
### 2. hash join 为什么会破坏这一切
|
|
||||||
|
|
||||||
- hash join 把被 join 表整棵读进内存哈希表,再用驱动表逐行**探测**,输出顺序不再对应任何一方的索引序
|
|
||||||
- 一旦计划中任何一个 JOIN 环节是 hash join,结果集都必须整体重排
|
|
||||||
- 优化器权衡:"反正要排序,主表走哪个访问路径都无所谓" → 放弃排序专用索引,选最便宜的读取方式(全表扫或全索引扫,见第四节变体)
|
|
||||||
|
|
||||||
### 3. 为什么偏偏是空表触发 hash join(反直觉)
|
|
||||||
|
|
||||||
- 空表没有统计信息,优化器估算行数 ≈ 1
|
|
||||||
- 估算"把空表整棵读进哈希表(≈1 次 IO)再探测" 比 "对主表每行做一次 B+ 树点查(N 次树遍历)" 更便宜
|
|
||||||
- 于是选了 hash join —— 对空表本身这是**正确的成本决策**(0 行数据怎么访问都对),副作用是拖垮了主表的排序计划
|
|
||||||
- 表有了真实数据后,统计信息更新,优化器改回索引点查(eq_ref),保序计划自动恢复
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、建表与造数(可直接执行)
|
|
||||||
|
|
||||||
### 1. 主表:10 万行,带 `(created_at, id)` 复合索引
|
|
||||||
|
|
||||||
```sql
|
|
||||||
DROP TABLE IF EXISTS users, user_profiles, user_tags_empty, user_tags_noindex;
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
username VARCHAR(64) NOT NULL,
|
|
||||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_created_id (created_at, id)
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
-- 递归 CTE 造数需要先放开深度限制(仅当前会话)
|
|
||||||
SET SESSION cte_max_recursion_depth = 100001;
|
|
||||||
|
|
||||||
INSERT INTO users (username, nickname, created_at)
|
|
||||||
WITH RECURSIVE seq AS (
|
|
||||||
SELECT 1 AS n
|
|
||||||
UNION ALL
|
|
||||||
SELECT n + 1 FROM seq WHERE n < 100000
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
CONCAT('user_', n),
|
|
||||||
CONCAT('nick_', n),
|
|
||||||
DATE_ADD('2025-01-01 00:00:00', INTERVAL FLOOR(RAND() * 31536000) SECOND)
|
|
||||||
FROM seq;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 对照用被 join 表:有数据,join 列唯一索引
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE user_profiles (
|
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
city VARCHAR(32) NOT NULL DEFAULT '',
|
|
||||||
UNIQUE KEY uk_user_id (user_id)
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
INSERT INTO user_profiles (user_id, city)
|
|
||||||
WITH RECURSIVE seq AS (
|
|
||||||
SELECT 1 AS n
|
|
||||||
UNION ALL
|
|
||||||
SELECT n + 1 FROM seq WHERE n < 100000
|
|
||||||
)
|
|
||||||
SELECT n, CONCAT('city_', FLOOR(RAND() * 100)) FROM seq;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. "罪魁"空表:结构与 user_profiles 完全同构,只是**不插入任何数据**
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE user_tags_empty (
|
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
tag VARCHAR(32) NOT NULL DEFAULT '',
|
|
||||||
UNIQUE KEY uk_user_id (user_id)
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
-- 故意不插数据
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、对照实验
|
|
||||||
|
|
||||||
> 每组实验观察四处:主表 users 的 `type` / `key` / `rows` / `Extra`,以及被 join 表的访问方式。
|
|
||||||
> **注意**:实验 SQL 故意 SELECT 了 `username`/`nickname` 等**索引外列**,原因见实验二的"变体"说明。
|
|
||||||
|
|
||||||
### 实验一(对照组):只 JOIN 有数据的表 → 索引完美生效
|
|
||||||
|
|
||||||
```sql
|
|
||||||
EXPLAIN
|
|
||||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
|
||||||
ORDER BY u.created_at DESC, u.id DESC
|
|
||||||
LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期结果:**
|
|
||||||
|
|
||||||
| table | type | key | rows | Extra |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| u | **index** | **idx_created_id** | **10** | **Backward index scan** |
|
|
||||||
| p | eq_ref | uk_user_id | 1 | (空) |
|
|
||||||
|
|
||||||
主表倒序走复合索引只读 10 行(LIMIT 提前终止),被 join 表逐行点查,无排序开销。保序计划下 SELECT 索引外列不影响走索引(逐行回表而已)。
|
|
||||||
|
|
||||||
### 实验二:仅追加一个 LEFT JOIN 空表 → 计划全面崩坏
|
|
||||||
|
|
||||||
```sql
|
|
||||||
EXPLAIN
|
|
||||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city, t.tag
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
|
||||||
LEFT JOIN user_tags_empty t ON t.user_id = u.id
|
|
||||||
ORDER BY u.created_at DESC, u.id DESC
|
|
||||||
LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期结果(SELECT 含索引外列时):**
|
|
||||||
|
|
||||||
| table | type | key | rows | Extra |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| u | **ALL** | NULL | **100003** | **Using temporary; Using filesort** |
|
|
||||||
| p | eq_ref | uk_user_id | 1 | (空) |
|
|
||||||
| t | **ALL** | NULL | 1 | **Using where; Using join buffer (hash join)** |
|
|
||||||
|
|
||||||
三个观察点:
|
|
||||||
|
|
||||||
1. 空表 t 的索引 `uk_user_id` 明明存在,却 `type=ALL` + hash join(空表上这是合理的成本选择)
|
|
||||||
2. 主表 u 的 `idx_created_id` 被**整个放弃**:全表扫描 + 临时表 + 文件排序,rows 从 10 涨到全量(LIMIT 提前终止失效)
|
|
||||||
3. 注意 p 的 eq_ref 没有受影响——**别的表的索引并没有被禁用,坏掉的是主表的"排序专用访问路径"**
|
|
||||||
|
|
||||||
**变体:主表退化的两种形态(重要,实测踩过的坑)**
|
|
||||||
|
|
||||||
如果 SELECT **只查索引内的列**(如仅 `u.id, u.created_at`),主表退化后不是 `ALL`,而是:
|
|
||||||
|
|
||||||
```
|
|
||||||
u: type=index, key=idx_created_id, rows=100003, Extra: Using index; Using temporary; Using filesort
|
|
||||||
```
|
|
||||||
|
|
||||||
机制:排序计划作废后优化器选"最便宜的读取方式"——查询列全在索引里时,扫**窄的二级索引**(覆盖扫描,`Using index` 免回表)比扫**宽的聚簇索引**(ALL)更便宜。`type=index` 全索引扫描与 `ALL` 全表扫本质相同:**都是全量读取 + 显式排序**,同样没有 LIMIT 提前终止。
|
|
||||||
|
|
||||||
判断"是否退化"的可靠标志是 `rows` 是否涨到全量、`Using temporary; Using filesort` 是否出现,而**不是** type 显示 ALL 还是 index。生产查询通常 SELECT 大量业务列(覆盖不成立),多表现为 ALL 形态;本实验因此故意带上索引外列。
|
|
||||||
|
|
||||||
> 加 `FORMAT=TREE` 可以更直观看到 `Left (hash join)` 节点:
|
|
||||||
> `EXPLAIN FORMAT=TREE <实验二的查询>;`
|
|
||||||
|
|
||||||
### 实验三:FORCE INDEX 把空表钉回点查 → 计划恢复
|
|
||||||
|
|
||||||
```sql
|
|
||||||
EXPLAIN
|
|
||||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city, t.tag
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
|
||||||
LEFT JOIN user_tags_empty t FORCE INDEX (uk_user_id) ON t.user_id = u.id
|
|
||||||
ORDER BY u.created_at DESC, u.id DESC
|
|
||||||
LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期结果:** t 变为 `eq_ref`,主表 u 恢复 `index + Backward index scan, rows=10`,temporary/filesort 消失。
|
|
||||||
|
|
||||||
结论:只要所有 JOIN 都是保序的嵌套循环点查,主表就能借索引序免排序。
|
|
||||||
|
|
||||||
### 实验四:空表填入数据 + 刷新统计信息 → 自愈
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO user_tags_empty (user_id, tag)
|
|
||||||
WITH RECURSIVE seq AS (
|
|
||||||
SELECT 1 AS n
|
|
||||||
UNION ALL
|
|
||||||
SELECT n + 1 FROM seq WHERE n < 100000
|
|
||||||
)
|
|
||||||
SELECT n, 'vip' FROM seq;
|
|
||||||
|
|
||||||
ANALYZE TABLE user_tags_empty;
|
|
||||||
|
|
||||||
-- 重跑实验二的 EXPLAIN
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期结果:** 与实验一/三相同,t 走 `eq_ref`,主表走 `idx_created_id`,无需任何 hint。
|
|
||||||
|
|
||||||
补充观察:只插入少量行(比如 50 行)时优化器可能仍选 hash join——对几十行的表,hash join 确实可能更便宜,这属于**合理决策**而非问题;关注点应是"数据量与计划是否匹配",而不是"必须永远走索引"。
|
|
||||||
|
|
||||||
### 进阶实验五:与空无关——任何原因导致的 hash join 都会触发同样的退化
|
|
||||||
|
|
||||||
证明"根因是 hash join 破坏保序计划,空表只是诱因之一":
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 有 10 万行、但 join 列上【没有索引】的表
|
|
||||||
CREATE TABLE user_tags_noindex (
|
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
tag VARCHAR(32) NOT NULL DEFAULT ''
|
|
||||||
) ENGINE=InnoDB;
|
|
||||||
|
|
||||||
INSERT INTO user_tags_noindex (user_id, tag)
|
|
||||||
WITH RECURSIVE seq AS (
|
|
||||||
SELECT 1 AS n
|
|
||||||
UNION ALL
|
|
||||||
SELECT n + 1 FROM seq WHERE n < 100000
|
|
||||||
)
|
|
||||||
SELECT n, 'new' FROM seq;
|
|
||||||
|
|
||||||
EXPLAIN
|
|
||||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city, t.tag
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
|
||||||
LEFT JOIN user_tags_noindex t ON t.user_id = u.id
|
|
||||||
ORDER BY u.created_at DESC, u.id DESC
|
|
||||||
LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期结果:** 与实验二相同的崩坏形态(t 为 `ALL + hash join`,主表全量读取 + temporary + filesort)。
|
|
||||||
|
|
||||||
这说明日常更常见的场景——**join 列漏建索引**——和空表是同一个病:hash join 一出现,依赖主表索引序的 ORDER BY + LIMIT 计划就没了。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、机制小结(对照表)
|
|
||||||
|
|
||||||
| JOIN 实现方式 | 输出是否保序 | LIMIT 能否提前终止 | 对主表排序索引的影响 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 嵌套循环 + eq_ref 点查 | ✅ 顺序=驱动表访问序 | ✅ 读够即停 | 可借索引序免排序 |
|
|
||||||
| hash join | ❌ 哈希探测序 | ❌ 需物化后整体排序 | 排序计划作废,主表退化为全量读取 |
|
|
||||||
|
|
||||||
主表退化后的具体形态由 SELECT 列集合决定:
|
|
||||||
|
|
||||||
| SELECT 列 | 退化形态 | 特征 |
|
|
||||||
|---|---|---|
|
|
||||||
| 含索引外列(生产常态) | `type=ALL` 全表扫 | key=NULL |
|
|
||||||
| 全在索引内 | `type=index` 全索引扫 | key=该索引,`Using index`(覆盖免回表) |
|
|
||||||
|
|
||||||
两种形态共同点:rows ≈ 全量、`Using temporary; Using filesort`、LIMIT 无法提前终止。
|
|
||||||
|
|
||||||
优化器的行为链条:
|
|
||||||
|
|
||||||
```
|
|
||||||
被 join 表选了 hash join
|
|
||||||
→ 结果集顺序不可保证
|
|
||||||
→ ORDER BY 必须显式排序(temporary + filesort)
|
|
||||||
→ "主表走排序专用索引"失去意义
|
|
||||||
→ 主表改用最便宜的读取方式(ALL 或覆盖索引全扫)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、实践建议
|
|
||||||
|
|
||||||
1. **排查口诀**:`ORDER BY ... LIMIT` 查询变慢时,先看 EXPLAIN 里是否出现 `Using join buffer (hash join)`——它常常是主表索引"无故失效"的信号
|
|
||||||
2. **判断退化看 rows 和 temporary/filesort**,不要只盯 type:`type=index` 也可能是全索引扫描的退化形态(对比正常时 rows 应接近 LIMIT 值)
|
|
||||||
3. **根治**:给被 join 表的 join 列建索引(唯一列建唯一索引,非唯一建普通索引),让点查比 hash join 更便宜
|
|
||||||
4. **空表/新表场景**(刚上线、还没数据的表):如果线上已出现该退化且影响明显,可用 `FORCE INDEX` 过渡;表有数据后可移除 hint(低频后台查询甚至可以不处理)
|
|
||||||
5. **不要盲信"必须走索引"**:空表/极小表走全表扫本身是正确的成本决策,要修的是"hash join 破坏排序计划"这个副作用,而不是全表扫本身
|
|
||||||
6. **改动 join 结构后**(新增关联表、调整关联条件)重新 EXPLAIN 一次带排序的查询,确认 `Backward index scan` / rows 接近 LIMIT 值的计划仍在
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、清理
|
|
||||||
|
|
||||||
```sql
|
|
||||||
DROP TABLE IF EXISTS users, user_profiles, user_tags_empty, user_tags_noindex;
|
|
||||||
SET SESSION cte_max_recursion_depth = DEFAULT;
|
|
||||||
```
|
|
||||||
@@ -109,48 +109,15 @@ func main() {
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# mysql根据远程数据库表生成
|
# mysql根据远程数据库表生成
|
||||||
goctl model mysql datasource --url="user:password@tcp(127.0.0.1:3306)/database" --table="user_table" --dir="." -c --home D:/template/1.8.3
|
goctl model mysql datasource --url="olive:xxvkikaDDx01&&99@tcp(127.0.0.1:3306)/ebming" --table="user_invitation_record" --dir="." -c --home ../../../../deploy/goctl/1.8.3
|
||||||
|
|
||||||
# mysql根据ddl生成
|
# mysql根据ddl生成
|
||||||
goctl model mysql ddl --src h5.sql --dir . -c --home D:/template/1.8.3
|
goctl model mysql ddl --src h5.sql --dir . -c --home D:/zhangyuxin/template/1.8.3
|
||||||
|
|
||||||
# pgsql根据远程数据库表生成
|
# pgsql根据远程数据库表生成
|
||||||
goctl model pg datasource -url "postgres://user:password@127.0.0.1:15432/database" -table user -dir ./user --cache --home D:/template/1.8.3
|
goctl model pg datasource -url "postgres://labman:tN5bWz2KpL9Fy4@127.0.0.1:15432/sern" -table user -dir ./user --cache --home ../../goctl_template/1.8.3
|
||||||
|
|
||||||
# pgsql根据远程数据库表生成,带模式(schema)
|
# pgsql根据远程数据库表生成,带模式(schema)
|
||||||
goctl model pg datasource -url "postgres://user:password@127.0.0.1:25432/database?sslmode=disable" -s "users" -table "profile" -dir ./go-zero/model/users/ --cache --home ../../deploy/goctl
|
goctl model pg datasource -url "postgres://admin:zhangyuxin@127.0.0.1:25432/test?sslmode=disable" -s "users" -table "profile" -dir ./go-zero/model/users/ --cache --home ../../deploy/goctl
|
||||||
```
|
```
|
||||||
|
|
||||||
> 有的时候会遇到`unsupported database type`的错误,如果需要支持该字段,需要在`goctl.yaml`文件添加数据库类型映射配置,并保证goctl环境变量`GOCTL_EXPERIMENTAL=on`开启。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## config
|
|
||||||
|
|
||||||
`goctl config`是用于对goctl静态配置文件做管理。
|
|
||||||
|
|
||||||
```
|
|
||||||
$ goctl config --help
|
|
||||||
Usage:
|
|
||||||
goctl config [command]
|
|
||||||
|
|
||||||
Available Commands:
|
|
||||||
clean Clean goctl config file
|
|
||||||
init Initialize goctl config file
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
-h, --help help for config
|
|
||||||
|
|
||||||
|
|
||||||
Use "goctl config [command] --help" for more information about a command.
|
|
||||||
```
|
|
||||||
|
|
||||||
`goctl.yaml`文件字段
|
|
||||||
|
|
||||||
目前暂时仅支持对 model 数据类型映射的配置
|
|
||||||
|
|
||||||
| 字段名称 | 说明 |
|
|
||||||
| --------------- | ------------------------------------------------------------ |
|
|
||||||
| model | model 配置 |
|
|
||||||
| model.types_map | model 配置之类型映射规则,是一个 map<string,obj> 结构,key 为数据库类型,value 为映射对象 |
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
## 1.27版本变化
|
|
||||||
|
|
||||||
### 支持泛型方法
|
|
||||||
|
|
||||||
Go 1.27 虽然开放了泛型方法,但有明确的限制:
|
|
||||||
|
|
||||||
1.接口定义中不允许包含泛型方法。
|
|
||||||
|
|
||||||
2.拥有泛型方法的方法,不能用于实现接口。
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 普通结构体,本身没有任何泛型参数
|
|
||||||
type Serializer struct{}
|
|
||||||
|
|
||||||
// T 属于方法 Serialize,每个调用可以独立拥有不同的 T
|
|
||||||
func (s *Serializer) Serialize[T any](data T) string {
|
|
||||||
return fmt.Sprintf("%v", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用:
|
|
||||||
sz := Serializer{}
|
|
||||||
|
|
||||||
// 同一个 sz 实例,每次调用都可以处理完全不同的类型!
|
|
||||||
str1 := sz.Serialize("hello") // 自动推导 T = string
|
|
||||||
str2 := sz.Serialize(123) // 自动推导 T = int
|
|
||||||
str3 := sz.Serialize(true) // 自动推导 T = bool
|
|
||||||
```
|
|
||||||
|
|
||||||
### 结构体字面量:字段选择器当 key
|
|
||||||
|
|
||||||
嵌套或匿名结构体中的字段,现在可以直接初始化,不必再一层层嵌套字面量
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Person struct {
|
|
||||||
Sex string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
type Student struct {
|
|
||||||
StudentID int
|
|
||||||
Person
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go 1.27 之前:必须显式嵌套 Person
|
|
||||||
s1 := Student{
|
|
||||||
StudentID: 1,
|
|
||||||
Person: Person{
|
|
||||||
Sex: "male",
|
|
||||||
Name: "张三",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Go 1.27:嵌套结构体Person的字段可以直接当 key 用
|
|
||||||
s1 := Student{
|
|
||||||
StudentID: 1,
|
|
||||||
Sex: "male",
|
|
||||||
Name: "张三",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 函数类型推断:从“手动挡”到“自动挡”
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Go 1.27 之前:必须手动写出类型实参 [int],否则编译报错
|
|
||||||
fn := IntToString(FormatToString[int])
|
|
||||||
formatter:= []IntToString{FormatToString[int]}
|
|
||||||
|
|
||||||
// Go 1.27 后:自动推导,不用手写
|
|
||||||
fn := IntToString(FormatToString)
|
|
||||||
formatter := []IntToString{FormatToString}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 标准库更新:`encoding/json/v2`
|
|
||||||
|
|
||||||
> Go 1.27 标准库中的 v1 `encoding/json` 内部已经被改用 v2 引擎驱动,保留了 v1 的原始 API 签名和行为
|
|
||||||
|
|
||||||
新旧版本主要行为对比
|
|
||||||
|
|
||||||
| **场景** | **encoding/json (v1)** | **encoding/json/v2** |
|
|
||||||
| ------------------------------- | -------------------------- | -------------------------------------------- |
|
|
||||||
| **`nil` 切片/Map 序列化** | 转为 `null` | 转为 `[]` 和 `{}` *(符合语义,防前端空指针)* |
|
|
||||||
| **重复键处理 (Duplicate Keys)** | 允许,后面的覆盖前面的 | **直接报错** *(增强安全性,防 JSON 注入)* |
|
|
||||||
| **非法 UTF-8 字符** | 格式化替换,静默忽略 | **直接报错** *(要求严格符合规范)* |
|
|
||||||
| **字段名匹配 (大小写敏感)** | 默认**忽略**大小写匹配 | 默认**严格匹配**大小写 *(提升解析性能)* |
|
|
||||||
| **固定长度数组解包** | 长度不一致时自动截断或补零 | 必须与 JSON 数组长度严格相等,否则报错 |
|
|
||||||
|
|
||||||
支持可选项
|
|
||||||
|
|
||||||
| Options | 作用 |
|
|
||||||
| -------------------------------------- | ----------------------- |
|
|
||||||
| `json.FormatNilSliceAsNull(true)` | 允许使用 v1 的 nil 行为 |
|
|
||||||
| `json.MatchCaseInsensitiveNames(true)` | 允许忽略大小写 |
|
|
||||||
|
|
||||||
新增标签`omitzero`
|
|
||||||
|
|
||||||
| **维度** | **omitempty** | **omitzero** |
|
|
||||||
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
|
|
||||||
| **判断标准** | **是否为“空 (Empty)”** (如:空字符串 `""`、长度为 0 的切片/Map、`false`、`0`、指针 `nil`) | **是否为“ Go 类型的零值 (Zero Value)”** (满足 `reflect.Value.IsZero()` 或实现 `IsZero() bool` 接口) |
|
|
||||||
| **结构体/时间类型处理** | **有重大缺陷**,对非指针的结构体(如 `time.Time`)无效!依然会被序列化。 | **完美支持** 未初始化的 `time.Time{}` 或任何嵌套结构体零值均会被正确跳过。 |
|
|
||||||
| **自定义类型零值** | 不支持自定义零值判断逻辑。 | 支持只要实现 `IsZero() bool` 方法即可自定义是否跳过。 |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 官方扩展库 `bcrypt`
|
|
||||||
|
|
||||||
### `bcrypt` 是什么
|
|
||||||
|
|
||||||
`bcrypt` 是专门用于**密码哈希**的算法,不是可逆加密。
|
|
||||||
|
|
||||||
```text
|
|
||||||
password
|
|
||||||
↓
|
|
||||||
bcrypt
|
|
||||||
↓
|
|
||||||
hash
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 核心原则
|
|
||||||
|
|
||||||
```text
|
|
||||||
数据库只存 hash
|
|
||||||
bcrypt 自动处理 salt
|
|
||||||
使用 CompareHashAndPassword 验证
|
|
||||||
cost 根据服务器性能决定
|
|
||||||
不要自己用 MD5 / SHA256 存密码
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
#### cost
|
|
||||||
|
|
||||||
`cost` 是 bcrypt 的计算难度。
|
|
||||||
|
|
||||||
生产环境通常可以从 `10 ~ 12` 开始测试,具体值应根据服务器性能决定。单次 bcrypt 计算控制在大约 `100ms ~ 300ms` 是比较常见的工程范围。
|
|
||||||
|
|
||||||
`cost` 每增加 1,计算量大约接近翻倍。
|
|
||||||
|
|
||||||
cost 越高:
|
|
||||||
|
|
||||||
- 计算越慢
|
|
||||||
- 暴力破解成本越高
|
|
||||||
- 服务器 CPU 消耗也越高
|
|
||||||
|
|
||||||
bcrypt 生成的 hash 本身会记录当时使用的 `cost`。
|
|
||||||
|
|
||||||
因此,即使生产环境将当前配置从:
|
|
||||||
|
|
||||||
```text
|
|
||||||
cost = 10
|
|
||||||
```
|
|
||||||
|
|
||||||
升级为:
|
|
||||||
|
|
||||||
```text
|
|
||||||
cost = 12
|
|
||||||
```
|
|
||||||
|
|
||||||
数据库中原本使用 `cost = 10` 生成的旧 hash,仍然可以通过:
|
|
||||||
|
|
||||||
```go
|
|
||||||
bcrypt.CompareHashAndPassword(...)
|
|
||||||
```
|
|
||||||
|
|
||||||
正常验证。
|
|
||||||
|
|
||||||
`CompareHashAndPassword` 会根据已有 hash 中记录的信息进行密码验证,不依赖当前系统配置的 cost。
|
|
||||||
|
|
||||||
登录成功后,可以通过 `bcrypt.Cost()` 获取旧 hash 使用的 cost。如果低于当前配置,可以重新生成 hash 并更新数据库。
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
#### 72-byte 限制
|
|
||||||
|
|
||||||
bcrypt 密码输入最多支持约 `72 bytes`。
|
|
||||||
|
|
||||||
如果只允许 ASCII:
|
|
||||||
|
|
||||||
```text
|
|
||||||
72 bytes ≈ 72 个字符
|
|
||||||
```
|
|
||||||
|
|
||||||
通常已经足够。
|
|
||||||
|
|
||||||
建议业务层主动限制:
|
|
||||||
|
|
||||||
```go
|
|
||||||
if len([]byte(password)) > 72 {
|
|
||||||
return errors.New("password too long")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
### 基础使用
|
|
||||||
|
|
||||||
#### 依赖导入
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "golang.org/x/crypto/bcrypt"
|
|
||||||
```
|
|
||||||
|
|
||||||
`golang.org/x/crypto/bcrypt` 属于 Go 官方维护的扩展库,但不属于 Go 标准库。
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
#### 生成 hash
|
|
||||||
|
|
||||||
```go
|
|
||||||
hash, err := bcrypt.GenerateFromPassword(
|
|
||||||
[]byte(password),
|
|
||||||
12,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
生成的 hash 类似:
|
|
||||||
|
|
||||||
```text
|
|
||||||
$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
其中大致包含:
|
|
||||||
|
|
||||||
```text
|
|
||||||
$2a$ → bcrypt 版本
|
|
||||||
12$ → cost
|
|
||||||
后续内容 → 编码后的 salt + hash
|
|
||||||
```
|
|
||||||
|
|
||||||
bcrypt 会自动生成随机 salt,因此不需要自己生成或额外保存 salt。
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
#### 验证密码
|
|
||||||
|
|
||||||
```go
|
|
||||||
err := bcrypt.CompareHashAndPassword(
|
|
||||||
[]byte(passwordHash),
|
|
||||||
[]byte(password),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
// 密码正确
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
#### 判断密码是否相同
|
|
||||||
|
|
||||||
不能直接比较两个 bcrypt hash,因为 bcrypt 每次都会生成新的随机 salt。
|
|
||||||
|
|
||||||
即使密码完全相同:
|
|
||||||
|
|
||||||
```text
|
|
||||||
hash1 != hash2
|
|
||||||
```
|
|
||||||
|
|
||||||
判断某个密码是否和旧密码相同时,应使用:
|
|
||||||
|
|
||||||
```go
|
|
||||||
err := bcrypt.CompareHashAndPassword(
|
|
||||||
[]byte(oldHash),
|
|
||||||
[]byte(password),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
// password 与 oldHash 对应的密码相同
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
|
||||||
|
|
||||||
#### 获取 hash 的 cost
|
|
||||||
|
|
||||||
```go
|
|
||||||
cost, err := bcrypt.Cost(
|
|
||||||
[]byte(user.PasswordHash),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
通常用于**用户登录成功后**,判断当前密码 hash 是否使用了旧的 cost。
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
```text
|
|
||||||
数据库 hash cost = 10
|
|
||||||
当前系统 cost = 12
|
|
||||||
↓
|
|
||||||
登录成功
|
|
||||||
↓
|
|
||||||
重新生成 cost = 12 的 hash
|
|
||||||
↓
|
|
||||||
更新数据库
|
|
||||||
```
|
|
||||||
|
|
||||||
这样可以在用户正常登录过程中逐步升级旧密码 hash。
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# squirrel 的坑
|
|
||||||
|
|
||||||
> `github.com/Masterminds/squirrel`,纯 SQL 查询构建器(query builder),2026-09 查证。
|
|
||||||
|
|
||||||
## 坑 1:Like 家族多 key 不排序,SQL 输出不确定(真雷)
|
|
||||||
|
|
||||||
`Eq` / `NotEq` / `Lt` / `Gt` 等 map 条件生成 SQL 前会对 key 做 `sort.Strings` 字典序排序(v1.1.0 起,`expr.go` 的 `getSortedKeys`),输出确定。
|
|
||||||
|
|
||||||
但 `Like` / `NotLike` / `ILike` / `NotILike` 的 `toSql` 是直接 `for key, val := range lk`——跟随 Go map 随机遍历序,**多 key 时每次生成的 SQL 条件顺序随机**:
|
|
||||||
|
|
||||||
```
|
|
||||||
第1次: WHERE nickname LIKE ? AND mobile LIKE ? | args=[%张% %138%]
|
|
||||||
第3次: WHERE mobile LIKE ? AND nickname LIKE ? | args=[%138% %张%] ← 顺序翻转
|
|
||||||
```
|
|
||||||
|
|
||||||
### 后果
|
|
||||||
|
|
||||||
- 查询结果没错(AND 交换律),但 **SQL 字符串 + args 顺序在变**
|
|
||||||
- 如果用「SQL 字符串 + 参数」生成缓存 key(hash 后当 Redis key 之类),同一查询会打出多个 key:
|
|
||||||
- 命中率暴跌:n 个 Like 条件最多 n! 种排列 → n! 个缓存 key
|
|
||||||
- 同一份数据存多份副本,TTL 内占内存
|
|
||||||
- 依赖 SQL diff 的单测会随机失败
|
|
||||||
|
|
||||||
### 规避
|
|
||||||
|
|
||||||
- `Like` 只放**单个 key**
|
|
||||||
- 多模糊条件拆成多次 `.Where(...)`(内部是 append 切片,天然保序)或用 `sq.And{...}` 显式控制顺序
|
|
||||||
|
|
||||||
### 上游状态
|
|
||||||
|
|
||||||
- 无任何 issue 报告过此事(全量搜索过,含 closed),作者从未回应
|
|
||||||
- 当年修 Eq 排序的 PR #141 只改了 Eq/Lt 两处,紧邻的 Like 一行未动,讨论里也没人发现——纯遗漏,非有意设计(作者在 #135 明确说过支持排序)
|
|
||||||
- 库近乎停滞:最新 release v1.5.4(2023-03),master 最后 commit 2024-02,**别指望上游修**
|
|
||||||
|
|
||||||
## 坑 2:Eq 排序按字典序,与联合索引列序无关(知道即可)
|
|
||||||
|
|
||||||
`Eq{"b":2, "a":1}` 永远生成 `WHERE a = ? AND b = ?`,与你书写的顺序无关(issue #321)。
|
|
||||||
|
|
||||||
对主流数据库**基本无影响**:MySQL / PostgreSQL / SQL Server / Oracle 的 CBO 都会重排谓词,索引选择看的是「条件覆盖了哪些列(最左前缀)、选择度、类型是否隐式转换」,不是 WHERE 里的书写顺序。只有对谓词顺序敏感的引擎/老优化器才会踩到。
|
|
||||||
|
|
||||||
## 顺带:这个库的亮点
|
|
||||||
|
|
||||||
- builder 是**值语义不可变**的(基于 lann/builder 持久化数据结构),每次链式调用返回新副本,可安全分叉复用,不像 GORM 那样共享 Statement、分叉前要 `Session()` 克隆
|
|
||||||
- `Eq` 家族的确定性输出有源码保证(`expr.go` 排序逻辑),可放心用于缓存 key 场景
|
|
||||||
- 子查询:`FromSelect(builder, alias)`,或任何 builder 作为 `Sqlizer` 嵌进 `Where/Having/Join`
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Casbin 学习笔记
|
|
||||||
|
|
||||||
> 版本基线:casbin **v3.11.0**;Go import 路径 `github.com/casbin/casbin/v3`(源码仓库 `apache/casbin`)。本文是概念速记,不是教程。
|
|
||||||
> Casbin 负责**授权**(能不能做某事),不负责**身份认证**(你是谁)。
|
|
||||||
|
|
||||||
## 1. 心智模型:PERM
|
|
||||||
|
|
||||||
| 段 | 作用 |
|
|
||||||
|---|---|
|
|
||||||
| `[request_definition]` `r = sub, obj, act` | 一次访问请求长什么样 |
|
|
||||||
| `[policy_definition]` `p = sub, obj, act` | 一条策略规则长什么样 |
|
|
||||||
| `[policy_effect]` `e = some(where (p.eft == allow))` | 多条策略如何合成结论 |
|
|
||||||
| `[matchers]` `m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act` | `r` 与 `p` 如何配对 |
|
|
||||||
|
|
||||||
- `sub, obj, act` 只是最典型的字段,不是硬编码:可加 `dom`、资源属性,也可用对象做 ABAC。
|
|
||||||
- RBAC 再加 `[role_definition] g = _,_`,表示主体与角色的继承关系;`g(a, b)` 意为“a 属于 b”,角色链会递归解析。
|
|
||||||
- 模型只描述“怎么判”,策略才决定“允许谁”,两者可分开存储与更新。
|
|
||||||
|
|
||||||
## 2. 核心组件
|
|
||||||
|
|
||||||
Casbin 运行时由以下组件构成,多数可在不同程度上替换或选配:
|
|
||||||
|
|
||||||
| 组件 | 职责 |
|
|
||||||
|---|---|
|
|
||||||
| Model | PERM 四段组成的规则模型,描述匹配逻辑,不含具体策略 |
|
|
||||||
| Policy | 具体策略规则的集合,可在内存或外部存储 |
|
|
||||||
| Enforcer | 判定入口:把请求套进模型与策略,得到 true/false |
|
|
||||||
| Adapter | 策略的存储读写层(DB/文件等),由应用选配 |
|
|
||||||
| RoleManager | 维护角色继承关系,支撑 `g()` |
|
|
||||||
| Watcher | 感知其他实例的变更并触发重载;Dispatcher 负责派发变更事件 |
|
|
||||||
|
|
||||||
组件协作:Model + Policy 交给 Enforcer 判定;Adapter 负责把 Policy 读入/写出;RoleManager 供 `g()` 解析继承;Watcher/Dispatcher 让多实例感知变更。
|
|
||||||
|
|
||||||
## 3. 判定链与策略示意
|
|
||||||
|
|
||||||
请求 → matcher 匹配 → 命中的策略 effect 合成 → 结果。
|
|
||||||
|
|
||||||
```
|
|
||||||
g, alice, admin # alice 属于 admin
|
|
||||||
p, admin, report, read # admin 可读 report
|
|
||||||
Enforce(alice, report, read) => true # 经角色继承
|
|
||||||
Enforce(bob, report, read) => false # 无授权
|
|
||||||
```
|
|
||||||
|
|
||||||
- 判定按请求逐条扫描策略:matcher 命中后,由 effect 合成最终结论。
|
|
||||||
- `some(where (p.eft == allow))` 是白名单语义:只要有一条 allow 就放行。
|
|
||||||
|
|
||||||
## 4. 关键提醒
|
|
||||||
|
|
||||||
- **deny 与 error 都 fail-closed**:`false` 是判定为拒,`err != nil` 是判定失败;安全场景两者都当拒绝,别把错误当放行。
|
|
||||||
- **并发优先 `SyncedEnforcer`**:常用 API 有锁;普通 `Enforcer` 需自行加锁,也不要假设所有接口都并发安全。
|
|
||||||
- **内存策略不会自动跨实例同步**:Casbin 核心不内置同步,多副本要自己配置 Watcher 通知或周期重载/对账。
|
|
||||||
- **ABAC 能判断单条对象的属性/所有权,但不会自动生成 SQL 查询过滤**,列表与行级数据范围仍需业务层实现。
|
|
||||||
- **迁移 v2 → v3**:只确认 adapter 自身路径带 `/v3` 不够,要核对其 `go.mod` 是否依赖 `github.com/casbin/casbin/v3`;v2 与 v3 的 `model.Model` 是不同 Go 包类型。
|
|
||||||
- `Enforce` 的参数要与 `r` 定义一致,类型/语义由模型与 matcher 决定,不是固定三元组。
|
|
||||||
|
|
||||||
## 官方来源
|
|
||||||
|
|
||||||
- v3.0.0 release(module path 变更):<https://github.com/apache/casbin/releases/tag/v3.0.0>
|
|
||||||
- v3.11.0 `persist/adapter.go`:<https://github.com/apache/casbin/blob/v3.11.0/persist/adapter.go>
|
|
||||||
- ABAC 文档:<https://casbin.org/docs/abac>
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
## `errors`
|
## `errors.AsType`
|
||||||
|
|
||||||
### `errors.AsType`
|
|
||||||
|
|
||||||
> *用泛型把* `errors.As` *的“先声明变量再传指针”简化为“直接返回* `(E, bool)`*”,是* `errors.As` *的语法糖升级*
|
> *用泛型把* `errors.As` *的“先声明变量再传指针”简化为“直接返回* `(E, bool)`*”,是* `errors.As` *的语法糖升级*
|
||||||
|
|
||||||
@@ -22,6 +20,3 @@ if mysqlErr, ok := errors.AsType[*mysql.MySQLError](err); ok {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `net/http/pprof`
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
### excelize库
|
||||||
|
|
||||||
|
读写Excel的Go库
|
||||||
|
|
||||||
|
[github](https://github.com/qax-os/excelize)
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
#### 创建工作簿
|
||||||
|
|
||||||
|
```go
|
||||||
|
f := excelize.NewFile()
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 创建一个新的Sheet,返回的index为该工作簿的索引
|
||||||
|
index, err := f.NewSheet("SheetName")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建Sheet失败: %w", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 打开文件
|
||||||
|
|
||||||
|
```go
|
||||||
|
f, err := excelize.OpenFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开 Excel失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 写入cell值
|
||||||
|
|
||||||
|
```go
|
||||||
|
f.SetCellValue("SheetName", "B2", 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 设置工作簿默认活动工作表
|
||||||
|
|
||||||
|
```go
|
||||||
|
f.SetActiveSheet(index)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 创建或更新excel表
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := f.SaveAs("Book1.xlsx"); err != nil {
|
||||||
|
return fmt.Errorf("保存失败: %w", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取指定cell值
|
||||||
|
|
||||||
|
```go
|
||||||
|
cell, err := f.GetCellValue("Sheet1", "B2")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取cell失败: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(cell)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 获取所有工作簿的cell值(二维数组)
|
||||||
|
|
||||||
|
```go
|
||||||
|
rows, err := f.GetRows("Sheet1")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取失败: %w", err)
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
for _, colCell := range row {
|
||||||
|
fmt.Print(colCell, "\t")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
opencode添加mcp
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode mcp add postgres -- npx -y @microsoft/postgres-mcp run
|
|
||||||
```
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
常见图片格式的核心区别,主要在于 **是否压缩、是否支持透明、是否支持矢量、文件大小、适合什么场景**
|
|
||||||
|
|
||||||
| 格式 | 类型 | 透明背景 | 动画 | 放大是否模糊 | 文件大小 | 常见用途 |
|
|
||||||
| -------------- | ---- | ---------- | ------ | ------------ | -------- | ------------------------ |
|
|
||||||
| **JPG / JPEG** | 位图 | ❌ | ❌ | ✅ 会模糊 | 小 | 照片、摄影、电商图 |
|
|
||||||
| **PNG** | 位图 | ✅ | ❌ | ✅ 会模糊 | 中~大 | Logo、截图、透明图片、UI |
|
|
||||||
| **SVG** | 矢量 | ✅ | 可实现 | ❌ 不模糊 | 通常很小 | Logo、图标、网页插画 |
|
|
||||||
| **WebP** | 位图 | ✅ | ✅ | ✅ 会模糊 | 很小 | 网站图片、App 图片 |
|
|
||||||
| **GIF** | 位图 | ✅但有限 | ✅ | ✅ 会模糊 | 通常较大 | 简单动图、表情包 |
|
|
||||||
| **AVIF** | 位图 | ✅ | ✅ | ✅ 会模糊 | 很小 | 新一代网页图片 |
|
|
||||||
| **BMP** | 位图 | 通常不支持 | ❌ | ✅ 会模糊 | 非常大 | 较少使用 |
|
|
||||||
| **TIFF** | 位图 | 视情况 | ❌ | ✅ 会模糊 | 很大 | 印刷、扫描、专业摄影 |
|
|
||||||
Reference in New Issue
Block a user