248 lines
9.5 KiB
Go
248 lines
9.5 KiB
Go
// Package auth 实现 user-service 的认证、三方登录、密码登录和 refresh session 用例。
|
||
package auth
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"hyapp/pkg/idgen"
|
||
authdomain "hyapp/services/user-service/internal/domain/auth"
|
||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||
userservice "hyapp/services/user-service/internal/service/user"
|
||
)
|
||
|
||
const (
|
||
// hashAlgBcrypt 是当前密码 hash 算法标识。
|
||
hashAlgBcrypt = "bcrypt"
|
||
// tokenType 是对外 token 类型。
|
||
tokenType = "Bearer"
|
||
// loginPassword 是密码登录审计类型。
|
||
loginPassword = "password"
|
||
// loginThird 是三方登录审计类型。
|
||
loginThird = "third_party"
|
||
// loginRefresh 是 refresh token 轮换审计类型。
|
||
loginRefresh = "refresh"
|
||
// loginSetPass 是设置密码审计类型。
|
||
loginSetPass = "set_password"
|
||
// resultSuccess 是审计成功结果。
|
||
resultSuccess = "success"
|
||
// resultFailed 是审计失败结果。
|
||
resultFailed = "failed"
|
||
)
|
||
|
||
// AuthRepository 是认证用例的持久化边界。
|
||
// 跨表写入仍按用例保留事务入口,例如三方首次登录要一次性创建用户、默认短号、三方身份和 session。
|
||
type AuthRepository interface {
|
||
// SetPassword 为已有用户首次写入密码身份。
|
||
SetPassword(ctx context.Context, account authdomain.PasswordAccount) error
|
||
// FindPasswordByDisplayUserID 通过当前 active display_user_id 读取密码身份。
|
||
FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error)
|
||
// CreateSession 创建新的 refresh session。
|
||
CreateSession(ctx context.Context, session authdomain.Session) error
|
||
// FindActiveSessionByRefreshHash 通过 refresh token hash 查找未过期未吊销 session。
|
||
FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error)
|
||
// ReplaceSession 原子吊销旧 session 并创建新 session。
|
||
ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error
|
||
// RevokeSession 按 session_id 或 refresh token hash 吊销 session。
|
||
RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error)
|
||
// FindThirdPartyIdentity 查找 provider + subject 的绑定。
|
||
FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error)
|
||
// CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。
|
||
CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session) error
|
||
// RecordLoginAudit 记录登录审计,主链路不会因为审计失败而失败。
|
||
RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error
|
||
}
|
||
|
||
// UserRepository 为 auth service 提供登录签发前的用户快照读取。
|
||
// Auth service 不关心用户创建和短号修改细节,只需要判断用户是否可登录以及签发当前展示号。
|
||
type UserRepository interface {
|
||
// GetUser 返回登录签发前需要的用户快照。
|
||
GetUser(ctx context.Context, userID int64) (userdomain.User, error)
|
||
}
|
||
|
||
// IdentityRepository 为 auth service 提供靓号懒过期能力。
|
||
// 过期恢复仍由 identity repository 的事务完成,避免 auth service 手动改 users 和短号表。
|
||
type IdentityRepository interface {
|
||
// ExpirePrettyDisplayUserID 原子恢复过期靓号,避免 token 签发过期展示号。
|
||
ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error)
|
||
}
|
||
|
||
// Repository 是兼容旧 WithRepository 调用的组合接口。
|
||
// 新增实现可以分别注入 AuthRepository、UserRepository 和 IdentityRepository,避免继续扩大单一接口。
|
||
type Repository interface {
|
||
AuthRepository
|
||
UserRepository
|
||
IdentityRepository
|
||
}
|
||
|
||
// ThirdPartyVerifier 把 provider credential 校验成稳定 provider_subject。
|
||
// gateway 不接触 provider secret,三方校验必须停留在 user-service。
|
||
type ThirdPartyVerifier interface {
|
||
// Verify 校验 provider credential,并返回 provider 内稳定身份。
|
||
Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error)
|
||
}
|
||
|
||
// Config 保存认证底座需要的稳定参数。
|
||
type Config struct {
|
||
// Issuer 写入 access token 的 iss claim。
|
||
Issuer string
|
||
// AccessTokenTTLSec 是短期 access token 有效期。
|
||
AccessTokenTTLSec int64
|
||
// RefreshTokenTTLSec 是服务端 refresh session 有效期。
|
||
RefreshTokenTTLSec int64
|
||
// SigningAlg 当前只支持 HS256。
|
||
SigningAlg string
|
||
// SigningSecret 是 HS256 签名密钥。
|
||
SigningSecret string
|
||
}
|
||
|
||
// Service 承载登录注册用例。
|
||
type Service struct {
|
||
// cfg 保存 token 签发配置。
|
||
cfg Config
|
||
// authRepository 负责密码身份、session、三方绑定和登录审计。
|
||
authRepository AuthRepository
|
||
// userRepository 提供登录签发前的用户主状态读取。
|
||
userRepository UserRepository
|
||
// identityRepository 提供靓号懒过期恢复事务。
|
||
identityRepository IdentityRepository
|
||
// countryRegionRepository 提供三方首次注册时的国家主数据校验和区域解析。
|
||
countryRegionRepository userservice.CountryRegionRepository
|
||
// idGenerator 生成三方首次登录时的新 user_id。
|
||
idGenerator userservice.IDGenerator
|
||
// displayUserIDAllocator 生成三方首次登录时的默认短号候选。
|
||
displayUserIDAllocator userservice.DisplayUserIDAllocator
|
||
// thirdPartyVerifier 把 provider credential 校验成稳定 subject。
|
||
thirdPartyVerifier ThirdPartyVerifier
|
||
// now 是服务时钟,测试可注入固定时间。
|
||
now func() time.Time
|
||
// allocateMaxAttempts 是默认短号冲突重试上限。
|
||
allocateMaxAttempts int
|
||
}
|
||
|
||
// Option 调整 auth service 的依赖和策略。
|
||
type Option func(*Service)
|
||
|
||
// New 创建认证服务。
|
||
func New(cfg Config, options ...Option) *Service {
|
||
// 默认依赖只保证可构造,生产必须通过 app 层注入 repository 和真实配置。
|
||
svc := &Service{
|
||
cfg: cfg,
|
||
idGenerator: idgen.NewInt64Generator(0),
|
||
displayUserIDAllocator: userservice.NumericDisplayUserIDAllocator{},
|
||
thirdPartyVerifier: NewStaticThirdPartyVerifier([]string{"wechat"}),
|
||
now: time.Now,
|
||
allocateMaxAttempts: 8,
|
||
}
|
||
|
||
for _, option := range options {
|
||
// option 只替换依赖和策略,不执行 IO。
|
||
option(svc)
|
||
}
|
||
|
||
return svc
|
||
}
|
||
|
||
// WithRepository 注入认证持久化实现。
|
||
func WithRepository(repository Repository) Option {
|
||
return func(s *Service) {
|
||
// 兼容旧构造方式:同一个 repository 同时承载 auth/user/identity 能力。
|
||
s.authRepository = repository
|
||
s.userRepository = repository
|
||
s.identityRepository = repository
|
||
if countryRegionRepository, ok := repository.(userservice.CountryRegionRepository); ok {
|
||
// 国家主数据校验是注册写 users.country 前的强约束。
|
||
s.countryRegionRepository = countryRegionRepository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithAuthRepository 注入纯认证持久化实现。
|
||
func WithAuthRepository(repository AuthRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
// nil 不覆盖现有配置,避免测试误传导致 service 不可用。
|
||
s.authRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithUserRepository 注入用户快照读取实现。
|
||
func WithUserRepository(repository UserRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
// auth service 只读取用户快照,不直接创建或修改用户资料。
|
||
s.userRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithIdentityRepository 注入短号懒过期实现。
|
||
func WithIdentityRepository(repository IdentityRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
// identityRepository 只负责过期靓号恢复事务。
|
||
s.identityRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithCountryRegionRepository 注入国家和区域解析 repository。
|
||
func WithCountryRegionRepository(repository userservice.CountryRegionRepository) Option {
|
||
return func(s *Service) {
|
||
if repository != nil {
|
||
s.countryRegionRepository = repository
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithIDGenerator 注入 user_id 发号器。
|
||
func WithIDGenerator(generator userservice.IDGenerator) Option {
|
||
return func(s *Service) {
|
||
if generator != nil {
|
||
// 三方首次登录需要生成新用户 ID。
|
||
s.idGenerator = generator
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithDisplayUserIDAllocator 注入短号候选生成器。
|
||
func WithDisplayUserIDAllocator(allocator userservice.DisplayUserIDAllocator) Option {
|
||
return func(s *Service) {
|
||
if allocator != nil {
|
||
// allocator 只生成候选,唯一性由 repository 事务保证。
|
||
s.displayUserIDAllocator = allocator
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithThirdPartyVerifier 注入三方 provider 校验器。
|
||
func WithThirdPartyVerifier(verifier ThirdPartyVerifier) Option {
|
||
return func(s *Service) {
|
||
if verifier != nil {
|
||
// verifier 可替换为真实三方 SDK 实现。
|
||
s.thirdPartyVerifier = verifier
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithClock 注入时间函数,保证 session 轮换测试稳定。
|
||
func WithClock(now func() time.Time) Option {
|
||
return func(s *Service) {
|
||
if now != nil {
|
||
// 固定时钟用于 session 过期和 refresh 轮换测试。
|
||
s.now = now
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithDisplayUserIDAllocateMaxAttempts 配置默认短号分配重试次数。
|
||
func WithDisplayUserIDAllocateMaxAttempts(maxAttempts int) Option {
|
||
return func(s *Service) {
|
||
if maxAttempts > 0 {
|
||
// 只接受正数,避免三方注册永远无法分配默认短号。
|
||
s.allocateMaxAttempts = maxAttempts
|
||
}
|
||
}
|
||
}
|