企业微信登录初步调通

pull/790/head
gsw945 2022-05-07 18:06:46 +08:00
parent e0fb5a67f7
commit 0671b0cd40
11 changed files with 1167 additions and 81 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@ _cgo_export.*
_testmain.go
*.exe
*.exe~
mindoc
database
*.test

View File

@ -56,6 +56,8 @@ go build -ldflags "-w"
./mindoc install
# 执行
./mindoc
# 开发阶段运行
bee run
```
MinDoc 如果使用MySQL储存数据则编码必须是`utf8mb4_general_ci`。请在安装前,把数据库配置填充到项目目录下的 `conf/app.conf` 中。

View File

@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"time"
_ "time/tzdata"
"bytes"
"encoding/json"
@ -110,6 +111,7 @@ func RegisterModel() {
new(models.TeamMember),
new(models.TeamRelationship),
new(models.Itemsets),
new(models.WorkWeixinAccount),
)
gob.Register(models.Blog{})
gob.Register(models.Document{})

View File

@ -231,7 +231,19 @@ dingtalk_qr_key="${MINDOC_DINGTALK_QRKEY}"
# 钉钉扫码登录Secret
dingtalk_qr_secret="${MINDOC_DINGTALK_QRSECRET}"
########企业微信登录配置##############
# 企业ID
workweixin_corpid="${MINDOC_WORKWEIXIN_CORPID}"
# 应用ID
workweixin_agentid="${MINDOC_WORKWEIXIN_AGENTID}"
# 应用密钥
workweixin_secret="${MINDOC_WORKWEIXIN_SECRET}"
# 通讯录密钥
workweixin_contact_secret="${MINDOC_WORKWEIXIN_CONTACT_SECRET}"
# i18n config
default_lang="zh-cn"

27
conf/workweixin.go 100644
View File

@ -0,0 +1,27 @@
package conf
import (
"github.com/beego/beego/v2/server/web"
)
type WorkWeixinConf struct {
CorpId string // 企业ID
AgentId string // 应用ID
Secret string // 应用密钥
ContactSecret string // 通讯录密钥
}
func GetWorkWeixinConfig() *WorkWeixinConf {
corpid, _ := web.AppConfig.String("workweixin_corpid")
agentid, _ := web.AppConfig.String("workweixin_agentid")
secret, _ := web.AppConfig.String("workweixin_secret")
contact_secret, _ := web.AppConfig.String("workweixin_contact_secret")
c := &WorkWeixinConf{
CorpId: corpid,
AgentId: agentid,
Secret: secret,
ContactSecret: contact_secret,
}
return c
}

View File

@ -1,24 +1,38 @@
package controllers
import (
"github.com/beego/i18n"
"encoding/json"
"fmt"
"html/template"
"math/rand"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"html/template"
"github.com/beego/beego/v2/client/orm"
"github.com/beego/beego/v2/core/logs"
"github.com/beego/beego/v2/server/web"
"github.com/beego/i18n"
"github.com/lifei6671/gocaptcha"
"github.com/mindoc-org/mindoc/conf"
"github.com/mindoc-org/mindoc/mail"
"github.com/mindoc-org/mindoc/models"
"github.com/mindoc-org/mindoc/utils"
"github.com/mindoc-org/mindoc/utils/dingtalk"
"github.com/mindoc-org/mindoc/utils/workweixin"
)
const (
WorkWeixin_AuthorizeUrlBase = "https://open.weixin.qq.com/connect/oauth2/authorize"
WorkWeixin_QRConnectUrlBase = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect"
SessionUserInfoKey = "session-user-info-key"
)
var src = rand.New(rand.NewSource(time.Now().UnixNano()))
// AccountController 用户登录与注册
type AccountController struct {
BaseController
@ -32,13 +46,24 @@ func (c *AccountController) referer() string {
return u
}
func (c *AccountController) IsInWorkWeixin() (is_in_workweixin bool) {
ua := c.Ctx.Input.UserAgent()
var wechatRule = regexp.MustCompile(`\bMicroMessenger\/\d+(\.\d+)*\b`)
var wxworkRule = regexp.MustCompile(`\bwxwork\/\d+(\.\d+)*\b`)
return wechatRule.MatchString(ua) && wxworkRule.MatchString(ua)
}
func (c *AccountController) Prepare() {
c.BaseController.Prepare()
c.EnableXSRF = web.AppConfig.DefaultBool("enablexsrf", true)
c.Data["xsrfdata"] = template.HTML(c.XSRFFormHTML())
c.Data["CanLoginWorkWeixin"] = len(web.AppConfig.DefaultString("workweixin_corpid", "")) > 0
c.Data["corpID"], _ = web.AppConfig.String("dingtalk_corpid")
if dtcorpid, _ := web.AppConfig.String("dingtalk_corpid"); dtcorpid != "" {
c.Data["CanLoginDingTalk"] = len(web.AppConfig.DefaultString("dingtalk_corpid", "")) > 0
if reflect.ValueOf(c.Data["CanLoginDingTalk"]).Bool() {
c.Data["ENABLE_QR_DINGTALK"] = true
}
c.Data["dingtalk_qr_key"], _ = web.AppConfig.String("dingtalk_qr_key")
@ -141,7 +166,40 @@ func (c *AccountController) Login() {
c.JsonResult(500, i18n.Tr(c.Lang, "message.wrong_account_password"), nil)
}
} else {
c.Data["url"] = c.referer()
// 默认登录方式
login_method := "AccountController.Login"
var redirect_uri string
// 企业微信登录检查
canLoginWorkWeixin := reflect.ValueOf(c.Data["CanLoginWorkWeixin"]).Bool()
referer := c.referer()
if canLoginWorkWeixin {
// 企业微信登录方式
login_method = "AccountController.WorkWeixinLogin"
u := c.GetString("url")
if u == "" {
u = referer
if u == "" {
u = conf.BaseUrl
}
} else {
var schemaRule = regexp.MustCompile(`^https?\:\/\/`)
if !schemaRule.MatchString(u) {
u = conf.BaseUrl + u
}
}
redirect_uri = conf.URLFor(login_method, "url", url.PathEscape(u))
// 是否在企业微信内部打开
isInWorkWeixin := c.IsInWorkWeixin()
c.Data["IsInWorkWeixin"] = isInWorkWeixin
if isInWorkWeixin {
// 客户端拥有微信标识和企业微信标识
c.Redirect(redirect_uri, 302)
return
} else {
c.Data["workweixin_login_url"] = redirect_uri
}
}
c.Data["url"] = referer
}
}
@ -199,6 +257,417 @@ func (c *AccountController) DingTalkLogin() {
c.JsonResult(0, "ok", username)
}
// WorkWeixinLogin 用户企业微信登录
func (c *AccountController) WorkWeixinLogin() {
c.Prepare()
logs.Info("UserAgent: ", c.Ctx.Input.UserAgent()) // debug
if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 {
u := c.GetString("url")
if u == "" {
u = c.Ctx.Request.Header.Get("Referer")
if u == "" {
u = conf.URLFor("HomeController.Index")
}
}
// session自动登录时刷新session内容
member, err := models.NewMember().Find(member.MemberId)
if err != nil {
c.DelSession(conf.LoginSessionName)
c.SetMember(models.Member{})
c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600)
} else {
c.SetMember(*member)
}
c.Redirect(u, 302)
}
var remember CookieRemember
// 如果 Cookie 中存在登录信息
if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok {
if err := utils.Decode(cookie, &remember); err == nil {
if member, err := models.NewMember().Find(remember.MemberId); err == nil {
c.SetMember(*member)
c.LoggedIn(false)
c.StopRun()
}
}
}
if c.Ctx.Input.IsPost() {
// account := c.GetString("account")
// password := c.GetString("password")
// captcha := c.GetString("code")
// isRemember := c.GetString("is_remember")
c.JsonResult(400, "request method not allowed", nil)
} else {
var callback_u string
u := c.GetString("url")
if u == "" {
u = c.referer()
}
if u != "" {
var schemaRule = regexp.MustCompile(`^https?\:\/\/`)
if !schemaRule.MatchString(u) {
u = strings.TrimRight(conf.BaseUrl, "/") + strings.TrimLeft(u, "/")
}
}
if u == "" {
callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback")
} else {
callback_u = conf.URLFor("AccountController.WorkWeixinLoginCallback", "url", url.PathEscape(u))
}
logs.Info("callback_u: ", callback_u) // debug
state := "mindoc"
workweixinConf := conf.GetWorkWeixinConfig()
appid := workweixinConf.CorpId
agentid := workweixinConf.AgentId
var redirect_uri string
isInWorkWeixin := c.IsInWorkWeixin()
c.Data["IsInWorkWeixin"] = isInWorkWeixin
if isInWorkWeixin {
// 企业微信内-网页授权登录
urlFmt := "%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect"
redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_AuthorizeUrlBase, appid, url.PathEscape(callback_u), state)
} else {
// 浏览器内-扫码授权登录
urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&state=%s"
redirect_uri = fmt.Sprintf(urlFmt, WorkWeixin_QRConnectUrlBase, appid, agentid, url.PathEscape(callback_u), state)
}
logs.Info("redirect_uri: ", redirect_uri) // debug
c.Redirect(redirect_uri, 302)
}
}
/*
:
1.
+
->->,
[]
2. ->->
(+)
: [++]
: UserId()
*/
// WorkWeixinLoginCallback 用户企业微信登录-回调
func (c *AccountController) WorkWeixinLoginCallback() {
c.TplName = "account/workweixin-login-callback.tpl"
if member, ok := c.GetSession(conf.LoginSessionName).(models.Member); ok && member.MemberId > 0 {
u := c.GetString("url")
if u == "" {
u = c.Ctx.Request.Header.Get("Referer")
}
if u == "" {
u = conf.URLFor("HomeController.Index")
}
member, err := models.NewMember().Find(member.MemberId)
if err != nil {
c.DelSession(conf.LoginSessionName)
c.SetMember(models.Member{})
c.SetSecureCookie(conf.GetAppKey(), "login", "", -3600)
} else {
c.SetMember(*member)
}
c.Redirect(u, 302)
}
var remember CookieRemember
// 如果 Cookie 中存在登录信息
if cookie, ok := c.GetSecureCookie(conf.GetAppKey(), "login"); ok {
if err := utils.Decode(cookie, &remember); err == nil {
if member, err := models.NewMember().Find(remember.MemberId); err == nil {
c.SetMember(*member)
c.LoggedIn(false)
c.StopRun()
}
}
}
// 请求参数获取
req_code := c.GetString("code")
logs.Warning("req_code: ", req_code)
req_state := c.GetString("state")
logs.Warning("req_state: ", req_state)
var user_info_json string
var error_msg string
var bind_existed string
if len(req_code) > 0 && req_state == "mindoc" {
// 获取当前应用的access_token
access_token, ok := workweixin.GetAccessToken(false)
if ok {
logs.Warning("access_token: ", access_token)
// 获取当前请求的userid
user_id, ok := workweixin.RequestUserId(access_token, req_code)
if ok {
logs.Warning("user_id: ", user_id)
// 获取通讯录应用的access_token
contact_access_token, ok := workweixin.GetAccessToken(true)
if ok {
logs.Warning("contact_access_token: ", contact_access_token)
user_info, err_msg, ok := workweixin.RequestUserInfo(contact_access_token, user_id)
if ok {
// [-------所有字段-Debug----------
// user_info.UserId
// user_info.Name
// user_info.HideMobile
// user_info.Mobile
// user_info.Department
// user_info.Email
// user_info.IsLeaderInDept
// user_info.IsLeader
// user_info.Avatar
// user_info.Alias
// user_info.Status
// user_info.MainDepartment
// -----------------------------]
// logs.Debug("user_info.UserId: ", user_info.UserId)
// logs.Debug("user_info.Name: ", user_info.Name)
json_info, _ := json.Marshal(user_info)
user_info_json = string(json_info)
// 查询系统现有数据,是否绑定了当前请求用户的企业微信
member, err := models.NewWorkWeixinAccount().ExistedMember(user_info.UserId)
if err == nil {
member.LastLoginTime = time.Now()
_ = member.Update("last_login_time")
c.SetMember(*member)
var remember CookieRemember
remember.MemberId = member.MemberId
remember.Account = member.Account
remember.Time = time.Now()
v, err := utils.Encode(remember)
if err == nil {
c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix())
}
bind_existed = "true"
error_msg = ""
u := c.GetString("url")
if u == "" {
u = conf.URLFor("HomeController.Index")
}
c.Redirect(u, 302)
} else {
if err == orm.ErrNoRows {
c.SetSession(SessionUserInfoKey, user_info)
bind_existed = "false"
error_msg = ""
} else {
logs.Error("Error: ", err)
error_msg = "数据库错误: " + err.Error()
}
}
//
} else {
error_msg = "获取用户信息失败: " + err_msg
}
} else {
error_msg = "通讯录访问凭据获取失败: " + contact_access_token
}
} else {
error_msg = "获取用户Id失败: " + user_id
}
} else {
error_msg = "应用凭据获取失败: " + access_token
}
} else {
error_msg = "参数错误"
}
if user_info_json == "" {
user_info_json = "{}"
}
if bind_existed == "" {
bind_existed = "null"
}
// refer & doc:
// - https://golang.org/pkg/html/template/#HTML
// - https://stackoverflow.com/questions/24411880/go-html-templates-can-i-stop-the-templates-package-inserting-quotes-around-stri
// - https://stackoverflow.com/questions/38035176/insert-javascript-snippet-inside-template-with-beego-golang
c.Data["bind_existed"] = template.JS(bind_existed)
logs.Debug("bind_existed: ", bind_existed)
c.Data["error_msg"] = template.JS(error_msg)
c.Data["user_info_json"] = template.JS(user_info_json)
/*
// 调试: 显示源码
result, err := c.RenderString()
if err != nil {
logs.Error(err)
} else {
logs.Warning(result)
}
*/
}
// WorkWeixinLoginBind 用户企业微信登录-绑定
func (c *AccountController) WorkWeixinLoginBind() {
c.Prepare()
if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserInfo); ok && len(user_info.UserId) > 0 {
req_account := c.GetString("account")
req_password := c.GetString("password")
if req_account == "" || req_password == "" {
c.JsonResult(400, "账号或密码不能为空")
} else {
member, err := models.NewMember().Login(req_account, req_password)
if err == nil {
account := models.NewWorkWeixinAccount()
account.MemberId = member.MemberId
account.WorkWeixin_UserId = user_info.UserId
member.CreateAt = 0
ormer := orm.NewOrm()
o, err := ormer.Begin()
if err != nil {
logs.Error("开启事物时出错 -> ", err)
c.JsonResult(500, "开启事物时出错: ", err.Error())
}
if err := account.AddBind(ormer); err != nil {
o.Rollback()
c.JsonResult(500, "绑定失败,数据库错误: "+err.Error())
} else {
member.LastLoginTime = time.Now()
member.RealName = user_info.Name
member.Avatar = user_info.Avatar
if len(member.Avatar) < 1 {
member.Avatar = conf.GetDefaultAvatar()
}
member.Email = user_info.Email
member.Phone = user_info.Mobile
if _, err := ormer.Update(member, "last_login_time", "real_name", "avatar", "email", "phone"); err != nil {
o.Rollback()
logs.Error("保存用户信息失败=>", err)
c.JsonResult(500, "绑定失败,现有账户信息更新失败: "+err.Error())
} else {
if err := o.Commit(); err != nil {
logs.Error("提交事物时出错 -> ", err)
c.JsonResult(500, "提交事物时出错: ", err.Error())
} else {
c.DelSession(SessionUserInfoKey)
c.SetMember(*member)
var remember CookieRemember
remember.MemberId = member.MemberId
remember.Account = member.Account
remember.Time = time.Now()
v, err := utils.Encode(remember)
if err == nil {
c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix())
c.JsonResult(0, "绑定成功", nil)
} else {
c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil)
}
}
}
}
} else {
logs.Error("用户登录 ->", err)
c.JsonResult(500, "账号或密码错误", nil)
}
c.JsonResult(500, "TODO: 绑定以后账号功能开发中")
}
} else {
if ok {
c.DelSession(SessionUserInfoKey)
}
c.JsonResult(400, "请求错误, 请从首页重新登录")
}
}
// WorkWeixinLoginIgnore 用户企业微信登录-忽略
func (c *AccountController) WorkWeixinLoginIgnore() {
if user_info, ok := c.GetSession(SessionUserInfoKey).(workweixin.WorkWeixinUserInfo); ok && len(user_info.UserId) > 0 {
c.DelSession(SessionUserInfoKey)
member := models.NewMember()
if _, err := member.FindByAccount(user_info.UserId); err == nil && member.MemberId > 0 {
c.JsonResult(400, "账号已存在")
}
ormer := orm.NewOrm()
o, err := ormer.Begin()
if err != nil {
logs.Error("开启事物时出错 -> ", err)
c.JsonResult(500, "开启事物时出错: ", err.Error())
}
member.Account = user_info.UserId
member.RealName = user_info.Name
var rnd = rand.New(src)
// fmt.Sprintf("%x", rnd.Uint64())
// strconv.FormatUint(rnd.Uint64(), 16)
member.Password = user_info.UserId + strconv.FormatUint(rnd.Uint64(), 16)
member.Password = "pathea.2020" // 强制设置默认密码,不然无法修改密码(因为目前修改密码需要知道当前密码)
hash, err := utils.PasswordHash(member.Password)
if err != nil {
logs.Error("加密用户密码失败 =>", err)
c.JsonResult(500, "加密用户密码失败"+err.Error())
} else {
logs.Error("member.Password: ", member.Password)
logs.Error("hash: ", hash)
member.Password = hash
}
member.Role = conf.MemberGeneralRole
member.Avatar = user_info.Avatar
if len(member.Avatar) < 1 {
member.Avatar = conf.GetDefaultAvatar()
}
member.CreateAt = 0
member.Email = user_info.Email
member.Phone = user_info.Mobile
member.Status = 0
if _, err = ormer.Insert(member); err != nil {
o.Rollback()
c.JsonResult(500, "注册失败,数据库错误: "+err.Error())
} else {
account := models.NewWorkWeixinAccount()
account.MemberId = member.MemberId
account.WorkWeixin_UserId = user_info.UserId
member.CreateAt = 0
if err := account.AddBind(ormer); err != nil {
o.Rollback()
c.JsonResult(500, "注册失败,数据库错误: "+err.Error())
} else {
if err := o.Commit(); err != nil {
logs.Error("提交事物时出错 -> ", err)
c.JsonResult(500, "提交事物时出错: ", err.Error())
} else {
member.LastLoginTime = time.Now()
_ = member.Update("last_login_time")
c.SetMember(*member)
var remember CookieRemember
remember.MemberId = member.MemberId
remember.Account = member.Account
remember.Time = time.Now()
v, err := utils.Encode(remember)
if err == nil {
c.SetSecureCookie(conf.GetAppKey(), "login", v, time.Now().Add(time.Hour*24*30*5).Unix())
c.JsonResult(0, "绑定成功", nil)
} else {
c.JsonResult(500, "绑定成功, 但自动登录失败, 请返回首页重新登录", nil)
}
}
}
}
} else {
if ok {
c.DelSession(SessionUserInfoKey)
}
c.JsonResult(400, "请求错误, 请从首页重新登录")
}
}
// QR二维码登录
func (c *AccountController) QRLogin() {
c.Prepare()
@ -266,6 +735,10 @@ func (c *AccountController) QRLogin() {
}
c.Redirect(conf.URLFor("AccountController.Login"), 302)
// 企业微信扫码登录
case "workweixin":
//
default:
c.Redirect(conf.URLFor("AccountController.Login"), 302)
c.StopRun()

View File

@ -0,0 +1,74 @@
// Package models .
package models
import (
"errors"
"time"
"github.com/beego/beego/v2/client/orm"
"github.com/beego/beego/v2/core/logs"
"github.com/mindoc-org/mindoc/conf"
)
type WorkWeixinAccount struct {
MemberId int `orm:"column(member_id);type(int);default(-1);index" json:"member_id"`
UserDbId int `orm:"pk;auto;unique;column(user_db_id)" json:"user_db_id"`
WorkWeixin_UserId string `orm:"size(100);unique;column(workweixin_user_id)" json:"workweixin_user_id"`
// WorkWeixin_Name string `orm:"size(255);column(workweixin_name)" json:"workweixin_name"`
// WorkWeixin_Phone string `orm:"size(25);column(workweixin_phone)" json:"workweixin_phone"`
// WorkWeixin_Email string `orm:"size(255);column(workweixin_email)" json:"workweixin_email"`
// WorkWeixin_Status int `orm:"type(int);column(status)" json:"status"`
// WorkWeixin_Avatar string `orm:"size(1024);column(avatar)" json:"avatar"`
CreateTime time.Time `orm:"type(datetime);column(create_time);auto_now_add" json:"create_time"`
CreateAt int `orm:"type(int);column(create_at)" json:"create_at"`
LastLoginTime time.Time `orm:"type(datetime);column(last_login_time);null" json:"last_login_time"`
}
// TableName 获取对应数据库表名.
func (m *WorkWeixinAccount) TableName() string {
return "workweixin_accounts"
}
// TableEngine 获取数据使用的引擎.
func (m *WorkWeixinAccount) TableEngine() string {
return "INNODB"
}
func (m *WorkWeixinAccount) TableNameWithPrefix() string {
return conf.GetDatabasePrefix() + m.TableName()
}
func NewWorkWeixinAccount() *WorkWeixinAccount {
return &WorkWeixinAccount{}
}
func (a *WorkWeixinAccount) ExistedMember(workweixin_user_id string) (*Member, error) {
o := orm.NewOrm()
account := NewWorkWeixinAccount()
member := NewMember()
err := o.QueryTable(a.TableNameWithPrefix()).Filter("workweixin_user_id", workweixin_user_id).One(account)
if err == nil {
if member, err = member.Find(account.MemberId); err == nil {
return member, nil
} else {
return member, err
}
} else {
return member, err
}
}
// Add 添加一个用户.
func (a *WorkWeixinAccount) AddBind(o orm.Ormer) error {
if c, err := o.QueryTable(a.TableNameWithPrefix()).Filter("member_id", a.MemberId).Count(); err == nil && c > 0 {
return errors.New("已绑定,不可重复绑定")
}
_, err := o.Insert(a)
if err != nil {
logs.Error("保存用户数据到数据时失败 =>", err)
return errors.New("用户信息绑定失败, 数据库错误")
}
return nil
}

View File

@ -1,8 +1,8 @@
package routers
import (
"crypto/tls"
"log"
// "crypto/tls"
// "log"
"net/http"
"net/http/httputil"
"net/url"
@ -11,65 +11,107 @@ import (
"github.com/beego/beego/v2/core/logs"
"github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
"github.com/mindoc-org/mindoc/conf"
// "github.com/mindoc-org/mindoc/conf"
"github.com/mindoc-org/mindoc/controllers"
)
func rt(req *http.Request) (*http.Response, error) {
log.Printf("request received. url=%s", req.URL)
// req.Header.Set("Host", "httpbin.org") // <--- I set it here as well
defer log.Printf("request complete. url=%s", req.URL)
return http.DefaultTransport.RoundTrip(req)
}
// roundTripper makes func signature a http.RoundTripper
type roundTripper func(*http.Request) (*http.Response, error)
func (f roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
type CorsTransport struct {
http.RoundTripper
}
func (t *CorsTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
// refer:
// - https://stackoverflow.com/questions/31535569/golang-how-to-read-response-body-of-reverseproxy/31536962#31536962
// - https://gist.github.com/simon-cj/b4da0b2bca793ec3b8a5abe04c8fca41
// refer: https://stackoverflow.com/questions/31535569/golang-how-to-read-response-body-of-reverseproxy/31536962#31536962
resp, err = t.RoundTripper.RoundTrip(req)
logs.Debug(resp)
// beego.Debug(resp)
if err != nil {
return nil, err
}
resp.Header.Del("Access-Control-Request-Method")
/*
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = resp.Body.Close()
if err != nil {
return nil, err
}
b = bytes.Replace(b, []byte("server"), []byte("schmerver"), -1)
body := ioutil.NopCloser(bytes.NewReader(b))
resp.Body = body
resp.ContentLength = int64(len(b))
resp.Header.Set("Content-Length", strconv.Itoa(len(b)))
*/
// resp.Body.Close()
// resp.Header.Del("Access-Control-Request-Method")
// resp.Header.Del("Access-Control-Request-Headers")
resp.Header.Set("Access-Control-Allow-Origin", "*")
resp.Header.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
// resp.Header.Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Requested-With")
hs := ""
for name, values := range resp.Header {
hs = hs + name + ", "
_ = values
}
hs = strings.TrimRight(hs, " ")
hs = strings.TrimRight(hs, ",")
// beego.Debug(hs)
resp.Header.Set("Access-Control-Allow-Headers", hs)
resp.Header.Del("Mindoc-Version")
resp.Header.Del("Mindoc-Site")
resp.Header.Del("Server")
resp.Header.Del("X-Xss-Protection")
return resp, nil
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
func init() {
web.Any("/hello-any", func(ctx *context.Context) {
ctx.Output.Body([]byte("hello any demo"))
})
web.Any("/cors-anywhere", func(ctx *context.Context) {
u, _ := url.PathUnescape(ctx.Input.Query("url"))
logs.Error("ReverseProxy: ", u)
if len(u) > 0 && strings.HasPrefix(u, "http") {
if strings.TrimRight(conf.BaseUrl, "/") == ctx.Input.Site() {
ctx.Redirect(302, u)
target, _ := url.Parse(u)
if target.Path == ctx.Request.URL.Path {
ctx.Output.Body([]byte(""))
} else {
target, _ := url.Parse(u)
logs.Debug("target: ", target)
logs.Error("target: ", target)
proxy := &httputil.ReverseProxy{
Transport: roundTripper(rt),
Director: func(req *http.Request) {
req.Header = ctx.Request.Header
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path
req.Header.Set("Host", target.Host)
},
reverseProxy := httputil.NewSingleHostReverseProxy(target)
reverseProxy.Director = func(req *http.Request) {
for name, values := range ctx.Request.Header {
for _, value := range values {
req.Header.Set(name, value)
}
}
req.Header.Add("X-Forwarded-Host", req.Host)
req.Header.Add("X-Origin-Host", target.Host)
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
// proxyPath := singleJoiningSlash(target.Path, req.URL.Path)
proxyPath := target.Path
if strings.HasSuffix(proxyPath, "/") && len(proxyPath) > 1 {
proxyPath = proxyPath[:len(proxyPath)-1]
}
req.URL.Path = proxyPath
}
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
proxy.ServeHTTP(ctx.ResponseWriter, ctx.Request)
reverseProxy.Transport = &CorsTransport{http.DefaultTransport}
reverseProxy.ServeHTTP(ctx.ResponseWriter, ctx.Request)
panic(web.ErrAbort)
}
} else {
ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
@ -81,6 +123,10 @@ func init() {
web.Router("/login", &controllers.AccountController{}, "*:Login")
web.Router("/dingtalk_login", &controllers.AccountController{}, "*:DingTalkLogin")
web.Router("/workweixin-login", &controllers.AccountController{}, "*:WorkWeixinLogin")
web.Router("/workweixin-callback", &controllers.AccountController{}, "*:WorkWeixinLoginCallback")
web.Router("/workweixin-bind", &controllers.AccountController{}, "*:WorkWeixinLoginBind")
web.Router("/workweixin-ignore", &controllers.AccountController{}, "*:WorkWeixinLoginIgnore")
web.Router("/qrlogin/:app", &controllers.AccountController{}, "*:QRLogin")
web.Router("/logout", &controllers.AccountController{}, "*:Logout")
web.Router("/register", &controllers.AccountController{}, "*:Register")

View File

@ -0,0 +1,222 @@
package workweixin
import (
"context"
"crypto/tls"
// "encoding/json"
"net/http"
"time"
"github.com/beego/beego/v2/client/httplib"
"github.com/beego/beego/v2/core/logs"
"github.com/mindoc-org/mindoc/cache"
"github.com/mindoc-org/mindoc/conf"
)
// doc
// - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313
const (
AccessTokenCacheKey = "access-token-cache-key"
ContactAccessTokenCacheKey = "contact-access-token-cache-key"
)
// 获取访问凭据-请求响应结构
type AccessTokenResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
AccessToken string `json:"access_token"` // 获取到的凭证,最长为512字节
ExpiresIn int `json:"expires_in"` // 凭证的有效时间(秒)
}
// 获取用户Id-请求响应结构
type UserIdResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
UserId string `json:"UserId"` // 企业成员UserID
OpenId string `json:"OpenId"` // 非企业成员的标识,对当前企业唯一
DeviceId string `json:"DeviceId"` // 设备号
}
// 获取用户信息-请求响应结构
type UserInfoResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
UserId string `json:"UserId"` // 企业成员UserID
Name string `json:"name"` // 成员名称
HideMobile int `json:"hide_mobile"` // 是否隐藏了手机号码
Mobile string `json:"mobile"` // 手机号码
Department []int `json:"department"` // 成员所属部门id列表
Email string `json:"email"` // 邮箱
IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
IsLeader int `json:"isleader"` // 是否是部门上级(领导)
Avatar string `json:"avatar"` // 头像url
Alias string `json:"alias"` // 别名
Status int `json:"status"` // 激活状态: 1=已激活2=已禁用4=未激活5=退出企业
MainDepartment int `json:"main_department"` // 主部门
}
// 访问凭据缓存-结构
type AccessTokenCache struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
UpdateTime time.Time `json:"update_time"`
}
// 企业微信用户信息-结构
type WorkWeixinUserInfo struct {
UserId string `json:"UserId"` // 企业成员UserID
Name string `json:"name"` // 成员名称
HideMobile int `json:"hide_mobile"` // 是否隐藏了手机号码
Mobile string `json:"mobile"` // 手机号码
Department []int `json:"department"` // 成员所属部门id列表
Email string `json:"email"` // 邮箱
IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
IsLeader int `json:"isleader"` // 是否是部门上级(领导)
Avatar string `json:"avatar"` // 头像url
Alias string `json:"alias"` // 别名
Status int `json:"status"` // 激活状态: 1=已激活2=已禁用4=未激活5=退出企业
MainDepartment int `json:"main_department"` // 主部门
}
func httpFilter(next httplib.Filter) httplib.Filter {
return func(ctx context.Context, req *httplib.BeegoHTTPRequest) (*http.Response, error) {
r := req.GetRequest()
logs.Info("filter-url: ", r.URL)
// Never forget invoke this. Or the request will not be sent
return next(ctx, req)
}
}
// 获取访问凭据-请求
func RequestAccessToken(corpid string, secret string) (cache_token AccessTokenCache, ok bool) {
url := "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
req := httplib.Get(url)
req.Param("corpid", corpid) // 企业ID
req.Param("corpsecret", secret) // 应用的凭证密钥
req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
req.AddFilters(httpFilter)
resp, err := req.Response()
_ = resp
var token AccessTokenCache
if err != nil {
logs.Error(err)
return token, false
}
var atr AccessTokenResponse
err = req.ToJSON(&atr)
if err != nil {
logs.Error(err)
return token, false
}
token = AccessTokenCache{
AccessToken: atr.AccessToken,
ExpiresIn: atr.ExpiresIn,
UpdateTime: time.Now(),
}
return token, true
}
// 获取访问凭据
func GetAccessToken(is_contact bool) (access_token string, ok bool) {
var cache_token AccessTokenCache
cache_key := AccessTokenCacheKey
if is_contact {
cache_key = ContactAccessTokenCacheKey
}
err := cache.Get(cache_key, &cache_token)
if err == nil {
logs.Info("AccessToken从缓存读取成功")
// TODO: access_token有效期判断, 刷新
return cache_token.AccessToken, true
} else {
logs.Warning(err)
workweixinConfig := conf.GetWorkWeixinConfig()
logs.Debug("corp_id: ", workweixinConfig.CorpId)
logs.Debug("agent_id: ", workweixinConfig.AgentId)
logs.Debug("secret: ", workweixinConfig.Secret)
logs.Debug("contact_secret: ", workweixinConfig.ContactSecret)
secret := workweixinConfig.Secret
if is_contact {
secret = workweixinConfig.ContactSecret
}
new_token, ok := RequestAccessToken(workweixinConfig.CorpId, secret)
if ok {
logs.Debug(new_token)
if err = cache.Put(cache_key, new_token, time.Second*time.Duration(new_token.ExpiresIn)); err == nil {
logs.Info("AccessToken缓存写入成功")
return new_token.AccessToken, true
}
logs.Warning("AccessToken缓存写入失败")
return "", false
}
logs.Warning("AccessToken请求失败")
return "", false
}
}
// 获取用户id-请求
func RequestUserId(access_token string, code string) (user_id string, ok bool) {
url := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"
req := httplib.Get(url)
req.Param("access_token", access_token) // 应用调用接口凭证
req.Param("code", code) // 通过成员授权获取到的code
req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
req.AddFilters(httpFilter)
resp, err := req.Response()
_ = resp
if err != nil {
logs.Error(err)
return "", false
}
var uir UserIdResponse
err = req.ToJSON(&uir)
if err != nil {
logs.Error(err)
return "", false
}
return uir.UserId, true
}
// 获取用户详细信息-请求
func RequestUserInfo(contact_access_token string, userid string) (user_info WorkWeixinUserInfo, error_msg string, ok bool) {
url := "https://qyapi.weixin.qq.com/cgi-bin/user/get"
req := httplib.Get(url)
req.Param("access_token", contact_access_token) // 通讯录应用调用接口凭证
req.Param("userid", userid) // 成员UserID
req.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: false})
req.AddFilters(httpFilter)
resp_str, err := req.String()
_ = resp_str
var info WorkWeixinUserInfo
if err != nil {
logs.Error(err)
return info, "请求失败", false
} else {
logs.Debug(resp_str)
}
var uir UserInfoResponse
err = req.ToJSON(&uir)
if err != nil {
logs.Error(err)
return info, "请求数据结果错误", false
}
if uir.ErrCode != 0 {
return info, uir.ErrMsg, false
}
info = WorkWeixinUserInfo{
UserId: uir.UserId,
Name: uir.Name,
HideMobile: uir.HideMobile,
Mobile: uir.Mobile,
Department: uir.Department,
Email: uir.Email,
IsLeaderInDept: uir.IsLeaderInDept,
IsLeader: uir.IsLeader,
Avatar: uir.Avatar,
Alias: uir.Alias,
Status: uir.Status,
MainDepartment: uir.MainDepartment,
}
return info, "", true
}

View File

@ -14,6 +14,23 @@
<link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
<link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
<link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
{{if .CanLoginWorkWeixin}}
<style type="text/css">
#wxwork-login-line > a {
display: block;
text-align: center;
border: 1px solid #ccc;
border-radius: 0.3em;
padding-top: 0.8em;
padding-bottom: 0.75em;
}
#wxwork-login-line > a:hover {
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c;
}
</style>
{{end}}
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
</head>
@ -82,6 +99,13 @@
</div>
{{end}}
{{end}}
{{if .CanLoginWorkWeixin}}
<div class="form-group">
<div id="wxwork-login-line">
<a href="{{ .workweixin_login_url }}" title="手机企业微信-扫码登录">手机企业微信-扫码登录</a>
</div>
</div>
{{end}}
</form>
<div class="form-group dingtalk-container" style="display: none;">
<div id="dingtalk-qr-container"></div>
@ -98,6 +122,7 @@
<script src="{{cdnjs "/static/js/dingtalk-jsapi.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/dingtalk-ddlogin.js"}}" type="text/javascript"></script>
{{if .ENABLE_QR_DINGTALK}}
<script type="text/javascript">
if (dd.env.platform !== "notInDingTalk"){
dd.ready(function() {
@ -135,37 +160,41 @@
});
}
</script>
$(document).ready(function () {
var url = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid={{.dingtalk_qr_key}}&response_type=code&scope=snsapi_login&state=1&redirect_uri={{ urlfor "AccountController.QRLogin" ":app" "dingtalk"}}'
var obj = DDLogin({
id:"dingtalk-qr-container",
goto: encodeURIComponent(url),
style: "border:none;background-color:#FFFFFF;",
width : "338",
height: "300"
});
$(window).on('message', function (event) {
var origin = event.origin;
if( origin == "https://login.dingtalk.com" ) { //ddLogin
layer.load(1, { shade: [0.1, '#fff'] })
var loginTmpCode = event.data;
//获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
console.log("loginTmpCode", loginTmpCode);
url = url + "&loginTmpCode=" + loginTmpCode
window.location = url
}
});
$("#btn-dingtalk-qr").on('click', function(){
$('form').hide()
$(".dingtalk-container").show()
})
<script type="text/javascript">
var url = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid={{.dingtalk_qr_key}}&response_type=code&scope=snsapi_login&state=1&redirect_uri={{ urlfor "AccountController.QRLogin" ":app" "dingtalk"}}'
var obj = DDLogin({
id:"dingtalk-qr-container",
goto: encodeURIComponent(url),
style: "border:none;background-color:#FFFFFF;",
width : "338",
height: "300"
$(".btn-dingtalk").on('click', function(){
$('form').show()
$(".dingtalk-container").hide()
})
});
var handleMessage = function (event) {
var origin = event.origin;
if( origin == "https://login.dingtalk.com" ) { //ddLogin
layer.load(1, { shade: [0.1, '#fff'] })
var loginTmpCode = event.data;
//获取到loginTmpCode后就可以在这里构造跳转链接进行跳转了
console.log("loginTmpCode", loginTmpCode);
url = url + "&loginTmpCode=" + loginTmpCode
window.location = url
}
};
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', handleMessage, false);
} else if (typeof window.attachEvent != 'undefined') {
window.attachEvent('onmessage', handleMessage);
}
</script>
{{end}}
<script type="text/javascript">
$(function () {
$(document).ready(function () {
$("#account,#password,#code").on('focus', function () {
$(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
});
@ -177,16 +206,6 @@
}
});
$("#btn-dingtalk-qr").on('click', function(){
$('form').hide()
$(".dingtalk-container").show()
})
$(".btn-dingtalk").on('click', function(){
$('form').show()
$(".dingtalk-container").hide()
})
$("#btn-login").on('click', function () {
$(this).tooltip('destroy').parents('.form-group').removeClass('has-error');
var $btn = $(this).button('loading');

View File

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="{{cdnimg "/favicon.ico"}}">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="MinDoc" />
<title>用户登录 - Powered by MinDoc</title>
<meta name="keywords" content="MinDoc,文档在线管理系统,WIKI,wiki,wiki在线,文档在线管理,接口文档在线管理,接口文档管理">
<meta name="description" content="MinDoc文档在线管理系统 {{.site_description}}">
<!-- Bootstrap -->
<link href="{{cdncss "/static/bootstrap/css/bootstrap.min.css"}}" rel="stylesheet">
<link href="{{cdncss "/static/font-awesome/css/font-awesome.min.css"}}" rel="stylesheet">
<link href="{{cdncss "/static/css/main.css" "version"}}" rel="stylesheet">
<style type="text/css">
.login > .login-body {
text-align: center;
padding-top: 1.5em;
}
.login > .login-body > a > strong:hover {
border-bottom: 1px solid #337ab7;
}
.login > .login-body > a > strong {
font-size: 1.5em;
vertical-align: middle;
padding: 0.5em;
}
.bind-existed-form > .form-group {
margin: auto 1.5em;
margin-top: 1em;
}
</style>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="{{cdnjs "/static/jquery/1.12.4/jquery.min.js"}}"></script>
<script type="text/javascript">
window.bind_existed = {{ .bind_existed }};
window.user_info_json = {{ .user_info_json }};
window.server_error_msg = "{{ .error_msg }}";
window.home_url = "{{ .BaseUrl }}";
window.workweixin_login_bind = "{{urlfor "AccountController.WorkWeixinLoginBind"}}";
window.workweixin_login_ignore = "{{urlfor "AccountController.WorkWeixinLoginIgnore"}}";
</script>
</head>
<body class="manual-container">
<header class="navbar navbar-static-top smart-nav navbar-fixed-top manual-header" role="banner">
<div class="container">
<div class="navbar-header col-sm-12 col-md-6 col-lg-5">
<a href="{{.BaseUrl}}" class="navbar-brand">{{.SITE_NAME}}</a>
</div>
</div>
</header>
<div class="container manual-body">
<div class="row login">
<div class="login-body">
返回 <a href="{{ .BaseUrl }}"><strong>首页</strong></a>
</div>
</div>
<div class="clearfix"></div>
<script type="text/x-template" id="bind-existed-template">
<div role="form" class="bind-existed-form">
{{ .xsrfdata }}
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-user"></i>
</div>
<input type="text" class="form-control" placeholder="邮箱 / 用户名" name="account" id="account" autocomplete="off">
</div>
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-lock"></i>
</div>
<input type="password" class="form-control" placeholder="密码" name="password" id="password" autocomplete="off">
</div>
</div>
</div>
</script>
</div>
{{template "widgets/footer.tpl" .}}
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/layer/layer.js"}}" type="text/javascript"></script>
<script type="text/javascript">
function showBindAccount() {
layer.confirm([
'检测到当前登录企业微信未绑定已有账户, 是否需要绑定已有账户?<br />',
'<ul style="padding-left: 1.2em;">',
'<li>若已有账户, 请 <strong>去绑定</strong></li>',
'<li>若没有现有账户, 请 <strong>忽略绑定</strong></li>',
'</ul>'
].join(''), {
title: "WIKI-绑定提示",
move: false,
area: 'auto',
offset: 'auto',
icon: 3,
btn: ['去绑定','忽略绑定'],
}, function(index, layero){
// layer.close(index);
// layer.msg(window.home_url);
// TODO: 现有账户[用户名+密码]查询现有账户 依据Session[user_info]绑定更新现有账户
console.log("yes");
layer.open({
title: "绑定已有账户",
type: 1,
move: false,
area: 'auto',
offset: 'auto',
content: $('#bind-existed-template').html(),
btn: ['绑定','取消'],
yes: function(index, layero){
$.ajax({
url: window.workweixin_login_bind,
type: 'POST',
beforeSend: function(request) {
request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val());
},
data: {
account: $('#account').val(),
password: $('#password').val()
},
dataType: 'json',
success: function(data) {
if(data.errcode == 0) {
layer.close(index);
// layer.msg(JSON.stringify(data), {icon: 1, time: 15500});
window.location.href = window.home_url;
}
else {
layer.msg(data.message, {icon: 5, time: 3500});
}
},
error: function(data) {
console.log(data);
}
});
return false;
},
cancel: function(index, layero){
// return false; // 不关闭
layer.close(index);
window.location.href = window.home_url;
}
});
}, function(index){
/*
// TODO: 依据Session[user_info]创建新账户
console.log("no");
var msg = '';
// msg = "<pre>" + JSON.stringify(window.location, null, 4) + "</pre>";
msg = "<pre>" + JSON.stringify(window.user_info_json, null, 4) + "</pre>";
// msg = "<pre>" + window.user_info_json + "</pre>";
layer.open({
title: "Degug-UserInfo",
type: 1,
skin: 'layui-layer-rim',
move: false,
area: 'auto',
offset: 'auto',
content: msg
});
*/
$.ajax({
url: window.workweixin_login_ignore,
type: 'GET',
beforeSend: function(request) {
request.setRequestHeader("X-Xsrftoken", $('.bind-existed-form input[name="_xsrf"]').val());
},
data: {},
dataType: 'json',
success: function(data) {
if(data.errcode == 0) {
layer.close(index);
layer.msg(JSON.stringify(data), {icon: 1, time: 15500});
window.location.href = window.home_url;
}
else {
layer.msg(data.message, {icon: 5, time: 3500});
}
},
error: function(data) {
console.log(data);
}
});
return false;
});
}
$(document).ready(function () {
$('#debug-panel').val($('html').html());
if (!!window.server_error_msg && window.server_error_msg.length > 0) {
layer.msg(window.server_error_msg, {icon: 5, time: 3500});
} else {
if (window.bind_existed === false) {
showBindAccount();
} else {
// alert(typeof window.bind_existed);
// alert('_' + window.bind_existed + '_');
window.location.href = window.home_url;
}
}
});
</script>
</body>
</html>