forked from wts/wts
203 lines
6.2 KiB
Go
203 lines
6.2 KiB
Go
// 和微信有关的handler
|
||
package handler
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"errors"
|
||
"fmt"
|
||
"log/slog"
|
||
"math/big"
|
||
"net/http"
|
||
"time"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
"github.com/jackc/pgx/v5/pgtype"
|
||
"github.com/labstack/echo/v4"
|
||
hutil "zsxyww.com/wts/handler/handlerUtilities"
|
||
"zsxyww.com/wts/handler/logic"
|
||
"zsxyww.com/wts/model/sqlc"
|
||
)
|
||
|
||
// GET: /api/v3p/wx
|
||
// POST: /api/v3p/wx
|
||
// reveive: WeChat Specificed Request
|
||
// return 200 on success,500 on error
|
||
// type: WeCaht Specificed JSON/XML Response
|
||
// this API is used to communicate with WeChat Server, Frontend developers are unneeded to care about it.
|
||
func WXEntry(i echo.Context) error {
|
||
c := i.(*hutil.WtsCtx)
|
||
|
||
id := i.Response().Header().Get(echo.HeaderXRequestID)
|
||
|
||
slog.Info("收到HTTP请求", "id", id, "URI", i.Request().URL.Path, "from", i.RealIP(), "method", i.Request().Method, "user_agent", i.Request().UserAgent())
|
||
slog.Debug("具体的请求信息", "id", id, "headers", i.Request().Header, "query_params", i.Request().URL.Query(), "body", i.Get("body"))
|
||
|
||
wc := c.WX.GetServer(i.Request(), i.Response().Writer)
|
||
//负责回复用户从公众号聊天栏发来的消息与推送的event
|
||
wc.SetMessageHandler(logic.WXMsgHandler(c))
|
||
err := wc.Serve()
|
||
if err != nil {
|
||
i.Logger().Error("wechat server error:", err)
|
||
i.String(500, "in: "+time.Now().String()+" wechat handler error,please view logs.")
|
||
return err
|
||
}
|
||
wc.Send()
|
||
return nil
|
||
}
|
||
|
||
// 执行微信的OAuth2.0授权流程,跳转到微信授权页面
|
||
// GET: /api/v3p/wx/auth
|
||
// receive: none
|
||
// return: 500 on error
|
||
// type: string on error,no return on success, passing a cookie for OAuth2.0 verification
|
||
// special: redirect to WeChat OAuth2.0 authorization page
|
||
func WXAuth(i echo.Context) error {
|
||
c := i.(*hutil.WtsCtx)
|
||
|
||
id := i.Response().Header().Get(echo.HeaderXRequestID)
|
||
|
||
slog.Info("收到HTTP请求", "id", id, "URI", i.Request().URL.Path, "from", i.RealIP(), "method", i.Request().Method, "user_agent", i.Request().UserAgent())
|
||
slog.Debug("具体的请求信息", "id", id, "headers", i.Request().Header, "query_params", i.Request().URL.Query(), "body", i.Get("body"))
|
||
|
||
// 构造请求参数
|
||
uri := "https://" + c.Cfg.WX.CallBackURL + "/api/v3p/wx/authsuccess"
|
||
s := "snsapi_userinfo"
|
||
state := genAuthState()
|
||
|
||
// 将随机生成的state存到前端,用来在回调时同步校验
|
||
cookie := new(http.Cookie)
|
||
cookie.Name = "oauth_state"
|
||
cookie.Path = "/"
|
||
cookie.Value = state
|
||
cookie.Expires = time.Now().Add(5 * time.Minute)
|
||
cookie.HttpOnly = true
|
||
cookie.Secure = true
|
||
i.SetCookie(cookie)
|
||
|
||
// 重定向到微信授权页面
|
||
to, err := c.WX.GetOauth().GetRedirectURL(uri, s, state)
|
||
if err != nil {
|
||
return c.String(500, "生成WeChat OAuth2.0授权链接失败:"+err.Error())
|
||
}
|
||
return c.Redirect(http.StatusFound, to)
|
||
}
|
||
|
||
// GET: /api/v3p/wx/authsuccess
|
||
// receive: WeChat specificed OAuth2.0 callback parameters
|
||
// return: 400/500 on error,200 on success
|
||
// type: string on error, Redirect on success(Cookie containing JWT is set)
|
||
// this is automaticly accessed after OAuth2.0 authorization success.
|
||
func WXAuthSuccess(i echo.Context) error {
|
||
c := i.(*hutil.WtsCtx)
|
||
|
||
id := i.Response().Header().Get(echo.HeaderXRequestID)
|
||
|
||
slog.Info("收到HTTP请求", "id", id, "URI", i.Request().URL.Path, "from", i.RealIP(), "method", i.Request().Method, "user_agent", i.Request().UserAgent())
|
||
slog.Debug("具体的请求信息", "id", id, "headers", i.Request().Header, "query_params", i.Request().URL.Query(), "body", i.Get("body"))
|
||
|
||
// 校验微信返回
|
||
s := i.QueryParam("state")
|
||
if s == "" {
|
||
return c.String(http.StatusBadRequest, "未获取到授权 state")
|
||
}
|
||
|
||
cookie, err := c.Cookie("oauth_state")
|
||
if err != nil {
|
||
return c.String(http.StatusBadRequest, "无法获取 state cookie,请求可能已过期或非法")
|
||
}
|
||
|
||
if s != cookie.Value {
|
||
return c.String(http.StatusForbidden, "state 参数校验失败")
|
||
}
|
||
|
||
cookie.Expires = time.Now().Add(-1 * time.Hour)
|
||
i.SetCookie(cookie)
|
||
|
||
code := i.QueryParam("code")
|
||
if code == "" {
|
||
return c.String(http.StatusBadRequest, "未获取到授权 code")
|
||
}
|
||
|
||
res, err := c.WX.GetOauth().GetUserAccessToken(code)
|
||
if err != nil {
|
||
return c.String(http.StatusInternalServerError, fmt.Sprintf("换取 access_token 失败: %v", err))
|
||
}
|
||
|
||
openID := res.OpenID
|
||
|
||
userInfo, err := c.WX.GetOauth().GetUserInfo(res.AccessToken, openID, "zh_CN")
|
||
if err != nil {
|
||
return c.String(http.StatusInternalServerError, fmt.Sprintf("获取用户信息失败: %v", err))
|
||
}
|
||
|
||
// 查询数据库,看看有没有OpenID对应的用户
|
||
ctx := i.Request().Context()
|
||
u := sqlc.WtsVUser{}
|
||
var reg bool
|
||
if err = c.DB.DoQuery(ctx, openID, func(q *sqlc.Queries) error {
|
||
u, err = q.GetUserByWX(ctx, openID)
|
||
if err != nil {
|
||
switch true {
|
||
case errors.Is(err, pgx.ErrNoRows):
|
||
reg = false
|
||
return nil
|
||
default:
|
||
return err
|
||
}
|
||
}
|
||
reg = true
|
||
return nil
|
||
}); err != nil {
|
||
return c.String(500, "Database Query Error,Transaction Rollbacked:"+err.Error())
|
||
}
|
||
|
||
//生成对应的JWT
|
||
var t string
|
||
if !reg {
|
||
t, err = hutil.NewWtsJWT(openID, "", sqlc.WtsAccessUnregistered, userInfo.Nickname, userInfo.HeadImgURL, "用户", 30)
|
||
if err != nil {
|
||
return c.String(500, "生成JWT(临时)失败:"+err.Error())
|
||
}
|
||
} else {
|
||
var access sqlc.WtsAccess
|
||
if u.Op && u.Access.Valid {
|
||
access = u.Access.WtsAccess
|
||
} else {
|
||
access = sqlc.WtsAccessUser
|
||
}
|
||
t, err = hutil.NewWtsJWT(u.Wx, u.Sid.String, access, userInfo.Nickname, userInfo.HeadImgURL, emptyText1(u.Name), 95)
|
||
if err != nil {
|
||
return c.String(500, "JWT生成失败:"+err.Error())
|
||
}
|
||
}
|
||
|
||
// 将JWT写入Cookie,后端不从cookie读取JWT,前端应该立即将该cookie存储到localStorage并通过请求头传递JWT
|
||
jwt := new(http.Cookie)
|
||
jwt.Name = "jwt"
|
||
jwt.Value = t
|
||
jwt.Expires = time.Now().Add(5 * time.Minute)
|
||
jwt.HttpOnly = false
|
||
jwt.Secure = true
|
||
jwt.Path = "/"
|
||
i.SetCookie(jwt)
|
||
|
||
return c.Redirect(http.StatusFound, c.Cfg.FrontEnd.OnAuthSuccess)
|
||
}
|
||
|
||
func genAuthState() string {
|
||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
result := [16]byte{}
|
||
for i := 0; i < 16; i++ {
|
||
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||
result[i] = charset[num.Int64()]
|
||
}
|
||
return string(result[:])
|
||
}
|
||
|
||
func emptyText1(t pgtype.Text) string {
|
||
if !t.Valid {
|
||
return "你是谁?"
|
||
}
|
||
return t.String
|
||
}
|