fix: 登录有效期由 8 小时改为 30 天 (#42) #43
@@ -163,6 +163,55 @@ func TestMySQLLoginUsesStoredPasswordWithoutChangingCreationPolicy(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMySQLSessionLifetimeIsThirtyDays pins the login lifetime that both clients share. It checks
|
||||
// the API response, the stored row and the privacy rule (only a digest is persisted) so changing the
|
||||
// value without updating this test is impossible.
|
||||
func TestMySQLSessionLifetimeIsThirtyDays(t *testing.T) {
|
||||
if SessionLifetime != 30*24*time.Hour {
|
||||
t.Fatalf("session lifetime %s, want 30 days", SessionLifetime)
|
||||
}
|
||||
db := testDB(t)
|
||||
u := admin.SysUser{Username: randomName("sesslife"), Password: fixturePassword, RoleId: 2, Status: "2"}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := Router(db, time.Now)
|
||||
before := time.Now()
|
||||
code, data := callAPI(t, r, "POST", "/api/v1/login", "", map[string]string{"username": u.Username, "password": fixturePassword})
|
||||
if code != 200 {
|
||||
t.Fatalf("login status %d", code)
|
||||
}
|
||||
var login struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &login); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
granted := login.ExpiresAt.Sub(before)
|
||||
if granted < SessionLifetime-time.Minute || granted > SessionLifetime+time.Minute {
|
||||
t.Fatalf("granted %s, want about %s", granted, SessionLifetime)
|
||||
}
|
||||
var stored Session
|
||||
if err := db.Where("token_hash = ?", digest(login.Token)).First(&stored).Error; err != nil {
|
||||
t.Fatal("session row missing")
|
||||
}
|
||||
if stored.ExpiresAt.Sub(stored.ExpiresAt.Truncate(time.Millisecond)) != 0 {
|
||||
t.Fatal("stored expiry lost precision")
|
||||
}
|
||||
if delta := login.ExpiresAt.Sub(stored.ExpiresAt); delta > time.Millisecond || delta < -time.Millisecond {
|
||||
t.Fatalf("stored expiry %s differs from the response by %s", stored.ExpiresAt, delta)
|
||||
}
|
||||
var raw int64
|
||||
db.Table("lexgo_sessions").Where("token_hash = ?", login.Token).Count(&raw)
|
||||
if raw != 0 {
|
||||
t.Fatal("raw token persisted")
|
||||
}
|
||||
if code, _ = callAPI(t, r, "GET", "/api/v1/me", login.Token, nil); code != 200 {
|
||||
t.Fatalf("fresh session rejected: %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLAccountIsolationAndRevocation(t *testing.T) {
|
||||
db := testDB(t)
|
||||
if err := Migrate(db); err != nil {
|
||||
@@ -281,7 +330,13 @@ func TestMySQLAccountIsolationAndRevocation(t *testing.T) {
|
||||
t.Fatal("old password accepted")
|
||||
}
|
||||
loginToken(t, r, users[0], "replacement-fixture-pass")
|
||||
clock = clock.Add(9 * time.Hour)
|
||||
// The lifetime is absolute, so a session is still valid just before it and gone right after;
|
||||
// asserting against SessionLifetime keeps this test honest when the value changes.
|
||||
clock = clock.Add(SessionLifetime - time.Hour)
|
||||
if code, _ = callAPI(t, r, "GET", "/api/v1/me", tokenB, nil); code != 200 {
|
||||
t.Fatal("session must still be valid before its lifetime ends", code)
|
||||
}
|
||||
clock = clock.Add(2 * time.Hour)
|
||||
code, _ = callAPI(t, r, "GET", "/api/v1/me", tokenB, nil)
|
||||
if code != 401 {
|
||||
t.Fatal("expired session valid")
|
||||
|
||||
@@ -38,6 +38,11 @@ type Space struct {
|
||||
|
||||
func (Space) TableName() string { return "lexgo_spaces" }
|
||||
|
||||
// SessionLifetime is how long a login lasts. It is absolute, not sliding: using the app does not
|
||||
// extend it, so a stolen token cannot be kept alive forever. Both the learner and the admin client
|
||||
// authenticate through the same login endpoint, so one value covers them.
|
||||
const SessionLifetime = 30 * 24 * time.Hour
|
||||
|
||||
type Session struct {
|
||||
TokenHash string `gorm:"primaryKey"`
|
||||
OwnerID int
|
||||
@@ -162,7 +167,7 @@ func login(db *gorm.DB, now time.Time, name, password string) (LoginResult, erro
|
||||
return err
|
||||
}
|
||||
token := hex.EncodeToString(b)
|
||||
expiry := now.Add(8 * time.Hour)
|
||||
expiry := now.Add(SessionLifetime)
|
||||
if err = tx.Where("owner_id = ? AND expires_at <= ?", u.UserId, now).Delete(&Session{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user