- 根据开源项目 https://github.com/openshift/osin 做了大量改造
- 支持http oauth2 server
- 支持grpc oauth2 server
- 支持多客户端的sso服务
- 支持类似notion,多账户登录同一个网页
- 支持后台踢掉某个终端登录态
- 支持配置终端个数,挤下线
- 支持查看token信息,例如UA,client ip, platform
cd examples/simple && EGO_DEBUG=true && go run main.go
// 访问 http://127.0.0.1:9090/app
type ReqOauthLogin struct {
RedirectUri string `json:"redirect_uri" form:"redirect_uri"` // redirect by backend
ClientId string `json:"client_id" form:"client_id"`
ResponseType string `json:"response_type" form:"response_type"`
State string `json:"state" form:"state"`
Scope string `json:"scope" form:"scope"`
}
type ReqToken struct {
GrantType string `json:"grant_type" form:"grant_type"`
Code string `json:"code" form:"code"`
ClientId string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
}
ego.New().Serve(func() *egin.Component {
oauth2 := server.DefaultContainer().Build(server.WithStorage(examples.NewTestStorage()))
router := egin.DefaultContainer().Build()
router.Any("/authorize", func (c *gin.Context) {
reqView := ReqOauthLogin{}
// 1 解析参数
err := c.Bind(&reqView)
if err != nil {
c.JSON(401, "参数错误")
return
}
// 2 设置oauth2的请求
ar := oauth2.HandleAuthorizeRequest(c.Request.Context(), server.AuthorizeRequestParam{
ClientId: reqView.ClientId,
RedirectUri: reqView.RedirectUri,
Scope: reqView.Scope,
State: reqView.State,
ResponseType: reqView.ResponseType,
})
if ar.IsError() {
c.JSON(401, ar.GetAllOutput())
return
}
// 3 处理登录页面
if !HandleLoginPage(ar, c.Writer, c.Request) {
return
}
// 4 处理授权
err = ar.Build(
server.WithAuthorizeRequestAuthorized(true),
server.WithAuthorizeRequestUserData("{uid:1}"),
)
if err != nil {
c.JSON(401, err.Error())
return
}
// 5 跳转登录
redirectUri, err := ar.GetRedirectUrl()
if err != nil {
c.JSON(401, "获取重定向地址失败")
return
}
c.Redirect(302, redirectUri)
return
})
// Authorize token endpoint
router.POST("/token", func (c *gin.Context) {
var ar *server.AccessRequest
reqView := ReqToken{}
err := c.Bind(&reqView)
if err != nil {
c.JSON(401, "参数错误")
return
}
auth := c.GetHeader("Authorization")
ar = oauth2.HandleAccessRequest(c.Request.Context(), server.ParamAccessRequest{
Method: "POST",
GrantType: reqView.GrantType,
AccessRequestParam: server.AccessRequestParam{
Code: reqView.Code,
ClientAuthParam: server.ClientAuthParam{
Authorization: auth,
},
RedirectUri: reqView.RedirectUri,
},
})
if ar.IsError() {
c.JSON(401, ar.GetAllOutput())
return
}
err = ar.Build(server.WithAccessRequestAuthorized(true))
if err != nil {
c.JSON(401, err.Error())
return
}
c.JSON(200, ar.GetAllOutput())
})
// ClientId, ClientSecret token endpoint
router.GET("/token", func (c *gin.Context) {
var ar *server.AccessRequest
reqView := ReqToken{}
err := c.Bind(&reqView)
if err != nil {
c.JSON(401, "参数错误")
return
}
// 默认没开启GET参数访问,所以这里要注意安全问题,建议不要开启,这里为了演示,其实是给的POST,模拟的GET
ar = oauth2.HandleAccessRequest(c.Request.Context(), server.ParamAccessRequest{
Method: "POST",
GrantType: reqView.GrantType,
AccessRequestParam: server.AccessRequestParam{
Code: reqView.Code,
ClientAuthParam: server.ClientAuthParam{
ClientId: reqView.ClientId,
ClientSecret: reqView.ClientSecret,
},
RedirectUri: reqView.RedirectUri,
},
})
if ar.IsError() {
c.JSON(401, ar.GetAllOutput())
return
}
err = ar.Build(server.WithAccessRequestAuthorized(true))
if err != nil {
c.JSON(401, err.Error())
return
}
c.JSON(200, ar.GetAllOutput())
})
return router
}()).Run()
- 客户端服务端写入state信息,并生成url
- 通过浏览器请求sso,sso返回给浏览器code信息
- code信息回传给客户端的服务端,请求sso服务,获得token
- 将token存入到浏览器的http only的cookie里
- 所有接口都可以通过该token,grpc获取用户信息
- 写入authorize表,生成code码
- 写入authorize过期时间
- 第一次保存token
- save access token
- remove authorization token
- 刷新token
- save access token
- remove previous access token
// 获取sso的用户信息
router.GET("/user", func (c *gin.Context) {
info, err := invoker.TokenStorage.GetAPI().GetAllByUser(c.Request.Context(), cast.ToInt64(c.Query("uid")))
if err != nil {
c.JSON(200, err.Error())
return
}
c.JSON(200, info)
})
// 获取sso的长token信息
router.GET("/parentToken", func (c *gin.Context) {
info, err := invoker.TokenStorage.GetAPI().GetAllByParentToken(c.Request.Context(), c.Query("token"))
if err != nil {
c.JSON(200, err.Error())
return
}
c.JSON(200, info)
})
// 获取sso的短token信息
router.GET("/subToken", func (c *gin.Context) {
info, err := invoker.TokenStorage.GetAPI().GetAllBySubToken(c.Request.Context(), c.Query("token"))
if err != nil {
c.JSON(200, err.Error())
return
}
c.JSON(200, info)
})
{
"ctime": 0,
"clients": {
"j14vJUQAQlCKA8D0TMIG4w": {
"token": "j14vJUQAQlCKA8D0TMIG4w",
"authAt": 1648802442,
"expiresIn": 2592000
}
},
"expireTimeList": [
{
"field": "_c:j14vJUQAQlCKA8D0TMIG4w",
"expireTime": 1651394442
}
],
"ttl": 2591922
}
{
"ctime": 1648802442,
"uids": [
1
],
"clients": {
"ebZJXwzXSQmJmAKOX8-gKQ": {
"token": "ebZJXwzXSQmJmAKOX8-gKQ",
"authAt": 1648802442,
"expiresIn": 86400
}
},
"users": {
"1": {
"ctime": 1648802442,
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36",
"clientIp": "::1",
"platform": "web"
}
},
"expireTimeList": [
{
"field": "_c:ebZJXwzXSQmJmAKOX8-gKQ",
"expireTime": 1648888842
}
],
"ttl": 2591978
}
{
"ctime": 1648802442,
"parentToken": "j14vJUQAQlCKA8D0TMIG4w",
"clientId": "1234",
"tokenInfo": {
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36",
"clientIP": "::1"
},
"accessInfo": {
"clientId": "1234",
"previous": "",
"accessToken": "ebZJXwzXSQmJmAKOX8-gKQ",
"expiresIn": 86400,
"scope": "",
"redirectUri": "http://localhost:29001/oauth/code",
"ctime": 1648802442
},
"ttl": 86361
}
获取parent token信息
hgetall sso:ptk:_xryboGNQU-490RpAOBMYQ
获取sub token信息
hgetall sso:stk:n3lgZ-jbR9OlVAfQxh2Hnw
获取uid信息
hgetall sso:uid:1