mindoc/utils/auth2/wecom/wecom.go

286 lines
8.5 KiB
Go
Raw Normal View History

package wecom
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/mindoc-org/mindoc/utils/auth2"
"net/http"
"net/url"
"time"
)
// doc
// - 全局错误码: https://work.weixin.qq.com/api/doc/90000/90139/90313
const (
AppName = "workwx"
auth2Url = "https://open.weixin.qq.com/connect/oauth2/authorize"
ssoUrl = "https://login.work.weixin.qq.com/wwlogin/sso/login"
callbackState = "mindoc"
)
type BasicResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
func (r *BasicResponse) Error() string {
return fmt.Sprintf("errcode=%d,errmsg=%s", r.ErrCode, r.ErrMsg)
}
func (r *BasicResponse) AsError() error {
if r == nil {
return nil
}
if r.ErrCode != 0 {
return r
}
return nil
}
// 获取用户Id-请求响应结构
type UserIdResponse struct {
// 接口文档: https://developer.work.weixin.qq.com/document/path/91023
*BasicResponse
UserId string `json:"userid"` // 企业成员UserID
UserTicket string `json:"user_ticket"` // 用于获取敏感信息
OpenId string `json:"openid"` // 非企业成员的标识,对当前企业唯一
ExternalUserId string `json:"external_userid"` // 外部联系人ID
}
// 获取用户信息-请求响应结构
type UserInfoResponse struct {
// 接口文档: https://developer.work.weixin.qq.com/document/path/90196
*BasicResponse
UserId string `json:"userid"` // 企业成员UserID
Name string `json:"name"` // 成员名称
Department []int `json:"department"` // 成员所属部门id列表
IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为上级
IsLeader int `json:"isleader"` // 是否是部门上级(领导)
Alias string `json:"alias"` // 别名
Status int `json:"status"` // 激活状态: 1=已激活2=已禁用4=未激活5=退出企业
MainDepartment int `json:"main_department"` // 主部门
}
type UserPrivateInfoResponse struct {
// 文档地址: https://developer.work.weixin.qq.com/document/path/95833
*BasicResponse
UserId string `json:"userid"` // 企业成员userid
Gender string `json:"gender"` // 成员性别
Avatar string `json:"avatar"` // 头像
QrCode string `json:"qr_code"` // 二维码
Mobile string `json:"mobile"` // 手机号
Mail string `json:"mail"` // 邮箱
BizMail string `json:"biz_mail"` // 企业邮箱
Address string `json:"address"` // 地址
}
// 访问凭据缓存-结构
type AccessToken struct {
*BasicResponse
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
createTime time.Time `json:"create_time"`
}
func (a AccessToken) GetToken() string {
return a.AccessToken
}
func (a AccessToken) GetExpireIn() time.Duration {
return time.Duration(a.ExpiresIn) * time.Second
}
func (a AccessToken) GetExpireTime() time.Time {
return a.createTime.Add(a.GetExpireIn())
}
// 企业微信用户敏感信息-结构
type WorkWeixinUserPrivateInfo struct {
UserId string `json:"userid"` // 企业成员userid
Name string `json:"name"` // 姓名
Gender string `json:"gender"` // 成员性别
Avatar string `json:"avatar"` // 头像
QrCode string `json:"qr_code"` // 二维码
Mobile string `json:"mobile"` // 手机号
Mail string `json:"mail"` // 邮箱
BizMail string `json:"biz_mail"` // 企业邮箱
Address string `json:"address"` // 地址
}
// 企业微信用户信息-结构
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 NewClient(corpId, appId, appSecrete string) auth2.Client {
return NewWorkWechatClient(corpId, appId, appSecrete)
}
func NewWorkWechatClient(corpId, appId, appSecrete string) *WorkWechatClient {
return &WorkWechatClient{
CorpId: corpId,
AppId: appId,
AppSecret: appSecrete,
}
}
type WorkWechatClient struct {
CorpId string
AppId string
AppSecret string
token auth2.IAccessToken
}
func (c *WorkWechatClient) GetAccessToken(ctx context.Context) (auth2.IAccessToken, error) {
if c.token != nil {
return c.token, nil
}
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", c.CorpId, c.AppSecret)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
var token AccessToken
if err := auth2.Request(req, &token); err != nil {
return token, err
}
token.createTime = time.Now()
return token, nil
}
func (c *WorkWechatClient) SetAccessToken(token auth2.IAccessToken) {
c.token = token
return
}
func (c *WorkWechatClient) BuildURL(callback string, isAppBrowser bool) string {
var endpoint string
if isAppBrowser {
// 企业微信内-网页授权登录
urlFmt := "%s?appid=%s&agentid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=%s#wechat_redirect"
endpoint = fmt.Sprintf(urlFmt, auth2Url, c.CorpId, c.AppId, url.PathEscape(callback), callbackState)
} else {
// 浏览器内-扫码授权登录
urlFmt := "%s?login_type=CorpApp&appid=%s&agentid=%s&redirect_uri=%s&state=%s"
endpoint = fmt.Sprintf(urlFmt, ssoUrl, c.CorpId, c.AppId, url.PathEscape(callback), callbackState)
}
return endpoint
}
func (c *WorkWechatClient) ValidateCallback(state string) error {
if state != callbackState {
return errors.New("auth2.state.wrong")
}
return nil
}
func (c *WorkWechatClient) getUserId(ctx context.Context, code string) (UserIdResponse, error) {
var userId UserIdResponse
token, err := c.GetAccessToken(ctx)
if err != nil {
return userId, err
}
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s", token.GetToken(), code)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err := auth2.Request(req, &userId); err != nil {
return userId, err
}
if userId.UserId == "" {
return userId, errors.New("auth2.userid.empty")
}
return userId, nil
}
func (c *WorkWechatClient) getUserInfo(ctx context.Context, userid string) (UserInfoResponse, error) {
var userInfo UserInfoResponse
token, err := c.GetAccessToken(ctx)
if err != nil {
return userInfo, err
}
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s", token.GetToken(), userid)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err := auth2.Request(req, &userInfo); err != nil {
return userInfo, err
}
return userInfo, nil
}
func (c *WorkWechatClient) getUserPrivateInfo(ctx context.Context, ticket string) (UserPrivateInfoResponse, error) {
var userInfo UserPrivateInfoResponse
token, err := c.GetAccessToken(ctx)
if err != nil {
return userInfo, err
}
endpoint := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=%s", token.GetToken())
b, _ := json.Marshal(map[string]string{
"user_ticket": ticket,
})
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(b))
if err := auth2.Request(req, &userInfo); err != nil {
return userInfo, err
}
return userInfo, nil
}
func (c *WorkWechatClient) GetUserInfo(ctx context.Context, code string) (auth2.UserInfo, error) {
var info auth2.UserInfo
userid, err := c.getUserId(ctx, code)
if err != nil {
return info, err
}
userInfo, err := c.getUserInfo(ctx, userid.UserId)
if err != nil {
return info, err
}
info.UserId = userInfo.UserId
info.Name = userInfo.Name
if userid.UserTicket == "" {
return info, nil
}
private, err := c.getUserPrivateInfo(ctx, userid.UserTicket)
if err != nil {
return info, err
}
info.Mail = private.BizMail
info.Avatar = private.Avatar
info.Mobile = private.Mobile
return info, nil
}