207 lines
8.3 KiB
Go
207 lines
8.3 KiB
Go
package auth
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
authdomain "hyapp/services/user-service/internal/domain/auth"
|
||
)
|
||
|
||
func (r *Repository) CreateSession(ctx context.Context, session authdomain.Session) error {
|
||
// refresh token 只保存 hash,原文不会写入数据库。
|
||
_, err := r.db.ExecContext(ctx, `
|
||
INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
|
||
VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
|
||
`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.CreatedAtMs, session.UpdatedAtMs)
|
||
if err != nil {
|
||
// session_id 或 refresh hash 冲突统一映射成 auth 领域冲突。
|
||
return mapAuthDuplicateError(err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// FindActiveSessionByRefreshHash 查找未过期且未吊销的 session。
|
||
|
||
func (r *Repository) FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error) {
|
||
// 查到 session 后再判断 revoked/expired,给调用方稳定 reason。
|
||
session, err := r.findSessionByRefreshHash(ctx, refreshTokenHash)
|
||
if err != nil {
|
||
return authdomain.Session{}, err
|
||
}
|
||
if session.RevokedAtMs > 0 {
|
||
// 已吊销 session 不能继续 refresh。
|
||
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
|
||
}
|
||
if session.ExpiresAtMs <= nowMs {
|
||
// 过期 session 与吊销 session 使用不同 reason。
|
||
return authdomain.Session{}, xerr.New(xerr.SessionExpired, "session expired")
|
||
}
|
||
|
||
return session, nil
|
||
}
|
||
|
||
// FindActiveSessionByID 查找未过期且未吊销的 session。
|
||
|
||
func (r *Repository) FindActiveSessionByID(ctx context.Context, sessionID string, nowMs int64) (authdomain.Session, error) {
|
||
// onboarding 完成后只需要重签 access token,不能要求客户端再提交 refresh token 原文。
|
||
session, err := r.findSessionByID(ctx, sessionID)
|
||
if err != nil {
|
||
return authdomain.Session{}, err
|
||
}
|
||
if session.RevokedAtMs > 0 {
|
||
// 已吊销 session 不能继续签发任何 access token。
|
||
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
|
||
}
|
||
if session.ExpiresAtMs <= nowMs {
|
||
// session 过期后只能重新登录,不能用完成注册接口续命。
|
||
return authdomain.Session{}, xerr.New(xerr.SessionExpired, "session expired")
|
||
}
|
||
|
||
return session, nil
|
||
}
|
||
|
||
// ReplaceSession 原子吊销旧 session 并创建新 session。
|
||
|
||
func (r *Repository) ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error {
|
||
// refresh 轮换必须原子吊销旧 session 并插入新 session。
|
||
tx, err := r.db.BeginTx(ctx, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer tx.Rollback()
|
||
|
||
var oldRevokedAt sql.Null[int64]
|
||
err = tx.QueryRowContext(ctx, `
|
||
SELECT revoked_at_ms
|
||
FROM auth_sessions
|
||
WHERE app_code = ? AND session_id = ?
|
||
FOR UPDATE
|
||
`, appcode.FromContext(ctx), oldSessionID).Scan(&oldRevokedAt)
|
||
if err == sql.ErrNoRows || oldRevokedAt.Valid {
|
||
// 旧 session 不存在或已经吊销时,不能再次轮换。
|
||
return xerr.New(xerr.SessionRevoked, "session revoked")
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
_, err = tx.ExecContext(ctx, `
|
||
UPDATE auth_sessions
|
||
SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND session_id = ?
|
||
`, revokedAtMs, "REFRESH_ROTATED", "", "refresh_token", revokedAtMs, appcode.FromContext(ctx), oldSessionID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := insertSession(ctx, tx, newSession); err != nil {
|
||
// 插入新 session 失败会回滚旧 session 吊销。
|
||
return err
|
||
}
|
||
|
||
return tx.Commit()
|
||
}
|
||
|
||
// RevokeSession 按 session_id 或 refresh token hash 吊销 session。
|
||
|
||
func (r *Repository) RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error) {
|
||
if sessionID == "" && refreshTokenHash != "" {
|
||
// 只传 refresh token 时先反查 session_id。
|
||
session, err := r.findSessionByRefreshHash(ctx, refreshTokenHash)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
sessionID = session.SessionID
|
||
}
|
||
|
||
return r.revokeSessionByID(ctx, sessionID, revokedAtMs, "USER_LOGOUT", "", "user_logout")
|
||
}
|
||
|
||
func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) {
|
||
return r.revokeSessionByID(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy)
|
||
}
|
||
|
||
func (r *Repository) revokeSessionByID(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) {
|
||
result, err := r.db.ExecContext(ctx, `
|
||
UPDATE auth_sessions
|
||
SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND session_id = ? AND revoked_at_ms IS NULL
|
||
`, revokedAtMs, reason, requestID, revokedBy, revokedAtMs, appcode.FromContext(ctx), sessionID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
affected, err := result.RowsAffected()
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
// affected=false 表示没有可吊销 session,调用方按幂等结果处理。
|
||
return affected > 0, nil
|
||
}
|
||
|
||
// FindThirdPartyIdentity 查找 provider + subject 绑定。
|
||
|
||
func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenHash string) (authdomain.Session, error) {
|
||
// refresh token hash 是唯一索引,最多命中一条 session。
|
||
var session authdomain.Session
|
||
var revokedAt sql.Null[int64]
|
||
err := r.db.QueryRowContext(ctx, `
|
||
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
|
||
FROM auth_sessions
|
||
WHERE app_code = ? AND refresh_token_hash = ?
|
||
`, appcode.FromContext(ctx), refreshTokenHash).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
|
||
if err == sql.ErrNoRows {
|
||
// 找不到 hash 按 revoked 返回,避免暴露 token 是否存在。
|
||
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
|
||
}
|
||
if err != nil {
|
||
return authdomain.Session{}, err
|
||
}
|
||
if revokedAt.Valid {
|
||
// sql.Null[int64] 转换成领域零值语义。
|
||
session.RevokedAtMs = revokedAt.V
|
||
}
|
||
|
||
return session, nil
|
||
}
|
||
|
||
func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (authdomain.Session, error) {
|
||
// session_id 来自已校验 access token 的 sid claim,仍必须回查服务端 session 状态。
|
||
var session authdomain.Session
|
||
var revokedAt sql.Null[int64]
|
||
err := r.db.QueryRowContext(ctx, `
|
||
SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
|
||
FROM auth_sessions
|
||
WHERE app_code = ? AND session_id = ?
|
||
`, appcode.FromContext(ctx), sessionID).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
|
||
if err == sql.ErrNoRows {
|
||
// 不暴露 session_id 是否存在,统一按 revoked 处理。
|
||
return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
|
||
}
|
||
if err != nil {
|
||
return authdomain.Session{}, err
|
||
}
|
||
if revokedAt.Valid {
|
||
session.RevokedAtMs = revokedAt.V
|
||
}
|
||
|
||
return session, nil
|
||
}
|
||
|
||
func insertSession(ctx context.Context, tx *sql.Tx, session authdomain.Session) error {
|
||
// insertSession 只在已有事务中使用,保证与 session 替换或三方注册同提交。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
|
||
VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
|
||
`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.CreatedAtMs, session.UpdatedAtMs)
|
||
if err != nil {
|
||
return mapAuthDuplicateError(err)
|
||
}
|
||
|
||
return nil
|
||
}
|