Compare commits
2
Commits
v0.5.0
...
79c9e4d9b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c9e4d9b9 | ||
|
|
2fea315430 |
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)(版本段不记日期),版本号遵循语义化版本。
|
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)(版本段不记日期),版本号遵循语义化版本。
|
||||||
|
|
||||||
|
## [0.5.1]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 修复外部身份(OAuth2)登录 / 绑定无系统日志的问题:回调成功记 200(用户名为面板账号)、失败记 401(附失败原因);此前回调为 GET 请求不经写操作日志中间件,完全无记录
|
||||||
|
|
||||||
## [0.5.0]
|
## [0.5.0]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
v0.5.0
|
v0.5.1
|
||||||
|
|||||||
+25
-2
@@ -5,10 +5,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
_ "oci-portal/internal/model" // swagger 注解引用
|
"oci-portal/internal/model"
|
||||||
|
|
||||||
"oci-portal/internal/service"
|
"oci-portal/internal/service"
|
||||||
)
|
)
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
type authxHandler struct {
|
type authxHandler struct {
|
||||||
auth *service.AuthService
|
auth *service.AuthService
|
||||||
oauth *service.OAuthService
|
oauth *service.OAuthService
|
||||||
|
logs *service.SystemLogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- TOTP(JWT 组内) ----
|
// ---- TOTP(JWT 组内) ----
|
||||||
@@ -301,10 +303,12 @@ func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) {
|
|||||||
// @Success 302 "登录 token 经 fragment 回前端,绑定回设置页"
|
// @Success 302 "登录 token 经 fragment 回前端,绑定回设置页"
|
||||||
// @Router /api/v1/auth/oauth/{provider}/callback [get]
|
// @Router /api/v1/auth/oauth/{provider}/callback [get]
|
||||||
func (h *authxHandler) oauthCallback(c *gin.Context) {
|
func (h *authxHandler) oauthCallback(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
provider := c.Param("provider")
|
provider := c.Param("provider")
|
||||||
token, _, mode, err := h.oauth.HandleCallback(
|
token, username, mode, err := h.oauth.HandleCallback(
|
||||||
c.Request.Context(), provider, c.Query("state"), c.Query("code"))
|
c.Request.Context(), provider, c.Query("state"), c.Query("code"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
h.recordOauth(c, username, http.StatusUnauthorized, oauthErrText(err), start)
|
||||||
target := "/login"
|
target := "/login"
|
||||||
if mode == "bind" {
|
if mode == "bind" {
|
||||||
target = "/settings"
|
target = "/settings"
|
||||||
@@ -312,6 +316,7 @@ func (h *authxHandler) oauthCallback(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusFound, target+"?oauthError="+url.QueryEscape(oauthErrText(err)))
|
c.Redirect(http.StatusFound, target+"?oauthError="+url.QueryEscape(oauthErrText(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
h.recordOauth(c, username, http.StatusOK, "", start)
|
||||||
if mode == "bind" {
|
if mode == "bind" {
|
||||||
// 绑定模式:版本已递增,新 token 经 fragment 带回设置页无感换发
|
// 绑定模式:版本已递增,新 token 经 fragment 带回设置页无感换发
|
||||||
c.Redirect(http.StatusFound, "/settings?oauth=bound#oauthToken="+url.QueryEscape(token))
|
c.Redirect(http.StatusFound, "/settings?oauth=bound#oauthToken="+url.QueryEscape(token))
|
||||||
@@ -321,6 +326,24 @@ func (h *authxHandler) oauthCallback(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusFound, "/login#oauthToken="+url.QueryEscape(token))
|
c.Redirect(http.StatusFound, "/login#oauthToken="+url.QueryEscape(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordOauth 把外部登录 / 绑定结果记入系统日志——回调是 GET,
|
||||||
|
// 不经写方法中间件;实际响应恒为 302,日志按语义记 200 / 401。
|
||||||
|
func (h *authxHandler) recordOauth(c *gin.Context, username string, status int, errMsg string, start time.Time) {
|
||||||
|
if h.logs == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.logs.Record(model.SystemLog{
|
||||||
|
Username: username,
|
||||||
|
Method: c.Request.Method,
|
||||||
|
Path: requestPath(c),
|
||||||
|
Status: status,
|
||||||
|
DurationMs: time.Since(start).Milliseconds(),
|
||||||
|
ClientIP: requestIP(c),
|
||||||
|
UserAgent: truncateLogField(c.Request.UserAgent(), 256),
|
||||||
|
ErrMsg: errMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// oauthErrText 把流程错误转为用户可读文案;内部错误不透出细节。
|
// oauthErrText 把流程错误转为用户可读文案;内部错误不透出细节。
|
||||||
func oauthErrText(err error) string {
|
func oauthErrText(err error) string {
|
||||||
for _, known := range []error{service.ErrOAuthNotConfigured, service.ErrOAuthNoAppURL, service.ErrOAuthDisabled, service.ErrOAuthState, service.ErrOAuthNotBound, service.ErrOAuthBound} {
|
for _, known := range []error{service.ErrOAuthNotConfigured, service.ErrOAuthNoAppURL, service.ErrOAuthDisabled, service.ErrOAuthState, service.ErrOAuthNotBound, service.ErrOAuthBound} {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func registerAuthPublic(v1 *gin.RouterGroup, auth *service.AuthService, oauth *s
|
|||||||
v1.POST("/auth/login", ah.login)
|
v1.POST("/auth/login", ah.login)
|
||||||
|
|
||||||
// 外部身份登录:provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调
|
// 外部身份登录:provider 列表 / 授权跳转(bind 模式 handler 内校验 JWT)/ 回调
|
||||||
ax := &authxHandler{auth: auth, oauth: oauth}
|
ax := &authxHandler{auth: auth, oauth: oauth, logs: systemLogs}
|
||||||
v1.GET("/auth/oauth/providers", ax.oauthProviders)
|
v1.GET("/auth/oauth/providers", ax.oauthProviders)
|
||||||
v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize)
|
v1.GET("/auth/oauth/:provider/authorize", ax.oauthAuthorize)
|
||||||
v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback)
|
v1.GET("/auth/oauth/:provider/callback", ax.oauthCallback)
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ type externalIdentity struct {
|
|||||||
|
|
||||||
// HandleCallback 完成授权码回调:换取身份后,bind 模式写绑定、login 模式签发 JWT;
|
// HandleCallback 完成授权码回调:换取身份后,bind 模式写绑定、login 模式签发 JWT;
|
||||||
// token 仅 login 模式非空;mode 尽力返回(state 无效时为空),供 api 决定错误回跳页面。
|
// token 仅 login 模式非空;mode 尽力返回(state 无效时为空),供 api 决定错误回跳页面。
|
||||||
func (o *OAuthService) HandleCallback(ctx context.Context, provider, state, code string) (token, display, mode string, err error) {
|
func (o *OAuthService) HandleCallback(ctx context.Context, provider, state, code string) (token, username, mode string, err error) {
|
||||||
p, err := o.takeState(provider, state)
|
p, err := o.takeState(provider, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
@@ -198,14 +198,14 @@ func (o *OAuthService) HandleCallback(ctx context.Context, provider, state, code
|
|||||||
}
|
}
|
||||||
if p.mode == "bind" {
|
if p.mode == "bind" {
|
||||||
if err := o.bind(ctx, p.username, provider, ident); err != nil {
|
if err := o.bind(ctx, p.username, provider, ident); err != nil {
|
||||||
return "", ident.Display, p.mode, err
|
return "", p.username, p.mode, err
|
||||||
}
|
}
|
||||||
// 绑定属敏感变更:版本递增使旧令牌失效,同时为操作者签新令牌随回跳带回
|
// 绑定属敏感变更:版本递增使旧令牌失效,同时为操作者签新令牌随回跳带回
|
||||||
token, _, err := o.auth.RevokeSessions(ctx, p.username)
|
token, _, err := o.auth.RevokeSessions(ctx, p.username)
|
||||||
return token, ident.Display, p.mode, err
|
return token, p.username, p.mode, err
|
||||||
}
|
}
|
||||||
token, display, err = o.loginByIdentity(ctx, provider, ident)
|
token, username, err = o.loginByIdentity(ctx, provider, ident)
|
||||||
return token, display, p.mode, err
|
return token, username, p.mode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchIdentity 用授权码向 provider 换取稳定 subject 与展示名。
|
// fetchIdentity 用授权码向 provider 换取稳定 subject 与展示名。
|
||||||
@@ -311,7 +311,7 @@ func (o *OAuthService) loginByIdentity(ctx context.Context, provider string, ide
|
|||||||
return "", "", fmt.Errorf("find bound user: %w", err)
|
return "", "", fmt.Errorf("find bound user: %w", err)
|
||||||
}
|
}
|
||||||
token, _, err := o.auth.signToken(user.Username, user.TokenVersion)
|
token, _, err := o.auth.signToken(user.Username, user.TokenVersion)
|
||||||
return token, ident.Display, err
|
return token, user.Username, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identities 列出账号已绑定的外部身份。
|
// Identities 列出账号已绑定的外部身份。
|
||||||
|
|||||||
@@ -169,12 +169,12 @@ func TestOAuthBindLoginUnbind(t *testing.T) {
|
|||||||
t.Errorf("重复绑定 err = %v, want ErrOAuthBound", err)
|
t.Errorf("重复绑定 err = %v, want ErrOAuthBound", err)
|
||||||
}
|
}
|
||||||
// 已绑定身份可登录并拿到有效 JWT
|
// 已绑定身份可登录并拿到有效 JWT
|
||||||
token, display, err := o.loginByIdentity(ctx, "github", externalIdentity{Subject: "10086", Display: "octocat"})
|
token, loginUser, err := o.loginByIdentity(ctx, "github", externalIdentity{Subject: "10086", Display: "octocat"})
|
||||||
if err != nil || token == "" {
|
if err != nil || token == "" {
|
||||||
t.Fatalf("loginByIdentity: %v", err)
|
t.Fatalf("loginByIdentity: %v", err)
|
||||||
}
|
}
|
||||||
if display != "octocat" {
|
if loginUser != "admin" {
|
||||||
t.Errorf("display = %q", display)
|
t.Errorf("loginUser = %q, want admin", loginUser)
|
||||||
}
|
}
|
||||||
if username, err := auth.ParseToken(context.Background(), token); err != nil || username != "admin" {
|
if username, err := auth.ParseToken(context.Background(), token); err != nil || username != "admin" {
|
||||||
t.Errorf("token 应属 admin, got %q (%v)", username, err)
|
t.Errorf("token 应属 admin, got %q (%v)", username, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user