login with sso,captcha,sms-code (#374)
* add logout v2 for sso * support sms-code login * use db instead of memory cache for login code * feature: support reset password by sms code * remove deprecated api/code * feature: support image captcha * use db instead of memory cache for sso.auth.state
This commit is contained in:
parent
7999c1fbe5
commit
9bef8ddee3
|
@ -24,6 +24,8 @@ sso:
|
||||||
coverAttributes: false
|
coverAttributes: false
|
||||||
stateExpiresIn: 300
|
stateExpiresIn: 300
|
||||||
|
|
||||||
|
captcha: true
|
||||||
|
|
||||||
tokens:
|
tokens:
|
||||||
- rdb-builtin-token
|
- rdb-builtin-token
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -24,6 +24,7 @@ require (
|
||||||
github.com/influxdata/influxdb v1.8.0
|
github.com/influxdata/influxdb v1.8.0
|
||||||
github.com/mattn/go-isatty v0.0.12
|
github.com/mattn/go-isatty v0.0.12
|
||||||
github.com/mattn/go-sqlite3 v1.14.0 // indirect
|
github.com/mattn/go-sqlite3 v1.14.0 // indirect
|
||||||
|
github.com/mojocn/base64Captcha v1.3.1
|
||||||
github.com/onsi/ginkgo v1.7.0 // indirect
|
github.com/onsi/ginkgo v1.7.0 // indirect
|
||||||
github.com/onsi/gomega v1.4.3 // indirect
|
github.com/onsi/gomega v1.4.3 // indirect
|
||||||
github.com/open-falcon/rrdlite v0.0.0-20200214140804-bf5829f786ad
|
github.com/open-falcon/rrdlite v0.0.0-20200214140804-bf5829f786ad
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -138,6 +138,7 @@ github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
@ -282,6 +283,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0=
|
||||||
|
github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
|
||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||||
|
@ -427,6 +430,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
|
|
@ -282,8 +282,7 @@ CREATE TABLE `operation_log`
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY (`clock`),
|
KEY (`clock`),
|
||||||
KEY (`res_cl`, `res_id`)
|
KEY (`res_cl`, `res_id`)
|
||||||
) ENGINE = InnoDB
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
|
||||||
DEFAULT CHARSET = utf8;
|
|
||||||
|
|
||||||
CREATE TABLE `login_code`
|
CREATE TABLE `login_code`
|
||||||
(
|
(
|
||||||
|
@ -297,5 +296,19 @@ CREATE TABLE `login_code`
|
||||||
) ENGINE = InnoDB
|
) ENGINE = InnoDB
|
||||||
DEFAULT CHARSET = utf8;
|
DEFAULT CHARSET = utf8;
|
||||||
|
|
||||||
|
CREATE TABLE `auth_state` (
|
||||||
|
`state` varchar(128) DEFAULT '' NOT NULL,
|
||||||
|
`typ` varchar(32) DEFAULT '' NOT NULL COMMENT 'response_type',
|
||||||
|
`redirect` varchar(1024) DEFAULT '' NOT NULL,
|
||||||
|
`expires_at` bigint DEFAULT '0' NOT NULL,
|
||||||
|
PRIMARY KEY (`state`)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `captcha` (
|
||||||
|
`captcha_id` varchar(128) NOT NULL,
|
||||||
|
`answer` varchar(128) DEFAULT '' NOT NULL,
|
||||||
|
`created_at` bigint DEFAULT '0' NOT NULL,
|
||||||
|
KEY (`captcha_id`, `answer`),
|
||||||
|
KEY (`created_at`)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthState struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
Typ string `json:"typ"`
|
||||||
|
Redirect string `json:"redirect"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthStateGet(where string, args ...interface{}) (*AuthState, error) {
|
||||||
|
var obj AuthState
|
||||||
|
has, err := DB["rdb"].Where(where, args...).Get(&obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has {
|
||||||
|
return nil, errors.New("auth state not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AuthState) Save() error {
|
||||||
|
_, err := DB["rdb"].Insert(p)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AuthState) Del() error {
|
||||||
|
_, err := DB["rdb"].Where("state=?", p.State).Delete(new(AuthState))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p AuthState) CleanUp() error {
|
||||||
|
_, err := DB["rdb"].Exec("delete from auth_state where expires_at < ?", time.Now().Unix())
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Captcha struct {
|
||||||
|
CaptchaId string `json:"captchaId"`
|
||||||
|
Answer string `json:"-"`
|
||||||
|
Image string `xorm:"-" json:"image"`
|
||||||
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CaptchaGet(where string, args ...interface{}) (*Captcha, error) {
|
||||||
|
var obj Captcha
|
||||||
|
has, err := DB["rdb"].Where(where, args...).Get(&obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has {
|
||||||
|
return nil, errors.New("captcha not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Captcha) Save() error {
|
||||||
|
_, err := DB["rdb"].Insert(p)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Captcha) Del() error {
|
||||||
|
_, err := DB["rdb"].Where("captcha_id=?", p.CaptchaId).Delete(new(Captcha))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const captchaExpiresIn = 600
|
||||||
|
|
||||||
|
func (p Captcha) CleanUp() error {
|
||||||
|
_, err := DB["rdb"].Exec("delete from captcha where created_at < ?", time.Now().Unix()-captchaExpiresIn)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ type ConfigT struct {
|
||||||
Sender map[string]senderSection `yaml:"sender"`
|
Sender map[string]senderSection `yaml:"sender"`
|
||||||
RabbitMQ rabbitmqSection `yaml:"rabbitmq"`
|
RabbitMQ rabbitmqSection `yaml:"rabbitmq"`
|
||||||
WeChat wechatSection `yaml:"wechat"`
|
WeChat wechatSection `yaml:"wechat"`
|
||||||
|
Captcha bool `yaml:"captcha"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type wechatSection struct {
|
type wechatSection struct {
|
||||||
|
@ -33,7 +34,7 @@ type ssoSection struct {
|
||||||
ClientId string `yaml:"clientId"`
|
ClientId string `yaml:"clientId"`
|
||||||
ClientSecret string `yaml:"clientSecret"`
|
ClientSecret string `yaml:"clientSecret"`
|
||||||
ApiKey string `yaml:"apiKey"`
|
ApiKey string `yaml:"apiKey"`
|
||||||
StateExpiresIn int `yaml:"stateExpiresIn"`
|
StateExpiresIn int64 `yaml:"stateExpiresIn"`
|
||||||
CoverAttributes bool `yaml:"coverAttributes"`
|
CoverAttributes bool `yaml:"coverAttributes"`
|
||||||
Attributes struct {
|
Attributes struct {
|
||||||
Dispname string `yaml:"dispname"`
|
Dispname string `yaml:"dispname"`
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/didi/nightingale/src/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cleanerInterval = 3600 * time.Second
|
||||||
|
|
||||||
|
func CleanerLoop() {
|
||||||
|
tc := time.Tick(cleanerInterval)
|
||||||
|
|
||||||
|
for {
|
||||||
|
models.AuthState{}.CleanUp()
|
||||||
|
models.Captcha{}.CleanUp()
|
||||||
|
<-tc
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,16 +18,13 @@ func Config(r *gin.Engine) {
|
||||||
notLogin.GET("/roles/local", localRoleGet)
|
notLogin.GET("/roles/local", localRoleGet)
|
||||||
notLogin.POST("/users/invite", userInvitePost)
|
notLogin.POST("/users/invite", userInvitePost)
|
||||||
|
|
||||||
notLogin.GET("/auth/authorize", authAuthorize)
|
|
||||||
notLogin.GET("/auth/callback", authCallback)
|
|
||||||
notLogin.GET("/auth/settings", authSettings)
|
|
||||||
|
|
||||||
notLogin.GET("/auth/v2/authorize", authAuthorizeV2)
|
notLogin.GET("/auth/v2/authorize", authAuthorizeV2)
|
||||||
notLogin.GET("/auth/v2/callback", authCallbackV2)
|
notLogin.GET("/auth/v2/callback", authCallbackV2)
|
||||||
notLogin.GET("/auth/v2/logout", logoutV2)
|
notLogin.GET("/auth/v2/logout", logoutV2)
|
||||||
|
|
||||||
notLogin.POST("/auth/send-rst-code-by-sms", sendRstCodeBySms)
|
notLogin.POST("/auth/send-rst-code-by-sms", sendRstCodeBySms)
|
||||||
notLogin.POST("/auth/rst-password", rstPassword)
|
notLogin.POST("/auth/rst-password", rstPassword)
|
||||||
|
notLogin.GET("/auth/captcha", captchaGet)
|
||||||
|
|
||||||
notLogin.GET("/v2/nodes", nodeGets)
|
notLogin.GET("/v2/nodes", nodeGets)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/mojocn/base64Captcha"
|
||||||
"github.com/toolkits/pkg/file"
|
"github.com/toolkits/pkg/file"
|
||||||
"github.com/toolkits/pkg/str"
|
"github.com/toolkits/pkg/str"
|
||||||
|
|
||||||
|
@ -24,6 +26,17 @@ import (
|
||||||
var (
|
var (
|
||||||
loginCodeSmsTpl *template.Template
|
loginCodeSmsTpl *template.Template
|
||||||
loginCodeEmailTpl *template.Template
|
loginCodeEmailTpl *template.Template
|
||||||
|
errUnsupportCaptcha = errors.New("unsupported captcha")
|
||||||
|
|
||||||
|
// https://captcha.mojotv.cn
|
||||||
|
captchaDirver = base64Captcha.DriverString{
|
||||||
|
Height: 30,
|
||||||
|
Width: 120,
|
||||||
|
ShowLineOptions: 0,
|
||||||
|
Length: 4,
|
||||||
|
Source: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||||
|
//ShowLineOptions: 14,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -102,23 +115,6 @@ func logout(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func authAuthorize(c *gin.Context) {
|
|
||||||
username := cookieUsername(c)
|
|
||||||
if username != "" { // alread login
|
|
||||||
c.String(200, "hi, "+username)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect := queryStr(c, "redirect", "/")
|
|
||||||
|
|
||||||
if config.Config.SSO.Enable {
|
|
||||||
c.Redirect(302, ssoc.Authorize(redirect))
|
|
||||||
} else {
|
|
||||||
c.String(200, "sso does not enable")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type authRedirect struct {
|
type authRedirect struct {
|
||||||
Redirect string `json:"redirect"`
|
Redirect string `json:"redirect"`
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
|
@ -134,29 +130,13 @@ func authAuthorizeV2(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if config.Config.SSO.Enable {
|
if config.Config.SSO.Enable {
|
||||||
ret.Redirect = ssoc.Authorize(redirect)
|
ret.Redirect, err = ssoc.Authorize(redirect)
|
||||||
} else {
|
} else {
|
||||||
ret.Redirect = "/login"
|
ret.Redirect = "/login"
|
||||||
}
|
}
|
||||||
renderData(c, ret, nil)
|
renderData(c, ret, err)
|
||||||
}
|
|
||||||
|
|
||||||
func authCallback(c *gin.Context) {
|
|
||||||
code := queryStr(c, "code", "")
|
|
||||||
state := queryStr(c, "state", "")
|
|
||||||
if code == "" {
|
|
||||||
if redirect := queryStr(c, "redirect"); redirect != "" {
|
|
||||||
c.Redirect(302, redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect, user, err := ssoc.Callback(code, state)
|
|
||||||
dangerous(err)
|
|
||||||
|
|
||||||
writeCookieUser(c, user.UUID)
|
|
||||||
c.Redirect(302, redirect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authCallbackV2(c *gin.Context) {
|
func authCallbackV2(c *gin.Context) {
|
||||||
|
@ -182,14 +162,6 @@ func authCallbackV2(c *gin.Context) {
|
||||||
renderData(c, ret, nil)
|
renderData(c, ret, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authSettings(c *gin.Context) {
|
|
||||||
renderData(c, struct {
|
|
||||||
Sso bool `json:"sso"`
|
|
||||||
}{
|
|
||||||
Sso: config.Config.SSO.Enable,
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logoutV2(c *gin.Context) {
|
func logoutV2(c *gin.Context) {
|
||||||
redirect := queryStr(c, "redirect", "")
|
redirect := queryStr(c, "redirect", "")
|
||||||
ret := &authRedirect{Redirect: redirect}
|
ret := &authRedirect{Redirect: redirect}
|
||||||
|
@ -516,3 +488,33 @@ func rstPassword(c *gin.Context) {
|
||||||
renderData(c, "reset successfully", nil)
|
renderData(c, "reset successfully", nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func captchaGet(c *gin.Context) {
|
||||||
|
ret, err := func() (*models.Captcha, error) {
|
||||||
|
if !config.Config.Captcha {
|
||||||
|
return nil, errUnsupportCaptcha
|
||||||
|
}
|
||||||
|
|
||||||
|
driver := captchaDirver.ConvertFonts()
|
||||||
|
id, content, answer := driver.GenerateIdQuestionAnswer()
|
||||||
|
item, err := driver.DrawCaptcha(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := &models.Captcha{
|
||||||
|
CaptchaId: id,
|
||||||
|
Answer: answer,
|
||||||
|
Image: item.EncodeB64string(),
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ret.Save(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
renderData(c, ret, err)
|
||||||
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@ func main() {
|
||||||
go cron.ConsumeSms()
|
go cron.ConsumeSms()
|
||||||
go cron.ConsumeVoice()
|
go cron.ConsumeVoice()
|
||||||
go cron.ConsumeIm()
|
go cron.ConsumeIm()
|
||||||
|
go cron.CleanerLoop()
|
||||||
|
|
||||||
http.Start()
|
http.Start()
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ssoc
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -16,15 +17,18 @@ import (
|
||||||
"github.com/didi/nightingale/src/modules/rdb/config"
|
"github.com/didi/nightingale/src/modules/rdb/config"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"k8s.io/apimachinery/pkg/util/cache"
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errState = errors.New("您的登录信息已过期,请前往首页重新登录..")
|
||||||
|
errUser = errors.New("用户信息异常")
|
||||||
)
|
)
|
||||||
|
|
||||||
type ssoClient struct {
|
type ssoClient struct {
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
config oauth2.Config
|
config oauth2.Config
|
||||||
apiKey string
|
apiKey string
|
||||||
cache *cache.LRUExpireCache
|
stateExpiresIn int64
|
||||||
stateExpiresIn time.Duration
|
|
||||||
ssoAddr string
|
ssoAddr string
|
||||||
callbackAddr string
|
callbackAddr string
|
||||||
coverAttributes bool
|
coverAttributes bool
|
||||||
|
@ -48,7 +52,6 @@ func InitSSO() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cache = cache.NewLRUExpireCache(1000)
|
|
||||||
cli.ssoAddr = cf.SsoAddr
|
cli.ssoAddr = cf.SsoAddr
|
||||||
cli.callbackAddr = cf.RedirectURL
|
cli.callbackAddr = cf.RedirectURL
|
||||||
cli.coverAttributes = cf.CoverAttributes
|
cli.coverAttributes = cf.CoverAttributes
|
||||||
|
@ -75,19 +78,26 @@ func InitSSO() {
|
||||||
}
|
}
|
||||||
cli.apiKey = cf.ApiKey
|
cli.apiKey = cf.ApiKey
|
||||||
|
|
||||||
if cf.StateExpiresIn == 0 {
|
if cli.stateExpiresIn = cf.StateExpiresIn; cli.stateExpiresIn == 0 {
|
||||||
cli.stateExpiresIn = time.Second * 60
|
cli.stateExpiresIn = 60
|
||||||
} else {
|
|
||||||
cli.stateExpiresIn = time.Second * time.Duration(cf.StateExpiresIn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorize return the sso authorize location with state
|
// Authorize return the sso authorize location with state
|
||||||
func Authorize(redirect string) string {
|
func Authorize(redirect string) (string, error) {
|
||||||
state := uuid.New().String()
|
state := &models.AuthState{
|
||||||
cli.cache.Add(state, redirect, cli.stateExpiresIn)
|
State: uuid.New().String(),
|
||||||
|
Typ: "OAuth2.CODE",
|
||||||
|
Redirect: redirect,
|
||||||
|
ExpiresAt: time.Now().Unix() + cli.stateExpiresIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := state.Save(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// log.Printf("add state %s", state)
|
// log.Printf("add state %s", state)
|
||||||
return cli.config.AuthCodeURL(state)
|
return cli.config.AuthCodeURL(state.State), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutLocation return logout location
|
// LogoutLocation return logout location
|
||||||
|
@ -100,25 +110,23 @@ func LogoutLocation(redirect string) string {
|
||||||
|
|
||||||
// Callback 用 code 兑换 accessToken 以及 用户信息,
|
// Callback 用 code 兑换 accessToken 以及 用户信息,
|
||||||
func Callback(code, state string) (string, *models.User, error) {
|
func Callback(code, state string) (string, *models.User, error) {
|
||||||
s, ok := cli.cache.Get(state)
|
s, err := models.AuthStateGet("state=?", state)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("invalid state %s", state)
|
return "", nil, errState
|
||||||
}
|
}
|
||||||
cli.cache.Remove(state)
|
|
||||||
// log.Printf("remove state %s", state)
|
|
||||||
|
|
||||||
redirect := s.(string)
|
s.Del()
|
||||||
// log.Printf("callback, get state %s redirect %s", state, redirect)
|
// log.Printf("remove state %s", state)
|
||||||
|
|
||||||
u, err := exchangeUser(code)
|
u, err := exchangeUser(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, errUser
|
||||||
}
|
}
|
||||||
// log.Printf("exchange user %v", u)
|
// log.Printf("exchange user %v", u)
|
||||||
|
|
||||||
user, err := models.UserGet("username=?", u.Username)
|
user, err := models.UserGet("username=?", u.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, errUser
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
|
@ -132,7 +140,7 @@ func Callback(code, state string) (string, *models.User, error) {
|
||||||
err = user.Update("email", "dispname", "phone", "im")
|
err = user.Update("email", "dispname", "phone", "im")
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect, user, err
|
return s.Redirect, user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func exchangeUser(code string) (*models.User, error) {
|
func exchangeUser(code string) (*models.User, error) {
|
||||||
|
|
Loading…
Reference in New Issue