diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 6f1b6baaed85cb033bf7f1159610bfcfd554ddfa..8bf2780aef5281cec08ef5da4e9c8f473f1b3aa4 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -29,5 +29,12 @@ jdbc:mysql://localhost:13316/webook $ProjectFileDir$ + + mysql.8 + true + com.mysql.cj.jdbc.Driver + jdbc:mysql://localhost:3308/mysql + $ProjectFileDir$ + \ No newline at end of file diff --git a/go.mod b/go.mod index ba5df9d3047dffab2441a2b8310549c148ba977f..0772ecf66d6fc51bdb5865c12dcc2803521ea018 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/gin-contrib/sessions v0.0.5 github.com/gin-gonic/gin v1.9.1 github.com/go-sql-driver/mysql v1.7.0 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/redis/go-redis/v9 v9.2.1 github.com/stretchr/testify v1.8.3 golang.org/x/crypto v0.9.0 gorm.io/driver/mysql v1.5.1 @@ -17,8 +19,10 @@ require ( require ( github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect diff --git a/go.sum b/go.sum index bb325f537fa83df453387ebaa9cb4a04fe4ad309..c5a2f2af96c18818fda5f332b5c78bcf661e6eeb 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= @@ -8,6 +12,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= @@ -37,6 +43,8 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -83,6 +91,8 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= +github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= diff --git a/qa/2023.09.26/main.go b/qa/2023.09.26/main.go new file mode 100644 index 0000000000000000000000000000000000000000..6ad9fcbb0d67197fd11bb5ed45e462a43206c255 --- /dev/null +++ b/qa/2023.09.26/main.go @@ -0,0 +1,44 @@ +package main + +import "fmt" + +func main() { + //DeferClosureLoopV1() + //DeferClosureLoopV2() + //DeferClosureLoopV3() + OfNullable[User](User{}).Apply(func(t User) { + println(t.Name) + }) +} + +func DeferClosureLoopV1() { + for i := 0; i < 10; i++ { + fmt.Printf("循环 %p \n", &i) + defer func() { + fmt.Printf("%p \n", &i) + println(i) + }() + } + println("跳出循环") +} + +func DeferClosureLoopV2() { + for i := 0; i < 10; i++ { + defer func(val int) { + fmt.Printf("%p \n", &val) + println(val) + }(i) + } + println("跳出循环") +} + +func DeferClosureLoopV3() { + for i := 0; i < 10; i++ { + j := i + defer func() { + fmt.Printf("%p \n", &j) + println(j) + }() + } + println("跳出循环") +} diff --git a/qa/2023.09.26/user.go b/qa/2023.09.26/user.go new file mode 100644 index 0000000000000000000000000000000000000000..f707a443c4df986c2c4b7a7ffa9600aa1491551e --- /dev/null +++ b/qa/2023.09.26/user.go @@ -0,0 +1,58 @@ +package main + +func UseUser() { + u, err := GetUser(12) + if err != nil { + println(err) + return + } + // u.Name + println(u.Name) +} + +type Optional[T any] struct { + Val T +} + +func (o Optional[T]) Apply(fn func(t T)) { + var val any = o.Val + if val == nil { + return + } + fn(o.Val) +} + +func OfNullable[T any](t T) Optional[T] { + return Optional[T]{ + Val: t, + } +} + +// GetUser 最佳实践,没有 error,*User 一定不为 nil +func GetUser(id int64) (*User, error) { + return &User{}, nil +} + +func Component() { + var u User // 这个时候。 + // Address就已经初始化了,零值(但不是 nil) + println(u.Friend.name) // panic,因为 Friend 是指针,所以初始化的是 nil + + var u1 User = User{ + Friend: &Friend{}, + } + println(u1.Friend.name) +} + +type User struct { + Name string + Address + *Friend +} + +type Address struct { +} + +type Friend struct { + name string +} diff --git a/syntax/funcs/defer.go b/syntax/funcs/defer.go index 7392e411b18fd23392419453e0d49747e94c7548..8e2d293028f0c1a4a84d460401e51d52e211c02c 100644 --- a/syntax/funcs/defer.go +++ b/syntax/funcs/defer.go @@ -1,5 +1,10 @@ package main +import ( + "os" + "sync" +) + func Defer() { defer func() { println("第一个 defer") @@ -45,6 +50,15 @@ func DeferReturn() int { return a } +func DeferReturnV0() (b int) { + a := 0 + defer func() { + a = 1 + }() + b = a + return +} + func DeferReturnV1() (a int) { a = 0 defer func() { @@ -63,6 +77,45 @@ func DeferReturnV2() *MyStruct { return res } +func DeferReturnV3() MyStruct { + res := MyStruct{ + name: "Tom", + } + defer func() { + res.name = "Jerry" + }() + return res +} + type MyStruct struct { name string } + +type SafeResourceV1 struct { + lock *sync.Mutex + resource any +} + +func (s SafeResourceV1) UseResource() { + s.lock.Lock() + defer s.lock.Unlock() +} + +type SafeResource struct { + lock sync.Mutex + resource any +} + +func (s *SafeResource) UseResource() { + s.lock.Lock() + defer s.lock.Unlock() +} + +func ReadFile(file string) { + f, err := os.Open(file) + if err != nil { + println(err) + return + } + defer f.Close() +} diff --git a/syntax/funcs/func.go b/syntax/funcs/func.go index d5b75632a6f184be32e8731df0492a1856da7a35..a7c014db47d9fa84303529a5370b05b19754f5e2 100644 --- a/syntax/funcs/func.go +++ b/syntax/funcs/func.go @@ -13,6 +13,7 @@ func main() { fmt.Println("DeferReturn", DeferReturn()) fmt.Println("DeferReturnV1", DeferReturnV1()) fmt.Println("DeferReturnV2", DeferReturnV2().name) + fmt.Println("DeferReturnV3", DeferReturnV3().name) } func Invoke() { diff --git a/webook-fe/src/axios/axios.ts b/webook-fe/src/axios/axios.ts index 7583d1b897dc1700459f678c9380c9105e39c83d..23dfd9dea151b5dd404c01d9908fa4ba13a31587 100644 --- a/webook-fe/src/axios/axios.ts +++ b/webook-fe/src/axios/axios.ts @@ -7,35 +7,35 @@ const instance = axios.create({ }) -// instance.interceptors.response.use(function (resp) { -// const newToken = resp.headers["x-jwt-token"] -// const newRefreshToken = resp.headers["x-refresh-token"] -// console.log("resp headers", resp.headers) -// if (newToken) { -// localStorage.setItem("token", newToken) -// } -// if (newRefreshToken) { -// localStorage.setItem("refresh_token", newRefreshToken) -// } -// if (resp.status == 401) { -// window.location.href="/users/login" -// } -// return resp -// }, (err) => { -// console.log(err) -// if (err.response.status == 401) { -// window.location.href="/users/login" -// } -// return err -// }) +instance.interceptors.response.use(function (resp) { + const newToken = resp.headers["x-jwt-token"] + const newRefreshToken = resp.headers["x-refresh-token"] + console.log("resp headers", resp.headers) + if (newToken) { + localStorage.setItem("token", newToken) + } + if (newRefreshToken) { + localStorage.setItem("refresh_token", newRefreshToken) + } + // if (resp.status == 401) { + // window.location.href="/users/login" + // } + return resp +}, (err) => { + console.log(err) + // if (err.response.status == 401) { + // window.location.href="/users/login" + // } + return err +}) // -// // 在这里让每一个请求都加上 authorization 的头部 -// instance.interceptors.request.use((req) => { -// const token = localStorage.getItem("token") -// req.headers.setAuthorization("Bearer " + token, true) -// return req -// }, (err) => { -// console.log(err) -// }) +// 在这里让每一个请求都加上 authorization 的头部 +instance.interceptors.request.use((req) => { + const token = localStorage.getItem("token") + req.headers.setAuthorization("Bearer " + token, true) + return req +}, (err) => { + console.log(err) +}) export default instance \ No newline at end of file diff --git a/webook/Dockerfile b/webook/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..082ceabe668182ee317b0fc11324a329a475df4c --- /dev/null +++ b/webook/Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:20.04 +COPY webook /app/webook +WORKDIR /app +CMD ["/app/webook"] \ No newline at end of file diff --git a/webook/Makefile b/webook/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e0c687b673d74ad8feda67197aa79d840616bbc8 --- /dev/null +++ b/webook/Makefile @@ -0,0 +1,7 @@ +.PHONY: docker +docker: + @rm webook || true + @go mod tidy + @GOOS=linux GOARCH=arm go build -tags=k8s -o webook . + @docker rmi -f flycash/webook:v0.0.1 + @docker build -t flycash/webook:v0.0.1 . \ No newline at end of file diff --git a/webook/config/dev.go b/webook/config/dev.go new file mode 100644 index 0000000000000000000000000000000000000000..f3085e9c4f48052c9483d2c5a364b70d568a96a6 --- /dev/null +++ b/webook/config/dev.go @@ -0,0 +1,12 @@ +//go:build !k8s + +package config + +var Config = config{ + DB: DBConfig{ + DSN: "root:root@tcp(localhost:13316)/webook", + }, + Redis: RedisConfig{ + Addr: "localhost:6379", + }, +} diff --git a/webook/config/k8s.go b/webook/config/k8s.go new file mode 100644 index 0000000000000000000000000000000000000000..b0bf5bc757e10529b9a6536cb5299e2db13e7792 --- /dev/null +++ b/webook/config/k8s.go @@ -0,0 +1,12 @@ +//go:build k8s + +package config + +var Config = config{ + DB: DBConfig{ + DSN: "root:root@tcp(webook-record-mysql:3308)/webook", + }, + Redis: RedisConfig{ + Addr: "webook-record-redis:6379", + }, +} diff --git a/webook/config/types.go b/webook/config/types.go new file mode 100644 index 0000000000000000000000000000000000000000..a2316f71147ceb6d4f862b50d30bb066a8596bcd --- /dev/null +++ b/webook/config/types.go @@ -0,0 +1,14 @@ +package config + +type config struct { + DB DBConfig + Redis RedisConfig +} + +type DBConfig struct { + DSN string +} + +type RedisConfig struct { + Addr string +} diff --git a/webook/docker-compose.yaml b/webook/docker-compose.yaml index c220889a6532a00133d3f458277886f180b38a89..b195c120e184d205dddf1da7ad825800c36abaed 100644 --- a/webook/docker-compose.yaml +++ b/webook/docker-compose.yaml @@ -2,7 +2,7 @@ version: "3" # 我这个 docker compose 由几个服务组成 services: mysql8: - image: mysql:8.0 + image: mysql:8.0.29 restart: always command: --default-authentication-plugin=mysql_native_password environment: @@ -14,3 +14,11 @@ services: # - 外部访问用 13316 - 13316:3306 + redis: + image: "bitnami/redis:latest" + restart: always + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - '6379:6379' + diff --git a/webook/internal/web/middleware/login.go b/webook/internal/web/middleware/login.go index fa05ae257f56ebb14fb62e9b227ea4cbb6b926c9..b3d3fa87b00db1bccdef77bca2fc4f85786c7f25 100644 --- a/webook/internal/web/middleware/login.go +++ b/webook/internal/web/middleware/login.go @@ -1,15 +1,20 @@ package middleware import ( + "encoding/gob" + "fmt" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "net/http" + "time" ) type LoginMiddlewareBuilder struct { } func (m *LoginMiddlewareBuilder) CheckLogin() gin.HandlerFunc { + // 注册一下这个类型 + gob.Register(time.Now()) return func(ctx *gin.Context) { path := ctx.Request.URL.Path if path == "/users/signup" || path == "/users/login" { @@ -17,10 +22,30 @@ func (m *LoginMiddlewareBuilder) CheckLogin() gin.HandlerFunc { return } sess := sessions.Default(ctx) - if sess.Get("userId") == nil { + userId := sess.Get("userId") + if userId == nil { // 中断,不要往后执行,也就是不要执行后面的业务逻辑 ctx.AbortWithStatus(http.StatusUnauthorized) return } + + now := time.Now() + + // 我怎么知道,要刷新了呢? + // 假如说,我们的策略是每分钟刷一次,我怎么知道,已经过了一分钟? + const updateTimeKey = "update_time" + // 试着拿出上一次刷新时间 + val := sess.Get(updateTimeKey) + lastUpdateTime, ok := val.(time.Time) + if val == nil || !ok || now.Sub(lastUpdateTime) > time.Second*10 { + // 你这是第一次进来 + sess.Set(updateTimeKey, now) + sess.Set("userId", userId) + err := sess.Save() + if err != nil { + // 打日志 + fmt.Println(err) + } + } } } diff --git a/webook/internal/web/middleware/login_jwt.go b/webook/internal/web/middleware/login_jwt.go new file mode 100644 index 0000000000000000000000000000000000000000..215069900781daf8d16514c461294704256faebd --- /dev/null +++ b/webook/internal/web/middleware/login_jwt.go @@ -0,0 +1,78 @@ +package middleware + +import ( + "gitee.com/geekbang/basic-go/webook/internal/web" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "log" + "net/http" + "strings" + "time" +) + +type LoginJWTMiddlewareBuilder struct { +} + +func (m *LoginJWTMiddlewareBuilder) CheckLogin() gin.HandlerFunc { + return func(ctx *gin.Context) { + path := ctx.Request.URL.Path + if path == "/users/signup" || path == "/users/login" { + // 不需要登录校验 + return + } + // 根据约定,token 在 Authorization 头部 + // Bearer XXXX + authCode := ctx.GetHeader("Authorization") + if authCode == "" { + // 没登录,没有 token, Authorization 这个头部都没有 + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + segs := strings.Split(authCode, " ") + if len(segs) != 2 { + // 没登录,Authorization 中的内容是乱传的 + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + tokenStr := segs[1] + var uc web.UserClaims + token, err := jwt.ParseWithClaims(tokenStr, &uc, func(token *jwt.Token) (interface{}, error) { + return web.JWTKey, nil + }) + if err != nil { + // token 不对,token 是伪造的 + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + if token == nil || !token.Valid { + // token 解析出来了,但是 token 可能是非法的,或者过期了的 + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + + if uc.UserAgent != ctx.GetHeader("User-Agent") { + // 后期我们讲到了监控告警的时候,这个地方要埋点 + // 能够进来这个分支的,大概率是攻击者 + ctx.AbortWithStatus(http.StatusUnauthorized) + return + } + + expireTime := uc.ExpiresAt + // 不判定都可以 + //if expireTime.Before(time.Now()) { + // ctx.AbortWithStatus(http.StatusUnauthorized) + // return + //} + // 剩余过期时间 < 50s 就要刷新 + if expireTime.Sub(time.Now()) < time.Second*50 { + uc.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 5)) + tokenStr, err = token.SignedString(web.JWTKey) + ctx.Header("x-jwt-token", tokenStr) + if err != nil { + // 这边不要中断,因为仅仅是过期时间没有刷新,但是用户是登录了的 + log.Println(err) + } + } + ctx.Set("user", uc) + } +} diff --git a/webook/internal/web/user.go b/webook/internal/web/user.go index 2278de09d24347865317d1de240247d9aa4af1d6..bd946bf7e7a5cc122027af16bf5efcb03cb349fd 100644 --- a/webook/internal/web/user.go +++ b/webook/internal/web/user.go @@ -6,7 +6,9 @@ import ( regexp "github.com/dlclark/regexp2" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + jwt "github.com/golang-jwt/jwt/v5" "net/http" + "time" ) const ( @@ -38,7 +40,8 @@ func (h *UserHandler) RegisterRoutes(server *gin.Engine) { // POST /users/signup ug.POST("/signup", h.SignUp) // POST /users/login - ug.POST("/login", h.Login) + //ug.POST("/login", h.Login) + ug.POST("/login", h.LoginJWT) // POST /users/edit ug.POST("/edit", h.Edit) // GET /users/profile @@ -96,6 +99,40 @@ func (h *UserHandler) SignUp(ctx *gin.Context) { } } +func (h *UserHandler) LoginJWT(ctx *gin.Context) { + type Req struct { + Email string `json:"email"` + Password string `json:"password"` + } + var req Req + if err := ctx.Bind(&req); err != nil { + return + } + u, err := h.svc.Login(ctx, req.Email, req.Password) + switch err { + case nil: + uc := UserClaims{ + Uid: u.Id, + UserAgent: ctx.GetHeader("User-Agent"), + RegisteredClaims: jwt.RegisteredClaims{ + // 1 分钟过期 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 5)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS512, uc) + tokenStr, err := token.SignedString(JWTKey) + if err != nil { + ctx.String(http.StatusOK, "系统错误") + } + ctx.Header("x-jwt-token", tokenStr) + ctx.String(http.StatusOK, "登录成功") + case service.ErrInvalidUserOrPassword: + ctx.String(http.StatusOK, "用户名或者密码不对") + default: + ctx.String(http.StatusOK, "系统错误") + } +} + func (h *UserHandler) Login(ctx *gin.Context) { type Req struct { Email string `json:"email"` @@ -111,8 +148,8 @@ func (h *UserHandler) Login(ctx *gin.Context) { sess := sessions.Default(ctx) sess.Set("userId", u.Id) sess.Options(sessions.Options{ - // 十五分钟 - MaxAge: 900, + // 十分钟 + MaxAge: 30, }) err = sess.Save() if err != nil { @@ -128,9 +165,19 @@ func (h *UserHandler) Login(ctx *gin.Context) { } func (h *UserHandler) Edit(ctx *gin.Context) { - + // 嵌入一段刷新过期时间的代码 } func (h *UserHandler) Profile(ctx *gin.Context) { + //us := ctx.MustGet("user").(UserClaims) ctx.String(http.StatusOK, "这是 profile") + // 嵌入一段刷新过期时间的代码 +} + +var JWTKey = []byte("k6CswdUm77WKcbM68UQUuxVsHSpTCwgK") + +type UserClaims struct { + jwt.RegisteredClaims + Uid int64 + UserAgent string } diff --git a/webook/main.go b/webook/main.go index 8fa1d2e189b5f2fce8121e44eda70cb5b33e813a..d3b730284995f9786fd847153540a3c0337e3736 100644 --- a/webook/main.go +++ b/webook/main.go @@ -1,17 +1,21 @@ package main import ( + "gitee.com/geekbang/basic-go/webook/config" "gitee.com/geekbang/basic-go/webook/internal/repository" "gitee.com/geekbang/basic-go/webook/internal/repository/dao" "gitee.com/geekbang/basic-go/webook/internal/service" "gitee.com/geekbang/basic-go/webook/internal/web" "gitee.com/geekbang/basic-go/webook/internal/web/middleware" + "gitee.com/geekbang/basic-go/webook/pkg/ginx/middleware/ratelimit" "github.com/gin-contrib/cors" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" "gorm.io/driver/mysql" "gorm.io/gorm" + "net/http" "strings" "time" ) @@ -21,6 +25,10 @@ func main() { server := initWebServer() initUserHdl(db, server) + //server := gin.Default() + server.GET("/hello", func(ctx *gin.Context) { + ctx.String(http.StatusOK, "hello,启动成功了!") + }) server.Run(":8080") } @@ -33,7 +41,7 @@ func initUserHdl(db *gorm.DB, server *gin.Engine) { } func initDB() *gorm.DB { - db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook")) + db, err := gorm.Open(mysql.Open(config.Config.DB.DSN)) if err != nil { panic(err) } @@ -53,7 +61,9 @@ func initWebServer() *gin.Engine { //AllowOrigins: []string{"http://localhost:3000"}, AllowCredentials: true, - AllowHeaders: []string{"Content-Type"}, + AllowHeaders: []string{"Content-Type", "Authorization"}, + // 这个是允许前端访问你的后端响应中带的头部 + ExposeHeaders: []string{"x-jwt-token"}, //AllowHeaders: []string{"content-type"}, //AllowMethods: []string{"POST"}, AllowOriginFunc: func(origin string) bool { @@ -68,10 +78,37 @@ func initWebServer() *gin.Engine { println("这是我的 Middleware") }) + redisClient := redis.NewClient(&redis.Options{ + Addr: config.Config.Redis.Addr, + }) + + server.Use(ratelimit.NewBuilder(redisClient, + time.Second, 1).Build()) + + useJWT(server) + //useSession(server) + return server +} + +func useJWT(server *gin.Engine) { + login := middleware.LoginJWTMiddlewareBuilder{} + server.Use(login.CheckLogin()) +} + +func useSession(server *gin.Engine) { login := &middleware.LoginMiddlewareBuilder{} // 存储数据的,也就是你 userId 存哪里 // 直接存 cookie store := cookie.NewStore([]byte("secret")) + // 基于内存的实现 + //store := memstore.NewStore([]byte("k6CswdUm75WKcbM68UQUuxVsHSpTCwgK"), + // []byte("eF1`yQ9>yT1`tH1,sJ0.zD8;mZ9~nC6(")) + //store, err := redis.NewStore(16, "tcp", + // "localhost:6379", "", + // []byte("k6CswdUm75WKcbM68UQUuxVsHSpTCwgK"), + // []byte("k6CswdUm75WKcbM68UQUuxVsHSpTCwgA")) + //if err != nil { + // panic(err) + //} server.Use(sessions.Sessions("ssid", store), login.CheckLogin()) - return server } diff --git a/webook/pkg/ginx/middleware/ratelimit/builder.go b/webook/pkg/ginx/middleware/ratelimit/builder.go new file mode 100644 index 0000000000000000000000000000000000000000..2a576ecd2e1d9387b55a5d2bb121d5c1c5fc3fc4 --- /dev/null +++ b/webook/pkg/ginx/middleware/ratelimit/builder.go @@ -0,0 +1,64 @@ +package ratelimit + +import ( + _ "embed" + "fmt" + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" + "log" + "net/http" + "time" +) + +type Builder struct { + prefix string + cmd redis.Cmdable + interval time.Duration + // 阈值 + rate int +} + +//go:embed slide_window.lua +var luaScript string + +func NewBuilder(cmd redis.Cmdable, interval time.Duration, rate int) *Builder { + return &Builder{ + cmd: cmd, + prefix: "ip-limiter", + interval: interval, + rate: rate, + } +} + +func (b *Builder) Prefix(prefix string) *Builder { + b.prefix = prefix + return b +} + +func (b *Builder) Build() gin.HandlerFunc { + return func(ctx *gin.Context) { + limited, err := b.limit(ctx) + if err != nil { + log.Println(err) + // 这一步很有意思,就是如果这边出错了 + // 要怎么办? + // 保守做法:因为借助于 Redis 来做限流,那么 Redis 崩溃了,为了防止系统崩溃,直接限流 + ctx.AbortWithStatus(http.StatusInternalServerError) + // 激进做法:虽然 Redis 崩溃了,但是这个时候还是要尽量服务正常的用户,所以不限流 + // ctx.Next() + return + } + if limited { + log.Println(err) + ctx.AbortWithStatus(http.StatusTooManyRequests) + return + } + ctx.Next() + } +} + +func (b *Builder) limit(ctx *gin.Context) (bool, error) { + key := fmt.Sprintf("%s:%s", b.prefix, ctx.ClientIP()) + return b.cmd.Eval(ctx, luaScript, []string{key}, + b.interval.Milliseconds(), b.rate, time.Now().UnixMilli()).Bool() +} diff --git a/webook/pkg/ginx/middleware/ratelimit/slide_window.lua b/webook/pkg/ginx/middleware/ratelimit/slide_window.lua new file mode 100644 index 0000000000000000000000000000000000000000..ee058b05dfea0cbb17d1807e962d02246d16a406 --- /dev/null +++ b/webook/pkg/ginx/middleware/ratelimit/slide_window.lua @@ -0,0 +1,26 @@ +-- 1, 2, 3, 4, 5, 6, 7 这是你的元素 +-- ZREMRANGEBYSCORE key1 0 6 +-- 7 执行完之后 + +-- 限流对象 +local key = KEYS[1] +-- 窗口大小 +local window = tonumber(ARGV[1]) +-- 阈值 +local threshold = tonumber( ARGV[2]) +local now = tonumber(ARGV[3]) +-- 窗口的起始时间 +local min = now - window + +redis.call('ZREMRANGEBYSCORE', key, '-inf', min) +local cnt = redis.call('ZCOUNT', key, '-inf', '+inf') +-- local cnt = redis.call('ZCOUNT', key, min, '+inf') +if cnt >= threshold then + -- 执行限流 + return "true" +else + -- 把 score 和 member 都设置成 now + redis.call('ZADD', key, now, now) + redis.call('PEXPIRE', key, window) + return "false" +end \ No newline at end of file diff --git a/webook/webook-deployment.yaml b/webook/webook-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..780f6d8beaedefe7f9b5904d1b7d6e3ce38cede5 --- /dev/null +++ b/webook/webook-deployment.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webook-record-service +spec: +# 三个副本 + replicas: 3 + selector: + matchLabels: + app: webook-record + template: + metadata: + labels: +# 这个 webook-record 一定要和前面的 selector 的 matchLabels 匹配上 + app: webook-record +# 这个是 Deployment 管理的 Pod 的模板 + spec: +# Pod 里面运行的所有的 container + containers: + - name: webook-record + image: flycash/webook:v0.0.1 + ports: + - containerPort: 8080 diff --git a/webook/webook-record-ingress.yaml b/webook/webook-record-ingress.yaml new file mode 100644 index 0000000000000000000000000000000000000000..46063f86dc60cba62eb44222dd666818cb22caba --- /dev/null +++ b/webook/webook-record-ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: webook-record-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + - host: localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: webook-record + port: + number: 98 + \ No newline at end of file diff --git a/webook/webook-record-mysql-deployment.yaml b/webook/webook-record-mysql-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..77e2123d7141234373f8a6f89de1a7a0cb4d8770 --- /dev/null +++ b/webook/webook-record-mysql-deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webook-record-mysql + labels: + app: webook-record-mysql +spec: + replicas: 1 + selector: + matchLabels: + app: webook-record-mysql + template: + metadata: + name: webook-record-mysql + labels: + app: webook-record-mysql + spec: + containers: + - name: webook-record-mysql + image: mysql:8.0 + env: + - name: MYSQL_ROOT_PASSWORD + value: root + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3306 + volumeMounts: + - mountPath: /var/lib/mysql + name: mysql-storage + restartPolicy: Always + volumes: + - name: mysql-storage + persistentVolumeClaim: +# PVC persistent volume claim + claimName: webook-mysql-pvc diff --git a/webook/webook-record-mysql-pv.yaml b/webook/webook-record-mysql-pv.yaml new file mode 100644 index 0000000000000000000000000000000000000000..548547bfb4a0c7e36f30aae6cef4027296e9b091 --- /dev/null +++ b/webook/webook-record-mysql-pv.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: webook-mysql-pvc +spec: + storageClassName: record + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + hostPath: + path: "/mnt/data" \ No newline at end of file diff --git a/webook/webook-record-mysql-pvc.yaml b/webook/webook-record-mysql-pvc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..868a729d67adcc39813f321180995468e4cca244 --- /dev/null +++ b/webook/webook-record-mysql-pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: webook-mysql-pvc +spec: + storageClassName: record + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/webook/webook-record-mysql-service.yaml b/webook/webook-record-mysql-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..767672308be72950d89a4160d888ba1ec8cc44da --- /dev/null +++ b/webook/webook-record-mysql-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: webook-record-mysql +spec: + selector: + app: webook-record-mysql + ports: + - protocol: TCP + port: 3308 + targetPort: 3306 + type: LoadBalancer + \ No newline at end of file diff --git a/webook/webook-record-redis-deployment.yaml b/webook/webook-record-redis-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2b5bdbb529d56e3651a749996fffa79db5b9d096 --- /dev/null +++ b/webook/webook-record-redis-deployment.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webook-record-redis + labels: + app: webook-record-redis +spec: + replicas: 1 + selector: + matchLabels: + app: webook-record-redis + template: + metadata: + name: webook-record-redis + labels: + app: webook-record-redis + spec: + containers: + - name: webook-record-redis + image: redis:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 6379 + restartPolicy: Always + \ No newline at end of file diff --git a/webook/webook-record-redis-service.yaml b/webook/webook-record-redis-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..827704004dde1350e8f1815348566bf6ad125197 --- /dev/null +++ b/webook/webook-record-redis-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: webook-record-redis +spec: + selector: + app: webook-record-redis + ports: + - protocol: TCP +# k8s 内部访问接口 + port: 6379 +# 外部访问端口,必须在 30000-32767 + nodePort: 31379 +# pod 暴露的端口 + targetPort: 6379 + type: NodePort + \ No newline at end of file diff --git a/webook/webook-service.yaml b/webook/webook-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3d564fedc0b239b495c8c64461e4bf38cfa5176c --- /dev/null +++ b/webook/webook-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: webook-record +spec: + selector: + app: webook-record + ports: + - protocol: TCP + port: 98 + targetPort: 8080 + type: ClusterIP + \ No newline at end of file