Skip to content

Commit 36df3d1

Browse files
author
dushixiang
committed
增加 OIDC 登录方式,优化登录逻辑
1 parent 1973cf1 commit 36df3d1

File tree

15 files changed

+781
-224
lines changed

15 files changed

+781
-224
lines changed

config.example.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ App:
2121
Users:
2222
# 使用 Bcrypt 加密,默认密码为 admin123,建议首次登录后修改密码,搜索 bcrypt在线加密网站 即可
2323
admin: "$2y$12$7DXcOiX1D59xNTIn5riUKusAPLP88LxxoczWmUT83MBj5EFznbp8a"
24+
OIDC:
25+
Enabled: false
26+
Issuer: ""
27+
ClientID: ""
28+
ClientSecret: ""
29+
RedirectURL: "http://localhost:8080/oidc/callback"
2430

2531
# 串口配置
2632
Serial:

config/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ type AppConfig struct {
44
JWT JWTConfig `json:"JWT"`
55
Users map[string]string `json:"Users"` // 用户名 -> bcrypt加密的密码
66
Serial SerialConfig `json:"Serial"` // 串口配置
7+
OIDC *OIDCConfig `json:"OIDC"` // OIDC配置(可选)
78
}
89

910
// JWTConfig JWT配置
@@ -16,3 +17,12 @@ type JWTConfig struct {
1617
type SerialConfig struct {
1718
Port string `json:"Port"` // 串口路径,为空则自动检测
1819
}
20+
21+
// OIDCConfig OIDC认证配置
22+
type OIDCConfig struct {
23+
Enabled bool `json:"Enabled"` // 是否启用OIDC
24+
Issuer string `json:"Issuer"` // OIDC Provider的Issuer URL
25+
ClientID string `json:"ClientID"` // Client ID
26+
ClientSecret string `json:"ClientSecret"` // Client Secret
27+
RedirectURL string `json:"RedirectURL"` // 回调URL
28+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/dushixiang/uart_sms_forwarder
33
go 1.25.0
44

55
require (
6+
github.com/coreos/go-oidc/v3 v3.17.0
67
github.com/go-orz/cache v0.0.4
78
github.com/go-orz/orz v0.2.10
89
github.com/golang-jwt/jwt/v5 v5.3.0
@@ -14,6 +15,7 @@ require (
1415
go.bug.st/serial v1.6.4
1516
go.uber.org/zap v1.27.1
1617
golang.org/x/crypto v0.46.0
18+
golang.org/x/oauth2 v0.34.0
1719
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
1820
gorm.io/gorm v1.31.1
1921
)
@@ -26,6 +28,7 @@ require (
2628
github.com/glebarez/go-sqlite v1.22.0 // indirect
2729
github.com/glebarez/sqlite v1.11.0 // indirect
2830
github.com/go-errors/errors v1.5.1 // indirect
31+
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
2932
github.com/go-sql-driver/mysql v1.9.3 // indirect
3033
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
3134
github.com/jackc/pgpassfile v1.0.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
22
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3+
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
4+
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
35
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
46
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
57
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -17,6 +19,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
1719
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
1820
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
1921
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
22+
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
23+
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
2024
github.com/go-orz/cache v0.0.4 h1:A8EwJQPiuctmnukFqkWFv4yoOKVen7DEpCVjSJAkAtw=
2125
github.com/go-orz/cache v0.0.4/go.mod h1:qY5/YWUiMMFDHnWMCUJQakXILwJ/sIvbNCR04k08fBs=
2226
github.com/go-orz/orz v0.2.10 h1:SGUwZxAh7B73K1FJTTqKcYeQANpQqJF8V6ej/9kRl/I=
@@ -120,6 +124,8 @@ golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
120124
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
121125
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
122126
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
127+
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
128+
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
123129
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
124130
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
125131
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/app.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ func setup(app *orz.App) error {
9292
)
9393
serialService.SetScheduledTaskStatusUpdater(schedulerService.UpdateLastRunStatusByMsgId)
9494

95-
// 8. 初始化 Handler
96-
authHandler := handler.NewAuthHandler(logger, &appConfig)
95+
// 8. 初始化 OIDC 和 Account Service
96+
oidcService := service.NewOIDCService(logger, &appConfig)
97+
accountService := service.NewAccountService(logger, oidcService, &appConfig)
98+
99+
// 9. 初始化 Handler
100+
authHandler := handler.NewAuthHandler(logger, accountService)
97101
propertyHandler := handler.NewPropertyHandler(logger, propertyService, notifier)
98102
textMessageHandler := handler.NewTextMessageHandler(logger, textMessageService, textMessageRepo)
99103
serialHandler := handler.NewSerialHandler(logger, serialService)
@@ -107,10 +111,10 @@ func setup(app *orz.App) error {
107111
ScheduledTask: scheduledTaskHandler,
108112
}
109113

110-
// 9. 设置 API 路由
114+
// 10. 设置 API 路由
111115
setupApi(app, handlers, &appConfig, logger)
112116

113-
// 10. 启动后台服务
117+
// 11. 启动后台服务
114118
background := context.Background()
115119
// 启动串口服务
116120
go serialService.Start()
@@ -171,6 +175,9 @@ func setupApi(app *orz.App, handlers *Handlers, appConfig *config.AppConfig, log
171175

172176
// 登录路由(不需要认证)
173177
e.POST("/api/login", handlers.Auth.Login)
178+
e.GET("/api/auth/config", handlers.Auth.GetAuthConfig)
179+
e.GET("/api/auth/oidc/url", handlers.Auth.GetOIDCAuthURL)
180+
e.POST("/api/auth/oidc/callback", handlers.Auth.OIDCCallback)
174181

175182
// API 路由组(需要认证)
176183
api := e.Group("/api")

internal/handler/auth_handler.go

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,22 @@ package handler
33
import (
44
"net/http"
55

6-
"github.com/dushixiang/uart_sms_forwarder/config"
7-
"github.com/dushixiang/uart_sms_forwarder/internal/util"
6+
"github.com/dushixiang/uart_sms_forwarder/internal/service"
87
"github.com/labstack/echo/v4"
98
"go.uber.org/zap"
10-
"golang.org/x/crypto/bcrypt"
119
)
1210

1311
// AuthHandler 认证处理器
1412
type AuthHandler struct {
15-
logger *zap.Logger
16-
config *config.AppConfig
13+
logger *zap.Logger
14+
accountService *service.AccountService
1715
}
1816

1917
// NewAuthHandler 创建认证处理器
20-
func NewAuthHandler(logger *zap.Logger, config *config.AppConfig) *AuthHandler {
18+
func NewAuthHandler(logger *zap.Logger, accountService *service.AccountService) *AuthHandler {
2119
return &AuthHandler{
22-
logger: logger,
23-
config: config,
20+
logger: logger,
21+
accountService: accountService,
2422
}
2523
}
2624

@@ -42,7 +40,6 @@ func (h *AuthHandler) Login(c echo.Context) error {
4240
// 获取请求参数
4341
var req LoginRequest
4442
if err := c.Bind(&req); err != nil {
45-
h.logger.Warn("登录请求参数解析失败", zap.Error(err))
4643
return c.JSON(http.StatusBadRequest, map[string]string{
4744
"error": "请求参数错误",
4845
})
@@ -55,49 +52,75 @@ func (h *AuthHandler) Login(c echo.Context) error {
5552
})
5653
}
5754

58-
// 从配置中获取用户密码哈希
59-
passwordHash, exists := h.config.Users[req.Username]
60-
if !exists {
61-
h.logger.Warn("用户不存在", zap.String("username", req.Username))
55+
// 使用 AccountService 进行登录
56+
ctx := c.Request().Context()
57+
loginResp, err := h.accountService.Login(ctx, req.Username, req.Password)
58+
if err != nil {
6259
return c.JSON(http.StatusBadRequest, map[string]string{
6360
"error": "用户名或密码错误",
6461
})
6562
}
6663

67-
// 验证密码
68-
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password))
64+
// 返回 token 和用户信息
65+
return c.JSON(http.StatusOK, LoginResponse{
66+
Token: loginResp.Token,
67+
Username: loginResp.User.Username,
68+
ExpiresAt: loginResp.ExpiresAt,
69+
})
70+
}
71+
72+
// GetAuthConfig 获取认证配置
73+
func (h *AuthHandler) GetAuthConfig(c echo.Context) error {
74+
config := h.accountService.GetAuthConfig()
75+
return c.JSON(http.StatusOK, config)
76+
}
77+
78+
// GetOIDCAuthURL 获取 OIDC 认证 URL
79+
func (h *AuthHandler) GetOIDCAuthURL(c echo.Context) error {
80+
authURL, err := h.accountService.GetOIDCAuthURL()
6981
if err != nil {
70-
h.logger.Warn("密码验证失败",
71-
zap.String("username", req.Username),
72-
zap.Error(err),
73-
)
7482
return c.JSON(http.StatusBadRequest, map[string]string{
75-
"error": "用户名或密码错误",
83+
"error": err.Error(),
7684
})
7785
}
86+
return c.JSON(http.StatusOK, authURL)
87+
}
7888

79-
// 生成 JWT token
80-
token, expiresAt, err := util.GenerateToken(
81-
req.Username,
82-
h.config.JWT.Secret,
83-
h.config.JWT.ExpiresHours,
84-
)
85-
if err != nil {
86-
h.logger.Error("生成 token 失败",
87-
zap.String("username", req.Username),
88-
zap.Error(err),
89-
)
90-
return c.JSON(http.StatusInternalServerError, map[string]string{
91-
"error": "登录失败,请稍后重试",
89+
// OIDCCallbackRequest OIDC 回调请求
90+
type OIDCCallbackRequest struct {
91+
Code string `json:"code" validate:"required"`
92+
State string `json:"state" validate:"required"`
93+
}
94+
95+
// OIDCCallback 处理 OIDC 回调
96+
func (h *AuthHandler) OIDCCallback(c echo.Context) error {
97+
var req OIDCCallbackRequest
98+
if err := c.Bind(&req); err != nil {
99+
return c.JSON(http.StatusBadRequest, map[string]string{
100+
"error": "请求参数错误",
92101
})
93102
}
94103

95-
h.logger.Info("用户登录成功", zap.String("username", req.Username))
104+
if req.Code == "" || req.State == "" {
105+
return c.JSON(http.StatusBadRequest, map[string]string{
106+
"error": "缺少必要参数",
107+
})
108+
}
109+
110+
// 使用 AccountService 处理 OIDC 登录
111+
ctx := c.Request().Context()
112+
loginResp, err := h.accountService.LoginWithOIDC(ctx, req.Code, req.State)
113+
if err != nil {
114+
h.logger.Error("OIDC 登录失败", zap.Error(err))
115+
return c.JSON(http.StatusUnauthorized, map[string]string{
116+
"error": "OIDC 认证失败",
117+
})
118+
}
96119

97120
// 返回 token 和用户信息
98121
return c.JSON(http.StatusOK, LoginResponse{
99-
Token: token,
100-
Username: req.Username,
101-
ExpiresAt: expiresAt,
122+
Token: loginResp.Token,
123+
Username: loginResp.User.Username,
124+
ExpiresAt: loginResp.ExpiresAt,
102125
})
103126
}

0 commit comments

Comments
 (0)