xsgz 登录后补拉 /auth/user 拿 roleId,不再只依赖 cas/callback 响应
核对前端源码发现两者的获取路径是分开的:
getOAuthToken: setToken(m.data.data?.access_token) // 只管 token
getUserInfo: POST /auth/user → data.role.roleId // roleId 在这里
axios 拦截器: s.headers.roleId = JSON.parse(getCurrentRole()).roleId
s.headers.Authorization = getToken() // 原文,无 Bearer 前缀
我们原先在同一处只读 cas/callback 的 data.userInfo.roleId。
若该校响应不含该嵌套结构,RoleID 会为空,导致:
- 所有后续请求缺少 roleId 头;
- 学校端可能直接拒绝,且报错信息不会指向真正原因。
改动:
- 登录成功后主动 POST /api/auth/user 取 data.role.roleId(roleName 同步);
- 该调用失败不阻断(slog.Warn 后沿用 cas 响应的值),保持向后兼容;
- 两者都拿不到 roleId 时才明确报错「未能获取角色信息」,
避免带着空 roleId 一路请求到被学校端拒绝;
- realName 缺失时回退 xm(国内学工系统常见字段名)。
顺带核实两处未被验证过的契约,结论是现有实现正确、无需改动:
- Authorization 头是 token 原文,不加 Bearer 前缀;
- RoleID 与前端 getCurrentRole().roleId 语义一致。
测试:/auth/user 的路径与 POST 方法、role/roleName/realName 解析、
realName 缺失回退 xm、非 0 code 必须报错。
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -110,13 +111,84 @@ func XsgzLogin(ctx context.Context, schoolID, casTicket string) (*XsgzSession, e
|
||||
return nil, fmt.Errorf("登录失败: %s", msg)
|
||||
}
|
||||
|
||||
return &XsgzSession{
|
||||
sess := &XsgzSession{
|
||||
Token: result.Data.AccessToken,
|
||||
RoleID: result.Data.UserInfo.RoleId,
|
||||
RoleName: result.Data.UserInfo.RoleName,
|
||||
RealName: result.Data.UserInfo.RealName,
|
||||
baseURL: base,
|
||||
client: client,
|
||||
}
|
||||
|
||||
// roleId 不能只依赖 cas/callback 的响应。前端把两者分开处理:
|
||||
// getOAuthToken: 只取 data.data.access_token(不管 roleId)
|
||||
// getUserInfo: POST /auth/user → data.role.roleId → setCurrentRole
|
||||
// 再由 axios 拦截器把 roleId 塞进每个请求头。
|
||||
// 若此处继续只读 cas 响应的 userInfo,一旦该校不返回该嵌套结构,
|
||||
// RoleID 就会为空,所有后续请求都缺 roleId 头。
|
||||
// 因此这里主动补一次 /auth/user,拿不到也不阻断(保持向后兼容)。
|
||||
if info, iErr := sess.fetchUserRole(ctx); iErr != nil {
|
||||
slog.Warn("xsgz 拉取用户角色失败,沿用 cas 响应中的 roleId",
|
||||
"roleId", sess.RoleID, "error", iErr)
|
||||
} else if info != nil {
|
||||
if info.RoleID != "" {
|
||||
sess.RoleID = info.RoleID
|
||||
}
|
||||
if info.RoleName != "" {
|
||||
sess.RoleName = info.RoleName
|
||||
}
|
||||
if info.RealName != "" {
|
||||
sess.RealName = info.RealName
|
||||
}
|
||||
}
|
||||
|
||||
if sess.RoleID == "" {
|
||||
return nil, fmt.Errorf("未能获取角色信息(roleId 为空),请刷新页面后重试")
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
// xsgzUserInfo /auth/user 返回的角色信息(前端 getUserInfo)。
|
||||
type xsgzUserInfo struct {
|
||||
RoleID string
|
||||
RoleName string
|
||||
RealName string
|
||||
}
|
||||
|
||||
// fetchUserRole 对应前端 GET/POST /api/auth/user,取 data.role.roleId。
|
||||
//
|
||||
// 前端因此把 roleId 存在 sessionStorage 并注入每个请求头。
|
||||
func (s *XsgzSession) fetchUserRole(ctx context.Context) (*xsgzUserInfo, error) {
|
||||
body, err := s.do(ctx, "POST", "/api/auth/user", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Role struct {
|
||||
RoleID string `json:"roleId"`
|
||||
RoleName string `json:"roleName"`
|
||||
} `json:"role"`
|
||||
RealName string `json:"realName"`
|
||||
Xm string `json:"xm"`
|
||||
} `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析用户信息失败: %w", err)
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("获取用户信息失败: %s", result.Message)
|
||||
}
|
||||
name := result.Data.RealName
|
||||
if name == "" {
|
||||
name = result.Data.Xm
|
||||
}
|
||||
return &xsgzUserInfo{
|
||||
RoleID: result.Data.Role.RoleID,
|
||||
RoleName: result.Data.Role.RoleName,
|
||||
RealName: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -360,3 +360,64 @@ func TestXsgzFetchLeaveDetailError(t *testing.T) {
|
||||
t.Fatal("code!=0 应报错")
|
||||
}
|
||||
}
|
||||
|
||||
// TestXsgzFetchUserRoleContract /auth/user 取 data.role.roleId。
|
||||
//
|
||||
// 前端不依赖 cas/callback 返回 roleId:
|
||||
//
|
||||
// getOAuthToken 只存 access_token
|
||||
// getUserInfo → POST /auth/user → data.role.roleId → 注入请求头
|
||||
func TestXsgzFetchUserRoleContract(t *testing.T) {
|
||||
var gotPath, gotMethod string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath, gotMethod = r.URL.Path, r.Method
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":{
|
||||
"realName":"张三",
|
||||
"role":{"roleId":"role-777","roleName":"学生"}
|
||||
}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
sess := newXsgzTestSession(t, srv)
|
||||
info, err := sess.fetchUserRole(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("获取用户角色失败: %v", err)
|
||||
}
|
||||
if gotPath != "/api/auth/user" {
|
||||
t.Fatalf("路径错误: %s", gotPath)
|
||||
}
|
||||
if gotMethod != http.MethodPost {
|
||||
t.Fatalf("方法应为 POST,实际 %s", gotMethod)
|
||||
}
|
||||
if info.RoleID != "role-777" || info.RoleName != "学生" || info.RealName != "张三" {
|
||||
t.Fatalf("解析异常: %+v", info)
|
||||
}
|
||||
}
|
||||
|
||||
// TestXsgzFetchUserRoleFallbackXm realName 缺失时回退 xm。
|
||||
func TestXsgzFetchUserRoleFallbackXm(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":{"xm":"李四","role":{"roleId":"r1"}}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
info, err := newXsgzTestSession(t, srv).fetchUserRole(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("失败: %v", err)
|
||||
}
|
||||
if info.RealName != "李四" {
|
||||
t.Fatalf("应回退 xm,实际 %q", info.RealName)
|
||||
}
|
||||
}
|
||||
|
||||
// TestXsgzFetchUserRoleError 非 0 code 必须报错。
|
||||
func TestXsgzFetchUserRoleError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"code":401,"message":"未登录"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if _, err := newXsgzTestSession(t, srv).fetchUserRole(context.Background()); err == nil {
|
||||
t.Fatal("code!=0 应报错")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user