package Netease import ( "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "io" "math/rand" "net/http" "net/url" "sort" "strconv" "strings" "time" "gitea.ddegame.cn/open/servicebase/pkg/log" "github.com/pkg/errors" "github.com/spf13/viper" "github.com/tjfoc/gmsm/sm3" ) const ( apiURL = "https://verify.dun.163.com/v1/face/liveness/h5/auth" recheckTokenUrl = "https://verify.dun.163.com/v1/face/liveness/h5/recheck" version = "v1" ) type ApplyInfoReq struct { Name string CardNo string RedirectUrl string CallBackUrl string DataId string CallbackValidate string EncryptType string } type BaseRsp[T any] struct { Code int `json:"code"` Msg string `json:"msg"` Result T `json:"result"` } type H5ApplyLivePersonRsp struct { AuthUrl string `json:"authUrl"` AuthToken string `json:"authToken"` } type H5ReCheckLivePersonTokenRsp struct { TaskId string `json:"taskId"` PicType int64 `json:"picType"` Avatar string `json:"avatar"` Status int64 `json:"status"` ReasonType int `json:"reasonType"` IsPayed int64 `json:"isPayed"` SimilarityScore float64 `json:"similarityScore"` FaceMatched int64 `json:"faceMatched"` FaceAttributeInfo any `json:"faceAttributeInfo"` ExtInfo ExtInfoEntity `json:"extInfo"` } type ExtInfoEntity struct { SuspectedNonageFlag bool `json:"suspectedNonageFlag"` } // H5ApplyLivePerson 请求易盾生成h5人身验证h5 url 和 authToken func H5ApplyLivePerson(ctx context.Context, req *ApplyInfoReq) (*H5ApplyLivePersonRsp, error) { params := url.Values{ "name": []string{req.Name}, "cardNo": []string{req.CardNo}, "redirectUrl": []string{req.RedirectUrl}, } rsp, err := apply(params) if err != nil { return nil, err } return rsp, nil } // H5ReCheckLivePersonToken 根据authToken二次验证,并获取相关信息,如正面拍照的活体图片 func H5ReCheckLivePersonToken(ctx context.Context, token string) (*H5ReCheckLivePersonTokenRsp, error) { params := url.Values{ "authToken": []string{token}, } rsp, err := checkToken(params) if err != nil { return nil, err } return rsp, nil } // 生成签名信息 func genSignature(params url.Values) string { var paramStr string keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { paramStr += key + params[key][0] } paramStr += viper.GetString("netease.captcha.secretKey") if params["signatureMethod"] != nil && params["signatureMethod"][0] == "SM3" { sm3Reader := sm3.New() sm3Reader.Write([]byte(paramStr)) return hex.EncodeToString(sm3Reader.Sum(nil)) } md5Reader := md5.New() md5Reader.Write([]byte(paramStr)) return hex.EncodeToString(md5Reader.Sum(nil)) } // 请求易盾接口 func apply(params url.Values) (*H5ApplyLivePersonRsp, error) { params["secretId"] = []string{viper.GetString("netease.captcha.secretId")} params["businessId"] = []string{viper.GetString("netease.captcha.businessId")} params["version"] = []string{version} params["timestamp"] = []string{strconv.FormatInt(time.Now().UnixNano()/1000000, 10)} params["nonce"] = []string{strconv.FormatInt(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(10000000000), 10)} params["signature"] = []string{genSignature(params)} resp, err := http.Post(apiURL, "application/x-www-form-urlencoded", strings.NewReader(params.Encode())) if err != nil { return nil, errors.WithStack(err) } if resp.StatusCode != http.StatusOK { return nil, errors.Errorf("请求网易易盾获取h5人身验证失败,状态码为%d", resp.StatusCode) } defer resp.Body.Close() contents, _ := io.ReadAll(resp.Body) log.InfoF("请求网易易盾获取h5人身验证响应: %s", string(contents)) var rsp BaseRsp[*H5ApplyLivePersonRsp] err = json.Unmarshal(contents, &rsp) if err != nil { return nil, errors.WithStack(err) } if rsp.Code != 200 { return nil, errors.Errorf("响应体状态码不是200(%d),信息: %s", rsp.Code, rsp.Msg) } return rsp.Result, nil } func checkToken(params url.Values) (*H5ReCheckLivePersonTokenRsp, error) { params["secretId"] = []string{viper.GetString("netease.captcha.secretId")} params["businessId"] = []string{viper.GetString("netease.captcha.businessId")} params["version"] = []string{version} params["timestamp"] = []string{strconv.FormatInt(time.Now().UnixNano()/1000000, 10)} params["nonce"] = []string{strconv.FormatInt(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(10000000000), 10)} params["signature"] = []string{genSignature(params)} resp, err := http.Post(recheckTokenUrl, "application/x-www-form-urlencoded", strings.NewReader(params.Encode())) if err != nil { return nil, errors.WithStack(err) } if resp.StatusCode != http.StatusOK { return nil, errors.Errorf("请求网易易盾获取h5人身验证失败,状态码为%d", resp.StatusCode) } defer resp.Body.Close() contents, _ := io.ReadAll(resp.Body) log.InfoF("请求网易云盾获取人身核验二次验证token响应: %s", string(contents)) var rsp BaseRsp[*H5ReCheckLivePersonTokenRsp] err = json.Unmarshal(contents, &rsp) if err != nil { return nil, errors.WithStack(err) } if rsp.Code != 200 { return nil, errors.Errorf("响应体状态码不是200(%d),信息: %s", rsp.Code, rsp.Msg) } return rsp.Result, nil } var livePersonAndIdCheckFailReason = map[int]string{ 2: "活体通过,姓名身份证号一致,人脸比对非同一人", 3: "活体通过,姓名身份证号不一致", 4: "活体不通过", 5: "活体检测超时或出现异常", 6: "活体通过,查无此身份证", 7: "活体通过,库中无此身份证照片", 8: "活体通过,人脸照过大", 9: "活体通过,权威数据源出现异常", 10: "疑似攻击,建议拦截", 11: "检测对象为未成年人", } func GetLivePersonAndIdCheckFailReason(code int) string { r, ok := livePersonAndIdCheckFailReason[code] if !ok { return fmt.Sprintf("其他原因:ReasonType==%d", code) } return r }