优化报销管理增加挂靠
This commit is contained in:
1 parent
7b1c94b5ff
commit
ee49ef15b5
24 files changed
+1267
-70
No files matched your search
@@ -35,14 +35,28 @@ func (c *AuthLoginController) LoginPage() {
|
||||
step := c.GetString("step")
|
||||
errMsg := c.GetString("error")
|
||||
|
||||
// 已登录且已选企业:直接放行到 authorize,无需再输密码
|
||||
// 已登录且已选企业:直接放行,无需再输密码。
|
||||
// 即使 redirect 丢失也要放行到业务站首页,否则会一直停留在登录页,
|
||||
// 表现为「登录后还是跳回登录界面」。
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid != "" && step != "tenant" {
|
||||
if session, err := authsvc.GetSession(sid); err == nil && session.Tid != authsvc.PendingTenantID {
|
||||
if back := decodeRedirect(redirect); back != "" {
|
||||
if session, err := authsvc.GetSession(sid); err == nil {
|
||||
if session.Tid == authsvc.PendingTenantID {
|
||||
// 已登录但还没选企业:直接带到企业选择步骤,
|
||||
// 不要再渲染一次登录表单让用户重新登录。
|
||||
target := "/auth/login?step=tenant&client_id=" + url.QueryEscape(clientID) +
|
||||
"&redirect=" + url.QueryEscape(redirect)
|
||||
c.Redirect(target, 302)
|
||||
return
|
||||
}
|
||||
if back := safeBackURL(clientID, redirect); back != "" {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 会话已被吊销/过期:清掉失效 Cookie,避免后续请求继续带上它
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
sid = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +69,17 @@ func (c *AuthLoginController) LoginPage() {
|
||||
// 只有一家企业直接进入,无需展示选择页
|
||||
if len(tenants) == 1 && step == "tenant" {
|
||||
if _, _, err := authsvc.ChooseTenant(sid, tenants[0].Tid, clientID); err == nil {
|
||||
if back := decodeRedirect(redirect); back != "" {
|
||||
if back := safeBackURL(clientID, redirect); back != "" {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if step == "tenant" {
|
||||
// 选企业时会话已失效:不要渲染一个空的企业列表,退回登录页重新登录
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
c.Redirect("/auth/login?client_id="+url.QueryEscape(clientID), 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +187,7 @@ func (c *AuthLoginController) LoginSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
back := decodeRedirect(req.Redirect)
|
||||
back := safeBackURL(req.ClientID, req.Redirect)
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
@@ -236,7 +255,7 @@ func (c *AuthLoginController) SwitchTenant() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "切换成功",
|
||||
"redirect": decodeRedirect(req.Redirect),
|
||||
"redirect": safeBackURL(req.ClientID, req.Redirect),
|
||||
"data": map[string]interface{}{"tokens": tokens, "identity": profile},
|
||||
})
|
||||
}
|
||||
@@ -360,6 +379,29 @@ func (c *AuthLoginController) LogoutAction() {
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已登出"})
|
||||
}
|
||||
|
||||
// ClearSiteData 清除当前站点在浏览器中的全部缓存数据
|
||||
// POST /auth/clear-site-data
|
||||
//
|
||||
// 登录页「清除缓存」按钮调用。前端 JS 已清理同源的 Web Storage / Cache
|
||||
// Storage / IndexedDB / Service Worker,但 yz_sid 是 HttpOnly Cookie,前端
|
||||
// 读不到也删不掉,因此这里:
|
||||
// - 用 Set-Cookie 直接删除会话 Cookie(兼容所有浏览器);
|
||||
// - 返回 Clear-Site-Data 响应头,让浏览器在安全上下文(HTTPS / localhost)
|
||||
// 下把本 origin 的 cookies / storage / HTTP cache 一并清空。
|
||||
//
|
||||
// Clear-Site-Data 严格限定在响应所属 origin,不会影响其它站点。
|
||||
func (c *AuthLoginController) ClearSiteData() {
|
||||
// Cookie 被清后服务端会话将无人使用,同步吊销,避免留下仍有效的孤儿会话
|
||||
// 占用「1 号 1 机」名额。
|
||||
if sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName)); sid != "" {
|
||||
_ = authsvc.RevokeSession(sid, models.RevokeReasonLogout)
|
||||
}
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
c.Ctx.Output.Header("Clear-Site-Data", `"cache", "cookies", "storage"`)
|
||||
c.Ctx.Output.Header("Cache-Control", "no-store")
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已清除"})
|
||||
}
|
||||
|
||||
// VerifyConfig 登录验证配置(替代 /backend/login/getOpenVerify)
|
||||
// GET /auth/verify-config?tid=
|
||||
//
|
||||
@@ -391,6 +433,34 @@ func (c *AuthLoginController) VerifyConfig() {
|
||||
|
||||
// ---------------------------------------------------------------- 工具
|
||||
|
||||
// safeBackURL 计算登录成功后的回跳地址。
|
||||
//
|
||||
// 优先使用业务端传来的 authorize 地址(base64url 编码);若链路中 redirect
|
||||
// 丢失(直接打开登录页、单点登出后再次登录、选企业时参数未透传等),则退回
|
||||
// 该应用已登记回跳地址所在的站点首页。业务站没有令牌时会自动重新发起 OIDC,
|
||||
// 认证中心此时已有会话可静默签码完成登录,不会卡在登录页形成循环。
|
||||
func safeBackURL(clientID, redirect string) string {
|
||||
if back := decodeRedirect(redirect); back != "" {
|
||||
return back
|
||||
}
|
||||
client, err := findClient(strings.TrimSpace(clientID))
|
||||
if err != nil || client.RedirectURIs == nil {
|
||||
return ""
|
||||
}
|
||||
var list []string
|
||||
if err := json.Unmarshal([]byte(*client.RedirectURIs), &list); err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, item := range list {
|
||||
u, err := url.Parse(strings.TrimSpace(item))
|
||||
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
continue
|
||||
}
|
||||
return u.Scheme + "://" + u.Host + "/"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeRedirect(encoded string) string {
|
||||
if encoded == "" {
|
||||
return ""
|
||||
|
||||
@@ -368,17 +368,17 @@ func authorizePath(c *AuthOidcController) string {
|
||||
return "/auth/authorize"
|
||||
}
|
||||
|
||||
// clearSessionCookie 清除认证中心会话 Cookie
|
||||
// clearSessionCookie 清除认证中心会话 Cookie。
|
||||
// 与写入复用同一实现:http 环境写入的 Cookie 不带 Secure,若清除时硬编码
|
||||
// Secure,浏览器会拒绝删除,失效的 yz_sid 一直残留,authorize 每次都判定
|
||||
// 未登录并把用户送回登录页。
|
||||
func clearSessionCookie(c *AuthOidcController) {
|
||||
c.Ctx.Output.Header("Set-Cookie",
|
||||
fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax", sessionCookieName))
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
}
|
||||
|
||||
// setSessionCookie 写入认证中心会话 Cookie
|
||||
func setSessionCookie(c *AuthOidcController, sid string) {
|
||||
c.Ctx.Output.Header("Set-Cookie",
|
||||
fmt.Sprintf("%s=%s; Path=/; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
|
||||
sessionCookieName, sid, sessionCookieTTL))
|
||||
setSessionCookieForCtx(c.Ctx, sid)
|
||||
}
|
||||
|
||||
func bearerToken(c *AuthOidcController) string {
|
||||
@@ -526,7 +526,10 @@ func exchangeCode(code, verifier, clientID string) (*authsvc.TokenPair, *jwtutil
|
||||
Filter("code_hash", stored.CodeHash).
|
||||
Update(map[string]interface{}{"used": 1})
|
||||
|
||||
session, err := authsvc.CreateSession(authsvc.SessionInfo{
|
||||
// 复用登录阶段建立的会话,切勿在此新建:新建会话会触发「1 号 1 机」
|
||||
// 互踢,把浏览器 yz_sid Cookie 对应的会话吊销,导致下一次回认证中心
|
||||
// 又看到登录页(登录成功后反复跳回登录界面)。
|
||||
session, err := authsvc.ReuseSession(authsvc.SessionInfo{
|
||||
IdentityID: stored.IdentityID,
|
||||
Tid: stored.Tid,
|
||||
ClientID: clientID,
|
||||
|
||||
@@ -268,6 +268,12 @@ func (c *AuthThirdController) Callback() {
|
||||
c.Redirect("/auth/authorize?"+v.Encode(), 302)
|
||||
return
|
||||
}
|
||||
// 缺少 OIDC 参数时也要把已登录用户送回业务站,由其重新发起授权流程;
|
||||
// 直接回登录页会让「登录成功」看起来又变成了未登录。
|
||||
if back := safeBackURL(st.ClientID, ""); back != "" {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
c.Redirect("/auth/login", 302)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user