书接上文,本篇记录一下如何在项目中使用助手鉴权
例子是我正在开发的面试系统,框架是 go-zero
示例地址: https://github.com/hduhelp/interview_backend/tree/9c05efb6a8614f79871876c0376a8d2cc123ceed
(后面重构了,结构可能有变化)
(私有仓库,需杭助内部身份)
先来看 API 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| syntax = "v1"
// 目前仅测试杭助登录 service user { @handler loginJump get /login/jump (loginJumpRequest) // 直接跳到杭助登录
@handler LoginCallback get /login/callback (LoginCallbackRequest) // 杭助登录回调
}
// 杭助登录回调 type ( loginJumpRequest { From string `form:"from,optional"` }
LoginCallbackRequest { Code string `form:"code"` // 杭助登录返回的code State string `form:"state"` // 杭助登录返回的state } )
|
可以看见,有两个路由:
-
/login/jump
构造 URL 直接跳转杭助鉴权
可选的 From
参数,用于记录从哪跳过来的,可以登录之后跳过去
-
/login/callback
接收杭助登录回来的路由
拿参数去请求杭助 token
再拿凭据去请求学生信息,如果成功就算登录成功
判断该学生属于哪个用户类型,并构造 JWT token
然后携 JWT token跳转至前端登录路由,前端拿到 JWT token
再来看配置文件
yaml 配置文件也是要加一些东西的,包括路由和票据,这样下面就可以轻松地读取了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Name: user Host: 0.0.0.0 Port: 8000
Routes: Frontend: https://frontend.interview.hduhelp.com Backend: https://backend.interview.hduhelp.com
Ticket: ClientId: ClientSecret:
Auth: AccessSecret: ThisIsMySecret AccessExpire: 1296000
|
当然啦, config.go
也是要加上的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package config
import "github.com/zeromicro/go-zero/rest"
type Config struct { rest.RestConf Auth struct { AccessSecret string AccessExpire int64 } Routes struct { Frontend string Backend string } Ticket struct { ClientId string ClientSecret string } }
|
handler 层
这个 go-zero 并没有能在 API 文件中定义跳转行为的语法,所以要自己去路由层编辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func loginJumpHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.LoginJumpRequest if err := httpx.Parse(r, &req); err != nil { httpx.Error(w, err) return }
l := logic.NewLoginJumpLogic(r.Context(), svcCtx) redirectUrl, err := l.LoginJump(&req) if err != nil { httpx.Error(w, err) } else { http.Redirect(w, r, redirectUrl, http.StatusFound) } } }
|
另外一个也是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func LoginCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req types.LoginCallbackRequest if err := httpx.Parse(r, &req); err != nil { httpx.Error(w, err) return }
l := logic.NewLoginCallbackLogic(r.Context(), svcCtx) redirectUrl, err := l.LoginCallback(&req) if err != nil { httpx.Error(w, err) } else { http.Redirect(w, r, redirectUrl, http.StatusFound) } } }
|
/login/jump logic
这个算是比较好理解的,只需要一个 loginjumplogic.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
func (l *LoginJumpLogic) LoginJump(req *types.LoginJumpRequest) (string, error) {
if req.From == "" { req.From = l.svcCtx.Config.Routes.Frontend + "/#/login" }
query := url.Values{} query.Add("response_type", "code") query.Add("client_id", l.svcCtx.Config.Ticket.ClientId) query.Add("redirect_uri", l.svcCtx.Config.Routes.Backend+"/login/callback") query.Add("state", svc.NewState())
redirectUrl := url.URL{ Scheme: "https", Host: "api.hduhelp.com", Path: "/oauth/authorize", RawQuery: query.Encode(), }
return redirectUrl.String(), nil }
|
附:servicecontext.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package svc
import ( "github.com/zeromicro/go-zero/zrpc" "interview_backend/0_user/api/internal/config" "interview_backend/0_user/api/internal/types" "interview_backend/0_user/rpc/userclient" )
type ServiceContext struct { Config config.Config UserRpc userclient.User }
func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)), } }
func NewState() string {
return "This_is_a_new_session"
}
func CheckState(state string) (bool, error) { return true, nil }
func GetUserType(types.GetStudentInfoResponse) (int32, error) { return 1, nil }
|
/login/callback logic
这个需要向杭助请求 token 和学生信息之类的,所以我决定抽离一下服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| . ├── etc │ ├── user.yaml │ └── user.yaml.example ├── internal │ ├── config │ │ └── config.go │ ├── handler │ │ ├── logincallbackhandler.go │ │ ├── loginjumphandler.go │ │ └── routes.go │ ├── logic │ │ ├── hduHelpService.go │ │ ├── logincallbacklogic.go │ │ └── loginjumplogic.go │ ├── middleware │ ├── svc │ │ └── servicecontext.go │ └── types │ ├── hduHelpServiceTypes.go │ └── types.go ├── user.api └── user.go
|
hduHelpService.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| package logic
import ( "errors" "fmt" "github.com/parnurzeal/gorequest" "interview_backend/0_user/api/internal/types" "net/http" "net/url" "time" )
func (l *LoginCallbackLogic) GetStudentInfo(token string) (types.GetStudentInfoResponse, error) { reqUrl := url.URL{ Scheme: "https", Host: "api.hduhelp.com", Path: "/salmon_base/student/info", } res := types.GetStudentInfoResponse{} _, _, err := gorequest.New(). Get(reqUrl.String()). Retry(3, time.Second, http.StatusBadRequest, http.StatusInternalServerError). AppendHeader("Authorization", "token "+token).EndStruct(&res) if err != nil { return res, errors.New(fmt.Sprintf("%v", err)) } else { return res, nil } }
func (l *LoginCallbackLogic) GetToken(code, state string) (string, error) { query := make(url.Values) query.Add("client_id", l.svcCtx.Config.Ticket.ClientId) query.Add("client_secret", l.svcCtx.Config.Ticket.ClientSecret) query.Add("grant_type", "authorization_code") query.Add("code", code) query.Add("state", state)
reqUrl := url.URL{ Scheme: "https", Host: "api.hduhelp.com", Path: "/oauth/token", RawQuery: query.Encode(), }
fmt.Println(reqUrl.String())
res := types.GetTokenResponse{} _, _, err := gorequest.New(). Get(reqUrl.String()). Retry(3, time.Second, http.StatusBadRequest, http.StatusInternalServerError). EndStruct(&res)
if err != nil { return res.Data.AccessToken, errors.New(fmt.Sprintf("%v", err)) } else if res.Error != 0 { return res.Data.AccessToken, errors.New(fmt.Sprintf("%v", res.Msg)) } else { return res.Data.AccessToken, nil } }
|
hduHelpServiceTypes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package types
type HduhelpBaseResponse struct { Cache bool `json:"cache"` Error int `json:"error"` Msg string `json:"msg"` }
type GetStudentInfoResponse struct { HduhelpBaseResponse Data struct { ClassId string `json:"classId"` MajorId string `json:"majorId"` MajorName string `json:"majorName"` StaffId string `json:"staffId"` StaffName string `json:"staffName"` TeacherId string `json:"teacherId"` TeacherName string `json:"teacherName"` UnitId string `json:"unitId"` UnitName string `json:"unitName"` } `json:"data"` }
type GetTokenResponse struct { HduhelpBaseResponse Data struct { AccessToken string `json:"access_token,omitempty"` AccessTokenExpire int64 `json:"access_token_expire,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` RefreshTokenExpire int64 `json:"refresh_token_expire,omitempty"` StaffId string `json:"staff_id,omitempty"` } `json:"data"` }
|
最后就是 logincallbacklogic.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| func (l *LoginCallbackLogic) newJwtToken(userType uint8, studentId string, studentName string) (string, error) { claims := make(jwt.MapClaims) claims["studentId"] = studentId claims["studentName"] = studentName claims["userType"] = userType
claims["exp"] = time.Now().Add(time.Second * time.Duration(l.svcCtx.Config.Auth.AccessExpire)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(l.svcCtx.Config.Auth.AccessSecret)) }
func (l *LoginCallbackLogic) LoginCallback(req *types.LoginCallbackRequest) (string, error) { var err error
if ok, err := svc.CheckState(req.State); err != nil || !ok { return "", errors.New("state error") } fmt.Println("state ok")
var token string if token, err = l.GetToken(req.Code, req.State); err != nil { return "", err } fmt.Println("get token ok")
var studentInfo types.GetStudentInfoResponse if studentInfo, err = l.GetStudentInfo(token); err != nil { return "", err } fmt.Println("get student info ok")
var userType int32 if userType, err = svc.GetUserType(studentInfo); err != nil { return "", err } fmt.Println("get user type ok")
var jwtToken string if jwtToken, err = l.newJwtToken(uint8(userType), studentInfo.Data.StaffId, studentInfo.Data.StaffName); err != nil { return "", err } fmt.Println("new jwt token ok")
var redirectUrl = l.svcCtx.Config.Routes.Frontend + "/#/login"
return redirectUrl + "?token=" + jwtToken, nil }
|
成果演示