You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

480 lines
16 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package ControllerOauth2
import (
"dsSso/Const"
"dsSso/Const/DefaultConst"
"dsSso/Controller/ControllerRecaptcha"
"dsSso/Dao/DaoBaseGlobal"
"dsSso/Model"
"dsSso/Service/ServiceJoinApp"
"dsSso/Service/ServiceLoginPerson"
"dsSso/Utils/AesUtil"
"dsSso/Utils/CommonUtil"
"dsSso/Utils/ConfigUtil"
"dsSso/Utils/ConvertUtil"
"dsSso/Utils/RedisStorage"
"dsSso/Utils/RedisUtil"
"dsSso/Utils/RsaUtil"
"dsSso/Utils/SsoUtil"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/dchest/captcha"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
)
//模块的路由配置
func Routers(r *gin.RouterGroup) {
rr := r.Group("")
//列表
rr.GET("/authorize", authorizeGet)
//注册POST接口用于验证
r.POST("/authorize", authorizePost)
//用于授权码换取access_token
r.POST("/access_token", accessToken)
//获取验证码
r.GET("/getCaptcha", getCaptcha)
//获取验证码图片
r.GET("/getCaptchaPng", getCaptchaPng)
//退出接口
r.GET("/logout", logout)
return
}
// @Summary 获取验证码
// @Description 获取验证码(为了以后WEB服务器的集群扩展将验证码保存到redis中避免ip_hash)
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Success 200 {object} Model.Res
// @Router /oauth2/getCaptcha [get]
func getCaptcha(c *gin.Context) {
//自定义redis为存储器
captcha.SetCustomStore(&ControllerRecaptcha.RedisStoreBean)
d := struct {
CaptchaId string
}{
captcha.NewLen(4),
}
if d.CaptchaId != "" {
c.JSON(http.StatusOK, Model.Res{
Code: http.StatusOK,
Msg: "获取成功",
Data: d.CaptchaId,
})
} else {
c.JSON(http.StatusOK, Model.Res{
Code: http.StatusInternalServerError,
Msg: "获取失败",
Items: nil,
})
}
}
// @Summary 获取验证码图片
// @Description 获取验证码图片
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param captchaId query string true "captchaId"
// @Success 200 {string} string
// @Router /oauth2/getCaptchaPng [get]
// @X-LengthLimit [{"captchaId":"20,20"}]
func getCaptchaPng(c *gin.Context) {
ControllerRecaptcha.ServeHTTP(c.Writer, c.Request)
}
/**
功能:产生授权码
作者:黄海
时间2020-03-13
*/
func generateAuthCode(context *gin.Context, identityId string, personId string, deviceId string, typeId string) {
//1、生成加密串
r, _ := AesUtil.Encrypt([]byte(identityId + "_" + personId + "_" + deviceId + "_" + ConvertUtil.Int64ToString(CommonUtil.GetCurrentTimestamp())))
//2、转base64
encodeString := base64.RawURLEncoding.EncodeToString([]byte(r))
//3、写cookie
context.SetCookie(ConfigUtil.AccessToken, encodeString, 0, "/", "", false, true)
//如果是统一认证管理员登录
if identityId == "0" && personId == "0" {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusOK,
Items: "/sso/static/ssoSystemList.html",
})
return
} else {
//如果不是第三方系统跳转过来而且还不是SSO管理员的情况下。
if context.PostForm("client_id") == "" && context.Query("client_id") == "" {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusOK,
Msg: "不是第三方系统跳转过来而且还不是SSO管理员",
Items: "/sso/static/Error.html",
})
return
}
//否则的话,返回授权码
req := context.Request
resp := RedisStorage.OsinServer.NewResponse()
defer resp.Close()
if ar := RedisStorage.OsinServer.HandleAuthorizeRequest(resp, req); ar != nil {
//标识验证通过
ar.Authorized = true
ar.Expiration = 3600 * 2 //过期时间两小时
RedisStorage.OsinServer.FinishAuthorizeRequest(resp, req, ar)
//用redis记录一下code--->personinfo的关系(黄海)
personStr := "{\"identity_id\":\"" + identityId + "\",\"person_id\":\"" + personId + "\",\"device_id\":\"" + deviceId + "\"}"
key := ConfigUtil.OAuth2RedisKeyPrefix + ":code_vs_person:" + resp.Output["code"].(string)
RedisUtil.SET(key, personStr, 10*60*time.Second)
}
//返回数据
url := resp.URL
code := resp.Output["code"].(string)
state := resp.Output["state"].(string)
//第三方的回调页面地址
oauthCallback := context.PostForm("oauth_callback")
if oauthCallback == "" {
oauthCallback = context.Query("oauth_callback")
}
//POST方式返回json数据
url = url + "?code=" + code + "&state=" + state + "&oauth_callback=" + oauthCallback
if typeId == "POST" {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusOK,
Items: url,
})
return
} else { //GET方式跳转回客户端的回调页
context.Redirect(302, url)
return
}
}
}
// @Summary 获取access_token
// @Description 获取access_token
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param code query string false "code"
// @Param refresh_token query string false "refresh_token"
// @Success 200 {string} string
// @Router /oauth2/access_token [post]
func accessToken(context *gin.Context) {
req := context.Request
resp := RedisStorage.OsinServer.NewResponse()
defer resp.Close()
var identityId string
var personId string
var deviceId string
//授权码
code := context.PostForm("code")
if code != "" {
//从redis中获取回来
personInfo, err := RedisUtil.GET(ConfigUtil.OAuth2RedisKeyPrefix + ":code_vs_person:" + code)
if err != nil {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "输入的授权码不正确或已过期!",
})
return
}
//还原结构体
m := make(map[string]interface{})
json.Unmarshal([]byte(personInfo), &m)
if err != nil {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "还原人员信息结构体失败!",
})
return
}
identityId, _ = m["identity_id"].(string)
personId, _ = m["person_id"].(string)
deviceId, _ = m["device_id"].(string)
if ar := RedisStorage.OsinServer.HandleAccessRequest(resp, req); ar != nil {
ar.Authorized = true
ar.Expiration = 3600 * 2
RedisStorage.OsinServer.FinishAccessRequest(resp, req, ar)
}
//自己动手,丰衣足食
accessToken := resp.Output["access_token"].(string)
expiresIn := resp.Output["expires_in"].(int32)
refreshToken := resp.Output["refresh_token"].(string)
tokenType := resp.Output["token_type"].(string)
identityIdInt, _ := strconv.Atoi(identityId)
//personIdInt, _ := strconv.Atoi(personId)
//记录refresh_token与identity_id,person_id关系
personStr := "{\"identity_id\":\"" + identityId + "\",\"person_id\":\"" + personId + "\",\"device_id\":\"" + deviceId + "\"}"
k := ConfigUtil.OAuth2RedisKeyPrefix + ":refresh_token_vs_person:" + refreshToken
RedisUtil.SET(k, personStr, time.Hour*24*30) //30天的有效期
//返回json数据
context.JSON(http.StatusOK,
gin.H{
"access_token": accessToken, "expires_in": expiresIn, "refresh_token": refreshToken,
"token_type": tokenType, "identity_id": identityIdInt, "person_id": personId})
return
} else {
//刷新token
clientRefreshToken := context.PostForm("refresh_token")
if clientRefreshToken != "" {
key := ConfigUtil.OAuth2RedisKeyPrefix + ":refresh_token_vs_person:" + clientRefreshToken
personInfo, err := RedisUtil.GET(key)
if err != nil {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "输入的刷新token不正确",
})
return
}
//还原结构体
m := make(map[string]interface{})
json.Unmarshal([]byte(personInfo), &m)
if err != nil {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "还原人员信息结构体失败!",
})
return
}
identityId, _ = m["identity_id"].(string)
personId, _ = m["person_id"].(string)
if ar := RedisStorage.OsinServer.HandleAccessRequest(resp, req); ar != nil {
ar.Authorized = true
RedisStorage.OsinServer.FinishAccessRequest(resp, req, ar)
}
accessToken := resp.Output["access_token"].(string)
expiresIn := resp.Output["expires_in"].(int32)
refreshToken := resp.Output["refresh_token"].(string)
tokenType := resp.Output["token_type"].(string)
identityIdInt, _ := strconv.Atoi(identityId)
personIdInt, _ := strconv.Atoi(personId)
//删除旧的refresh_token
RedisUtil.DEL(key)
//返回json数据
context.JSON(http.StatusOK,
gin.H{
"access_token": accessToken, "expires_in": expiresIn, "refresh_token": refreshToken,
"token_type": tokenType, "identity_id": identityIdInt, "person_id": personIdInt})
return
}
}
}
// @Summary 验证post
// @Description 验证post
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param captchaId formData string true "验证码id"
// @Param value formData string true "验证码值"
// @Param username formData string true "用户名"
// @Param password formData string true "密码"
// @Param device_id formData string true "设备ID"
// @Param redirect_uri formData string false "第三方系统的接口回调地址"
// @Param client_id formData string false "第三方系统的ID"
// #Param oauth_callback formData string false "第三方系统的回调页面地址"
// @Success 200 {object} Model.Res
// @Router /oauth2/authorize [post]
// @X-IntLimit ["device_id"]
// @X-EmptyLimit ["username","password","captchaId","value"]
func authorizePost(context *gin.Context) {
var identityId string
var personId string
var success bool
var remainCount int
//1、验证码
captchaId := context.PostForm("captchaId")
value := context.PostForm("value")
//2、用户名和密码
username := context.PostForm("username")
encryptPwd := context.PostForm("password")
//3、设备号
deviceId := context.PostForm("device_id")
//4、校验函数根据redis的读取结果进行检查
var redisStore ControllerRecaptcha.RedisStore
if redisStore.VerifyString(captchaId, value) {
//对前端AES加密过的密码进行解密
b, err := base64.StdEncoding.DecodeString(encryptPwd)
if err != nil {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "密码不是系统允许的base64方式",
})
return
}
decryptPwd, err := RsaUtil.RsaDecrypt(b)
if err != nil {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "密码不是系统允许的加密方式!",
})
return
}
//调用service层的用户名和密码校验办法判断是不是允许登录
ip := context.ClientIP()
success, identityId, personId, _, remainCount = ServiceLoginPerson.Login(username, string(decryptPwd), ip)
if !success {
//三次以上不提示
if remainCount >= 3 {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "用户名密码不正确或已禁用!",
})
} else if remainCount > 0 {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "用户名密码不正确或已禁用!您还可以尝试" + CommonUtil.ConvertIntToString(remainCount) + "次超出次数限制后账号将被禁用2小时",
})
} else {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "账号已处于禁用状态,请稍后再试!",
})
}
return
}
} else {
context.JSON(http.StatusOK, Model.Res{
Code: http.StatusNotImplemented,
Msg: "验证码不正确!",
})
return
}
//验证通过的话,返回授权码
generateAuthCode(context, identityId, personId, deviceId, "POST")
}
// @Summary 登录验证跳转路由
// @Description 登录验证跳转路由
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param client_id query string true "client_id"
// @Param redirect_uri query string true "redirect_uri"
// @Param device_id query string true "device_id"
// @Param oauth_callback query string true "第三方系统回跳的页面地址"
// @Success 200 {string} string
// @Router /oauth2/authorize [get]
// @X-EmptyLimit ["client_id","redirect_uri","device_id","oauth_callback"]
func authorizeGet(context *gin.Context) {
//客户端的client_id
var paraClientId = context.Query("client_id")
//回调接口地址
var redirectUri = context.Query("redirect_uri")
//回调的页面地址
var oauthCallback = context.Query("oauth_callback")
//检查这个client_id是不是经过授权
client, err := RedisStorage.OAuth2RedisStorage.GetClient(paraClientId)
if err != nil {
msg := "传入的client_id不在系统的允许范围内"
context.JSON(http.StatusOK, map[string]interface{}{"success": false, "msg": msg})
return
}
var sysUrl = client.GetRedirectUri()
if strings.Index(sysUrl, redirectUri) < 0 {
msg := "回调地址与系统中保存的不一致!"
context.JSON(http.StatusOK, map[string]interface{}{"success": false, "msg": msg})
return
}
var deviceId = context.Query("device_id")
//注册验证是否登录的接口这个GET接口是直接跳转走的
//此处判断人员是否存在本站下的session,如果有的话
var identityId = DefaultConst.IdentityId
var personId = DefaultConst.PersonId
SsoSessionId := SsoUtil.ReadSsoCookie(context)
if SsoSessionId != "" {
//取出UserData
identityId, personId, _ = SsoUtil.AnalyzeSessionId(SsoSessionId)
if identityId != "" && personId != "" {
//为其生成授权码,并返回给客户端系统
generateAuthCode(context, identityId, personId, deviceId, "GET")
return
}
}
//如果没有本domain下的cookie,那么跳转到登录页
url := "/sso/static/index.html?redirect_uri=" + redirectUri + "&response_type=code&client_id=" + paraClientId + "&oauth_callback=" + oauthCallback
context.Redirect(302, url)
}
// @Summary 退出统一认证
// @Description 退出统一认证
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Param redirect_uri query string true "redirect_uri"
// @Produce json
// @Success 200 {string} string
// @Router /oauth2/logout [get]
func logout(context *gin.Context) {
//取出现在的cookie中的accessToken
accessToken := SsoUtil.ReadSsoCookie(context)
//获取所有接入系统的
list, _ := ServiceJoinApp.GetJoinAppList(1, Const.Int32Max, "")
for i := range list {
//每个系统的退出地址
logoutUri := list[i]["logout_uri"].(string)
//如果注册了退出地址的话
if len(logoutUri) > 0 {
//开启协程进行调用
go func() {
resp, err := http.Get(logoutUri + "?access_token=" + accessToken)
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}()
}
}
//清除cookie
SsoUtil.DeleteSsoCookie(context)
//判断来源是内网还是外网是指客户端的IP地址即浏览器的IP
ipStart := strings.Split(context.Request.Host, ".")[0]
isIntranetIP := strings.Index("10,192,172", ipStart)
//外网
var s string
if isIntranetIP < 0 {
s = "defaultIndexWwUrl"
} else { //内网
s = "defaultIndexNwUrl"
}
//跳转回去,系统默认统一的回调首页
list, err := DaoBaseGlobal.GetGlobalInfoByValue(s)
if err != nil || len(list) == 0 {
msg := "获取全局变量defaultIndexUrl失败"
context.JSON(http.StatusOK, map[string]interface{}{"success": false, "msg": msg})
return
}
globalValue := list[0]["global_value"].(string)
context.Redirect(302, globalValue)
return
}
// @Summary 重置错误重试次数限制
// @Description 重置错误重试次数限制
// @Tags 登录验证类
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param userName query string true "登录用的用户名"
// @Success 200 {string} string
// @Router /oauth2/resetRemainCount [get]
// @X-EmptyLimit ["userName"]
func resetRemainCount(context *gin.Context) {
userName := context.Query("userName")
RedisUtil.DEL(Const.RemainCountRedisPrefix + userName)
msg := "成功清除禁用标识!"
context.JSON(http.StatusOK, map[string]interface{}{"success": true, "msg": msg})
return
}