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