diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e8443a8..6debd00 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -33,8 +33,5 @@ jobs: - name: Build run: go build -v ./... - - name: Test - run: go test -race -coverprofile=cover.out -v ./... - - - name: Post Coverage - uses: codecov/codecov-action@v2 \ No newline at end of file + - name: Unit Test + run: go test -race -v ./... \ No newline at end of file diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 99cf52b..882f3a0 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -31,4 +31,7 @@ jobs: go-version: '1.21.1' - name: Test - run: make e2e \ No newline at end of file + run: make e2e + + - name: Post Coverage + uses: codecov/codecov-action@v2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 82c4ca1..dcd9544 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ # Go workspace file go.work -.idea \ No newline at end of file +.idea +*.out +.run \ No newline at end of file diff --git a/.script/integrate_test.sh b/.script/integrate_test.sh index f455b56..edb4468 100644 --- a/.script/integrate_test.sh +++ b/.script/integrate_test.sh @@ -17,5 +17,5 @@ set -e docker compose -f .script/integration_test_compose.yml down docker compose -f .script/integration_test_compose.yml up -d -go test ./... -tags=e2e +go test -race -coverprofile=cover.out -tags=e2e ./... docker compose -f .script/integration_test_compose.yml down \ No newline at end of file diff --git a/internal/integration/activelimit_test.go b/internal/e2e/activelimit_test.go similarity index 96% rename from internal/integration/activelimit_test.go rename to internal/e2e/activelimit_test.go index 0f0f280..efd3aa0 100644 --- a/internal/integration/activelimit_test.go +++ b/internal/e2e/activelimit_test.go @@ -14,7 +14,7 @@ //go:build e2e -package integration +package e2e import ( "context" @@ -25,19 +25,13 @@ import ( "time" "github.com/ecodeclub/ginx/middlewares/activelimit/redislimit" - "github.com/redis/go-redis/v9" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBuilder_e2e_ActiveRedisLimit(t *testing.T) { - redisClient := redis.NewClient(&redis.Options{ - Addr: "localhost:16379", - Password: "", - DB: 0, - }) + redisClient := newRedisTestClient() ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() err := redisClient.Ping(ctx).Err() @@ -160,6 +154,7 @@ func TestBuilder_e2e_ActiveRedisLimit(t *testing.T) { time.Sleep(time.Millisecond * 100) redisClient.Del(context.Background(), tc.key) fmt.Println(redisClient.Get(context.Background(), tc.key).Int64()) + tc := tc t.Run(tc.name, func(t *testing.T) { server := gin.Default() diff --git a/internal/e2e/base_suite.go b/internal/e2e/base_suite.go new file mode 100644 index 0000000..8d64485 --- /dev/null +++ b/internal/e2e/base_suite.go @@ -0,0 +1,37 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package e2e + +import ( + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/suite" +) + +type BaseSuite struct { + suite.Suite + RDB redis.Cmdable +} + +func (s *BaseSuite) SetupSuite() { + s.RDB = newRedisTestClient() +} + +func (s *BaseSuite) TearDownSuite() { + if s.RDB != nil { + s.RDB.(*redis.Client).Close() + } +} diff --git a/internal/e2e/dependency.go b/internal/e2e/dependency.go new file mode 100644 index 0000000..7e82a49 --- /dev/null +++ b/internal/e2e/dependency.go @@ -0,0 +1,29 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package e2e + +import "github.com/redis/go-redis/v9" + +var redisCfg = &redis.Options{ + Addr: "localhost:16379", + Password: "", + DB: 0, +} + +func newRedisTestClient() *redis.Client { + return redis.NewClient(redisCfg) +} diff --git a/internal/e2e/gin_writer.go b/internal/e2e/gin_writer.go new file mode 100644 index 0000000..f755a1f --- /dev/null +++ b/internal/e2e/gin_writer.go @@ -0,0 +1,61 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "bufio" + "net" + "net/http" +) + +type GinResponseWriter struct { + http.ResponseWriter +} + +func (g *GinResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + panic("implement me") +} + +func (g *GinResponseWriter) Flush() { + panic("implement me") +} + +func (g *GinResponseWriter) CloseNotify() <-chan bool { + panic("implement me") +} + +func (g *GinResponseWriter) Status() int { + panic("implement me") +} + +func (g *GinResponseWriter) Size() int { + panic("implement me") +} + +func (g *GinResponseWriter) WriteString(s string) (int, error) { + panic("implement me") +} + +func (g *GinResponseWriter) Written() bool { + panic("implement me") +} + +func (g *GinResponseWriter) WriteHeaderNow() { + panic("implement me") +} + +func (g *GinResponseWriter) Pusher() http.Pusher { + panic("implement me") +} diff --git a/internal/integration/ratelimit_test.go b/internal/e2e/ratelimit_test.go similarity index 99% rename from internal/integration/ratelimit_test.go rename to internal/e2e/ratelimit_test.go index b36d1a6..6249e34 100644 --- a/internal/integration/ratelimit_test.go +++ b/internal/e2e/ratelimit_test.go @@ -14,7 +14,7 @@ //go:build e2e -package integration +package e2e import ( "context" diff --git a/session/global.go b/session/global.go index 7addc97..68f719b 100644 --- a/session/global.go +++ b/session/global.go @@ -45,3 +45,7 @@ func SetDefaultProvider(sp Provider) { func CheckLoginMiddleware() gin.HandlerFunc { return (&MiddlewareBuilder{sp: defaultProvider}).Build() } + +func RenewAccessToken(ctx *gctx.Context) error { + return defaultProvider.RenewAccessToken(ctx) +} diff --git a/session/memory.go b/session/memory.go index 0092c02..4a925c4 100644 --- a/session/memory.go +++ b/session/memory.go @@ -21,12 +21,23 @@ import ( "github.com/ecodeclub/ginx/internal/errs" ) +var _ Session = &MemorySession{} + // MemorySession 一般用于测试 type MemorySession struct { data map[string]any claims Claims } +func (m *MemorySession) Destroy(ctx context.Context) error { + return nil +} + +func (m *MemorySession) Del(ctx context.Context, key string) error { + delete(m.data, key) + return nil +} + func NewMemorySession(cl Claims) *MemorySession { return &MemorySession{ data: map[string]any{}, diff --git a/session/provider.mock_test.go b/session/provider.mock_test.go index f4282f0..634c44e 100644 --- a/session/provider.mock_test.go +++ b/session/provider.mock_test.go @@ -68,6 +68,34 @@ func (mr *MockSessionMockRecorder) Claims() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Claims", reflect.TypeOf((*MockSession)(nil).Claims)) } +// Del mocks base method. +func (m *MockSession) Del(ctx context.Context, key string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Del", ctx, key) + ret0, _ := ret[0].(error) + return ret0 +} + +// Del indicates an expected call of Del. +func (mr *MockSessionMockRecorder) Del(ctx, key any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Del", reflect.TypeOf((*MockSession)(nil).Del), ctx, key) +} + +// Destroy mocks base method. +func (m *MockSession) Destroy(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Destroy", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Destroy indicates an expected call of Destroy. +func (mr *MockSessionMockRecorder) Destroy(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Destroy", reflect.TypeOf((*MockSession)(nil).Destroy), ctx) +} + // Get mocks base method. func (m *MockSession) Get(ctx context.Context, key string) ekit.AnyValue { m.ctrl.T.Helper() @@ -148,3 +176,17 @@ func (mr *MockProviderMockRecorder) NewSession(ctx, uid, jwtData, sessData any) mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSession", reflect.TypeOf((*MockProvider)(nil).NewSession), ctx, uid, jwtData, sessData) } + +// RenewAccessToken mocks base method. +func (m *MockProvider) RenewAccessToken(ctx *gctx.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RenewAccessToken", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// RenewAccessToken indicates an expected call of RenewAccessToken. +func (mr *MockProviderMockRecorder) RenewAccessToken(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewAccessToken", reflect.TypeOf((*MockProvider)(nil).RenewAccessToken), ctx) +} diff --git a/session/redis/redis.go b/session/redis/provider.go similarity index 68% rename from session/redis/redis.go rename to session/redis/provider.go index f1811dd..31c1bbb 100644 --- a/session/redis/redis.go +++ b/session/redis/provider.go @@ -15,83 +15,24 @@ package redis import ( - "context" + "errors" "strings" "time" - "github.com/ecodeclub/ekit" + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/ginx/gctx" - "github.com/ecodeclub/ginx/internal/errs" ijwt "github.com/ecodeclub/ginx/internal/jwt" "github.com/ecodeclub/ginx/session" - "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/redis/go-redis/v9" ) -// Session 生命周期应该和 http 请求保持一致 -// 注意该实现本身预加载了 Session 的所有数据 -type Session struct { - client redis.Cmdable - // key 是 ssid 拼接而成。注意,它不是 access token,也不是 refresh token - key string - data map[string]string - claims session.Claims - expiration time.Duration -} - -func newRedisSession( - ssid string, - expiration time.Duration, - client redis.Cmdable, cl session.Claims) *Session { - return &Session{ - client: client, - key: "session:" + ssid, - expiration: expiration, - claims: cl, - } -} - -func (r *Session) Set(ctx context.Context, key string, val any) error { - return r.client.HMSet(ctx, r.key, key, val).Err() -} - -func (r *Session) init(ctx context.Context, kvs map[string]any) error { - pip := r.client.Pipeline() - for k, v := range kvs { - pip.HMSet(ctx, r.key, k, v) - } - pip.Expire(ctx, r.key, r.expiration) - _, err := pip.Exec(ctx) - return err -} - -func (r *Session) Get(ctx context.Context, key string) ekit.AnyValue { - val, ok := r.data[key] - if ok { - return ekit.AnyValue{Val: val} - } - return ekit.AnyValue{ - // 报错 - Err: errs.ErrSessionKeyNotFound, - } -} - -func (r *Session) preload(ctx context.Context) error { - var err error - r.data, err = r.client.HGetAll(ctx, r.key).Result() - if err != nil { - return err - } - if len(r.data) == 0 { - return errs.ErrUnauthorized - } - return nil -} +var ( + keyRefreshToken = "refresh_token" +) -func (r *Session) Claims() session.Claims { - return r.claims -} +var _ session.Provider = &SessionProvider{} // SessionProvider 默认是预加载机制,即 Get 的时候会顺便把所有的数据都拿过来 // 默认情况下,产生的 Session 对应了两个 token,access token 和 refresh token @@ -108,21 +49,36 @@ type SessionProvider struct { expiration time.Duration } -// NewSessionProvider 长短 token + session 机制。短 token 的过期时间是一小时 -// 长 token 的过期时间是 30 天 -func NewSessionProvider(client redis.Cmdable, key string) *SessionProvider { - // 长 token 过期时间,被看做是 Session 的过期时间 - expiration := time.Hour * 24 * 30 - m := ijwt.NewManagement[session.Claims](ijwt.NewOptions(time.Hour, key), - ijwt.WithRefreshJWTOptions[session.Claims](ijwt.NewOptions(expiration, key))) - return &SessionProvider{ - client: client, - atHeader: "X-Access-Token", - rtHeader: "X-Refresh-Token", - tokenHeader: "Authorization", - m: m, - expiration: expiration, +func (rsp *SessionProvider) RenewAccessToken(ctx *ginx.Context) error { + // 此时这里应该放着 RefreshToken + rt := rsp.extractTokenString(ctx) + jwtClaims, err := rsp.m.VerifyRefreshToken(rt) + if err != nil { + return err + } + claims := jwtClaims.Data + sess := newRedisSession(claims.SSID, rsp.expiration, rsp.client, claims) + defer func() { + // refresh_token 只能用一次,不管成功与否 + _ = sess.Del(ctx, keyRefreshToken) + }() + oldToken := sess.Get(ctx, keyRefreshToken).StringOrDefault("") + // 说明这个 rt 是已经用过的 refreshToken + // 或者 session 本身就已经过期了 + if oldToken != rt { + return errors.New("refresh_token 已经过期") + } + accessToken, err := rsp.m.GenerateAccessToken(claims) + if err != nil { + return err + } + refreshToken, err := rsp.m.GenerateRefreshToken(claims) + if err != nil { + return err } + ctx.Header(rsp.rtHeader, refreshToken) + ctx.Header(rsp.atHeader, accessToken) + return sess.Set(ctx, keyRefreshToken, refreshToken) } // NewSession 的时候,要先把这个 data 写入到对应的 token 里面 @@ -152,13 +108,13 @@ func (rsp *SessionProvider) NewSession(ctx *gctx.Context, sessData = make(map[string]any, 2) } sessData["uid"] = uid - sessData["refresh_token"] = refreshToken + sessData[keyRefreshToken] = refreshToken err = res.init(ctx, sessData) return res, err } // extractTokenString 提取 token 字符串. -func (rsp *SessionProvider) extractTokenString(ctx *gin.Context) string { +func (rsp *SessionProvider) extractTokenString(ctx *ginx.Context) string { authCode := ctx.GetHeader(rsp.tokenHeader) const bearerPrefix = "Bearer " if strings.HasPrefix(authCode, bearerPrefix) { @@ -175,10 +131,27 @@ func (rsp *SessionProvider) Get(ctx *gctx.Context) (session.Session, error) { return res, nil } - claims, err := rsp.m.VerifyAccessToken(rsp.extractTokenString(ctx.Context)) + claims, err := rsp.m.VerifyAccessToken(rsp.extractTokenString(ctx)) if err != nil { return nil, err } res = newRedisSession(claims.Data.SSID, rsp.expiration, rsp.client, claims.Data) - return res, res.preload(ctx) + return res, nil +} + +// NewSessionProvider 长短 token + session 机制。短 token 的过期时间是一小时 +// 长 token 的过期时间是 30 天 +func NewSessionProvider(client redis.Cmdable, jwtKey string) *SessionProvider { + // 长 token 过期时间,被看做是 Session 的过期时间 + expiration := time.Hour * 24 * 30 + m := ijwt.NewManagement[session.Claims](ijwt.NewOptions(time.Hour, jwtKey), + ijwt.WithRefreshJWTOptions[session.Claims](ijwt.NewOptions(expiration, jwtKey))) + return &SessionProvider{ + client: client, + atHeader: "X-Access-Token", + rtHeader: "X-Refresh-Token", + tokenHeader: "Authorization", + m: m, + expiration: expiration, + } } diff --git a/session/redis/redis_test.go b/session/redis/provider_test.go similarity index 63% rename from session/redis/redis_test.go rename to session/redis/provider_test.go index 23eac3c..35897c7 100644 --- a/session/redis/redis_test.go +++ b/session/redis/provider_test.go @@ -12,11 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build e2e + package redis import ( + "context" + "net/http" "net/http/httptest" "testing" + "time" + + "github.com/ecodeclub/ginx/internal/e2e" + "github.com/stretchr/testify/suite" "github.com/ecodeclub/ginx/gctx" "github.com/ecodeclub/ginx/internal/mocks" @@ -28,11 +36,43 @@ import ( "go.uber.org/mock/gomock" ) +type ProviderTestSuite struct { + e2e.BaseSuite +} + +func (s *ProviderTestSuite) TestRenewSession() { + sp := NewSessionProvider(s.RDB, "session") + req, err := http.NewRequest(http.MethodGet, "localhost:8080/hello", nil) + require.NoError(s.T(), err) + writer := httptest.NewRecorder() + gxCtx := &gctx.Context{ + Context: &gin.Context{ + Request: req, + Writer: &e2e.GinResponseWriter{ResponseWriter: writer}, + }, + } + sess, err := sp.NewSession(gxCtx, 123, map[string]string{"jwtKey1": "jwtVal1"}, map[string]any{"sessKe1": "sessVal1"}) + require.NoError(s.T(), err) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err = sess.Set(ctx, "sessKey2", "sessVal2") + require.NoError(s.T(), err) + // 先把 refresh token 取出来,放过去 req 的 header,从而模拟 renew 的请求 + rt := writer.Header().Get("X-Refresh-Token") + req.Header.Set("Authorization", "Bearer "+rt) + err = sp.RenewAccessToken(gxCtx) + require.NoError(s.T(), err) +} + +func TestProvider(t *testing.T) { + suite.Run(t, new(ProviderTestSuite)) +} + +// 历史测试,后面考虑删了 func TestSessionProvider_NewSession(t *testing.T) { testCases := []struct { - name string - mock func(ctrl *gomock.Controller) redis.Cmdable - + name string + mock func(ctrl *gomock.Controller) redis.Cmdable key string wantErr error }{ diff --git a/session/redis/session.go b/session/redis/session.go new file mode 100644 index 0000000..4d9ae66 --- /dev/null +++ b/session/redis/session.go @@ -0,0 +1,83 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package redis + +import ( + "context" + "time" + + "github.com/ecodeclub/ekit" + "github.com/ecodeclub/ginx/session" + "github.com/redis/go-redis/v9" +) + +var _ session.Session = &Session{} + +// Session 生命周期应该和 http 请求保持一致 +type Session struct { + client redis.Cmdable + // key 是 ssid 拼接而成。注意,它不是 access token,也不是 refresh token + key string + claims session.Claims + expiration time.Duration +} + +func (sess *Session) Destroy(ctx context.Context) error { + return sess.client.Del(ctx, sess.key).Err() +} + +func (sess *Session) Del(ctx context.Context, key string) error { + return sess.client.Del(ctx, sess.key, key).Err() +} + +func (sess *Session) Set(ctx context.Context, key string, val any) error { + return sess.client.HSet(ctx, sess.key, key, val).Err() +} + +func (sess *Session) init(ctx context.Context, kvs map[string]any) error { + pip := sess.client.Pipeline() + for k, v := range kvs { + pip.HMSet(ctx, sess.key, k, v) + } + pip.Expire(ctx, sess.key, sess.expiration) + _, err := pip.Exec(ctx) + return err +} + +func (sess *Session) Get(ctx context.Context, key string) ekit.AnyValue { + res, err := sess.client.HGet(ctx, sess.key, key).Result() + if err != nil { + return ekit.AnyValue{Err: err} + } + return ekit.AnyValue{ + Val: res, + } +} + +func (sess *Session) Claims() session.Claims { + return sess.claims +} + +func newRedisSession( + ssid string, + expiration time.Duration, + client redis.Cmdable, cl session.Claims) *Session { + return &Session{ + client: client, + key: "session:" + ssid, + expiration: expiration, + claims: cl, + } +} diff --git a/session/redis/session_test.go b/session/redis/session_test.go new file mode 100644 index 0000000..47ddd11 --- /dev/null +++ b/session/redis/session_test.go @@ -0,0 +1,57 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package redis + +import ( + "context" + "testing" + "time" + + "github.com/ecodeclub/ginx/internal/e2e" + "github.com/ecodeclub/ginx/session" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type SessionE2ETestSuite struct { + e2e.BaseSuite +} + +func (s *SessionE2ETestSuite) TestGetSetDel() { + ssid := "test_ssid" + sess := newRedisSession(ssid, time.Minute, s.RDB, session.Claims{ + Uid: 123, + SSID: ssid, + Data: map[string]string{ + "key1": "value1", + }, + }) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + defer sess.Destroy(ctx) + ssKey1, ssVal1 := "ss_key1", "ss_val1" + err := sess.Set(ctx, ssKey1, ssVal1) + require.NoError(s.T(), err) + val, err := sess.Get(ctx, ssKey1).AsString() + require.NoError(s.T(), err) + assert.Equal(s.T(), ssVal1, val) +} + +func TestSession(t *testing.T) { + suite.Run(t, new(SessionE2ETestSuite)) +} diff --git a/session/types.go b/session/types.go index 9cbca8d..4bffea5 100644 --- a/session/types.go +++ b/session/types.go @@ -28,6 +28,10 @@ type Session interface { Set(ctx context.Context, key string, val any) error // Get 从 Session 中获取数据,注意,这个方法不会从 JWT 里面获取数据 Get(ctx context.Context, key string) ekit.AnyValue + // Del 删除对应的数据 + Del(ctx context.Context, key string) error + // Destroy 销毁整个 Session + Destroy(ctx context.Context) error // Claims 编码进去了 JWT 里面的数据 Claims() Claims } @@ -41,8 +45,13 @@ type Provider interface { NewSession(ctx *gctx.Context, uid int64, jwtData map[string]string, sessData map[string]any) (Session, error) // Get 尝试拿到 Session,如果没有,返回 error - // Get 本身并不校验 Session 的有效性 + // Get 必须校验 Session 的合法性。 + // 也就是,用户可以预期拿到的 Session 永远是没有过期,直接可用的 Get(ctx *gctx.Context) (Session, error) + + // RenewAccessToken 刷新并且返回一个新的 access token + // 这个过程会校验长 token 的合法性 + RenewAccessToken(ctx *gctx.Context) error } type Claims struct {