package query import ( "context" "encoding/json" "errors" "fmt" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/bcache" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/utils" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-openapi/internal/service" "net/url" "strings" "sync/atomic" "time" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" ) const ydOperatorTokenURL = "https://api.iot.10086.cn/v5/ec/get/token" type ydOperatorTokenResp struct { Status string `json:"status"` Message string `json:"message"` Result []struct { Token string `json:"token"` Ttl int64 `json:"ttl"` } `json:"result"` } var ydOperatorTokenSeq atomic.Int64 type sQueryToken struct { } func NewQueryToken() *sQueryToken { return &sQueryToken{} } func init() { service.RegisterQueryToken(NewQueryToken()) } // Token 获取Token func (l *sQueryToken) Token(ctx context.Context, input model.TokenInputApi) (res model.TokenOutputApi, err error) { // 查询API集成信息 var tApi entity.TApi err = dao.TApi.Ctx(ctx).Where(dao.TApi.Columns().AppKey, input.AppKey).Scan(&tApi) if err != nil { g.Log().Errorf(ctx, "QueryToken.Token, Select t api by app key failed, err: %v, input: %v", err, input) return res, gerror.New("APPKey或APPSecret错误") } // 校验APPKey是否存在和APPSecret是否正确 if tApi.Id == 0 { g.Log().Debugf(ctx, "QueryToken.Token, The t api does not exist, input: %v", input) return res, gerror.New("APPKey或APPSecret错误") } if tApi.AppSecret != input.AppSecret { g.Log().Debugf(ctx, "QueryToken.Token, APPSecret verification failed, input: %v", input) return res, gerror.New("APPKey或APPSecret错误") } if tApi.Status == 1 { g.Log().Debugf(ctx, "QueryToken.Token, The t api is disabled, input: %v", input) return res, gerror.New("API集成已停用") } // 验证客户 // dept, err := service.SysDept().GetDeptById(ctx, tApi.DeptId) var dept entity.SysDept err = dao.SysDept.Ctx(ctx).Where(dao.SysDept.Columns().DeptId, tApi.DeptId).Scan(&dept) if err != nil { g.Log().Errorf(ctx, "QueryToken.Token, Select dept by id failed, err: %v, input: %v", err, input) return res, gerror.New("APPKey或APPSecret错误") } if dept.DeptId == 0 { g.Log().Debugf(ctx, "QueryToken.Token, The organization does not exist, input: %v", input) return res, gerror.New("APPKey或APPSecret错误") } g.Log().Debugf(ctx, "dept.DeptId: %v", dept.DeptId) if dept.DeptKey == "" { g.Log().Debugf(ctx, "QueryToken.Token, The organization does not have API service enabled, input: %v", input) return res, gerror.New("客户未开启API服务") } identity := &model.CustomClaimsApi{ BaseClaimsApi: model.BaseClaimsApi{ UUID: strings.ReplaceAll(uuid.New().String(), "-", ""), ApiId: tApi.Id, AppKey: tApi.AppKey, GroupId: tApi.GroupId, DeptKey: dept.DeptKey, DeptName: dept.DeptName, DeptType: dept.DeptType, TenantId: dept.TenantId, DeptId: dept.DeptId, LoginAt: time.Now().Unix(), }, } contextModel := new(model.Context) ctx = contexts.Set(ctx, contextModel) if input.Refresh == "" { input.Refresh = "1" } else if input.Refresh != "0" && input.Refresh != "1" { return res, gerror.New("刷新参数错误") } res.AppId = dept.DeptKey if input.Refresh == "0" { tokenUuid, cacheErr := bcache.GetApiAuthActiveToken(ctx, dept.DeptId) if cacheErr == nil && tokenUuid != "" { token, tokenErr := l.GenerateShortToken(ctx, tokenUuid) if tokenErr != nil { return res, tokenErr } res.AccessToken = token return res, nil } } if input.Refresh == "1" { tokenUuid, cacheErr := bcache.GetApiAuthActiveToken(ctx, dept.DeptId) if cacheErr == nil && tokenUuid != "" { _ = bcache.DelApiAuthToken(ctx, tokenUuid) } _ = bcache.DelApiAuthActiveToken(ctx, dept.DeptId) } _, token, err := l.GenerateToken(ctx, identity) if err != nil { return res, err } res.AccessToken = token return res, nil } // OperatorToken 获取移动运营商Token(仅需chanKey,不校验白名单和AppId) func (l *sQueryToken) OperatorToken(ctx context.Context, input model.OperatorTokenInput) (res model.OperatorTokenOutput, err error) { return l.fetchYdOperatorToken(ctx, input, false) } // OperatorTokenWithAuth 获取移动运营商Token(校验白名单+AppId,需传appId+chanKey) func (l *sQueryToken) OperatorTokenWithAuth(ctx context.Context, input model.OperatorTokenInput) (res model.OperatorTokenOutput, err error) { if input.AppId == "" { return res, gerror.New("AppId不能为空") } return l.fetchYdOperatorToken(ctx, input, true) } // fetchYdOperatorToken 内部实现,checkShare=true时校验白名单和AppId func (l *sQueryToken) fetchYdOperatorToken(ctx context.Context, input model.OperatorTokenInput, checkShare bool) (res model.OperatorTokenOutput, err error) { if input.ChanKey == "" { return res, gerror.New("通道Key不能为空") } fields := []any{ dao.TSimChannels.Columns().TempKey, dao.TSimChannels.Columns().ChanAlias, dao.TSimChannels.Columns().ChanKey, dao.TSimChannels.Columns().AppKey, dao.TSimChannels.Columns().AppSecret, } if checkShare { fields = append(fields, dao.TSimChannels.Columns().ShareEnabled) } var simChannels entity.TSimChannels dbErr := dao.TSimChannels.DB().Model(dao.TSimChannels.Table()).Safe().Ctx(ctx). Fields(fields...). Where(dao.TSimChannels.Columns().ChanKey, input.ChanKey). Scan(&simChannels) if dbErr != nil { g.Log().Errorf(ctx, "QueryToken.fetchYdOperatorToken, Select sim channel by chan key failed, err: %v, input: %v", dbErr, input) return res, gerror.New("通道不存在") } if simChannels.ChanKey == "" { return res, gerror.New("通道不存在") } if checkShare { if simChannels.ShareEnabled != 1 { g.Log().Warningf(ctx, "QueryToken.fetchYdOperatorToken, sim channel share not enabled, chanKey: %s", input.ChanKey) return res, gerror.New("该通道未开放共享Token") } if input.AppId != simChannels.AppKey { g.Log().Warningf(ctx, "QueryToken.fetchYdOperatorToken, appId mismatch, chanKey: %s, inputAppId: %s, channelAppId: %s", input.ChanKey, input.AppId, simChannels.AppKey) return res, gerror.New("AppId与通道不匹配") } } var tempModel string if simChannels.TempKey != "" { var template entity.TSimChannelTemplate if tmplErr := dao.TSimChannelTemplate.Ctx(ctx). Fields(dao.TSimChannelTemplate.Columns().TempModel). Where(dao.TSimChannelTemplate.Columns().TempKey, simChannels.TempKey). Limit(1).Scan(&template); tmplErr == nil { tempModel = strings.TrimSpace(template.TempModel) } } if strings.EqualFold(tempModel, "shared_token") { result, sharedErr := service.UpstreamChan().GetSharedToken(ctx, input.ChanKey) if sharedErr != nil { return res, gerror.Wrap(sharedErr, "获取共享Token失败") } return model.OperatorTokenOutput{ ChanAlias: result.ChanAlias, ChanKey: result.ChanKey, AccessToken: result.AccessToken, AccessTokenExpire: result.AccessTokenExpire, }, nil } if simChannels.AppKey == "" || simChannels.AppSecret == "" { return res, gerror.New("通道不存在") } now := time.Now() transID := buildYdOperatorTokenTransID(simChannels.AppKey, now) params := g.Map{ "appid": simChannels.AppKey, "password": simChannels.AppSecret, "transid": transID, "refresh": "0", } g.Log().Infof(ctx, "QueryToken.OperatorToken, request yd token url: %s, chanKey: %s", buildYdOperatorTokenRequestURL(simChannels.AppKey, simChannels.AppSecret, transID), input.ChanKey) client := g.Client() client.SetTimeout(10 * time.Second) resp, err := client.Get(ctx, ydOperatorTokenURL, params) if err != nil { g.Log().Errorf(ctx, "QueryToken.OperatorToken, request yd token failed, err: %v, chanKey: %s", err, input.ChanKey) return res, gerror.New("获取移动运营商Token失败") } body := resp.ReadAllString() var ydResp ydOperatorTokenResp if err = json.Unmarshal([]byte(body), &ydResp); err != nil { g.Log().Errorf(ctx, "QueryToken.OperatorToken, parse yd token response failed, err: %v, chanKey: %s, body: %s", err, input.ChanKey, body) return res, gerror.New("解析移动运营商Token响应失败") } if ydResp.Status != "0" { g.Log().Warningf(ctx, "QueryToken.OperatorToken, yd token response error, status: %s, message: %s, chanKey: %s", ydResp.Status, ydResp.Message, input.ChanKey) if ydResp.Message != "" { return res, gerror.New(ydResp.Message) } return res, gerror.New("获取移动运营商Token失败") } if len(ydResp.Result) == 0 || ydResp.Result[0].Token == "" { return res, gerror.New("移动运营商Token为空") } if ydResp.Result[0].Ttl <= 0 { return res, gerror.New("移动运营商Token有效期无效") } return model.OperatorTokenOutput{ ChanAlias: simChannels.ChanAlias, ChanKey: simChannels.ChanKey, AccessToken: ydResp.Result[0].Token, AccessTokenExpire: gtime.NewFromTime(now.Add(time.Duration(ydResp.Result[0].Ttl) * time.Second)), }, nil } func buildYdOperatorTokenTransID(appKey string, now time.Time) string { return appKey + now.Format("20060102150405") + fmt.Sprintf("%08d", ydOperatorTokenSeq.Add(1)%100000000) } func buildYdOperatorTokenRequestURL(appKey, password, transID string) string { values := url.Values{} values.Set("appid", appKey) values.Set("password", password) values.Set("transid", transID) values.Set("refresh", "0") return ydOperatorTokenURL + "?" + values.Encode() } // GenerateToken 生成Token func (s *sQueryToken) GenerateToken(ctx context.Context, customer *model.CustomClaimsApi) (claims *model.CustomClaimsApi, shortToken string, err error) { ets := g.Cfg().MustGet(ctx, "jwt.expiresTime", "3650d").String() bts := g.Cfg().MustGet(ctx, "jwt.bufferTime", "1d").String() iss := g.Cfg().MustGet(ctx, "jwt.issuer", "JieIOTAPI").String() sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String() et, _ := utils.ParseDuration(ets) bt, _ := utils.ParseDuration(bts) g.Log().Infof(ctx, "GenerateToken: customer:%+v, et:%s, bt:%s, iss:%s", customer, et, bt, iss) tokenUuid := strings.ReplaceAll(uuid.New().String(), "-", "") expiresAt := time.Now().Add(et).Unix() claims = &model.CustomClaimsApi{ BaseClaimsApi: model.BaseClaimsApi{ UUID: tokenUuid, ApiId: customer.ApiId, AppKey: customer.AppKey, GroupId: customer.GroupId, DeptKey: customer.DeptKey, DeptName: customer.DeptName, DeptType: customer.DeptType, DeptId: customer.DeptId, TenantId: customer.TenantId, LoginAt: time.Now().Unix(), }, BufferTime: int64(bt / time.Second), RegisteredClaims: jwt.RegisteredClaims{ Audience: jwt.ClaimStrings{"JIEIOT"}, ExpiresAt: jwt.NewNumericDate(time.Unix(expiresAt, 0)), NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), Issuer: iss, }, } fullToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) fullTokenString, err := fullToken.SignedString([]byte(sk)) if err != nil { return nil, "", err } if err = bcache.SetApiAuthToken(ctx, tokenUuid, fullTokenString, et); err != nil { return nil, "", err } if err = bcache.SetApiAuthActiveToken(ctx, customer.DeptId, tokenUuid, et); err != nil { return nil, "", err } shortToken, err = s.GenerateShortToken(ctx, tokenUuid) if err != nil { return nil, "", err } return claims, shortToken, nil } // GenerateShortToken 生成短令牌 func (s *sQueryToken) GenerateShortToken(ctx context.Context, tokenUuid string) (token string, err error) { sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String() shortToken := jwt.NewWithClaims(jwt.SigningMethodHS256, &model.ApiTokenClaims{ UUID: tokenUuid, }) delete(shortToken.Header, "typ") return shortToken.SignedString([]byte(sk)) } // 解析Token func (s *sQueryToken) ParseToken(ctx context.Context, token string) (claims *model.CustomClaimsApi, err error) { sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String() t, err := jwt.ParseWithClaims(token, &model.ApiTokenClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(sk), nil }) if err != nil { g.Log().Warningf(ctx, "sApiAuth.ParseToken short token err: %+v", err) return nil, err } shortClaims, ok := t.Claims.(*model.ApiTokenClaims) if !ok || !t.Valid { return nil, errors.New("token invalid") } fullTokenString, err := bcache.GetApiAuthToken(ctx, shortClaims.UUID) if err != nil { g.Log().Warningf(ctx, "sApiAuth.ParseToken get full token err: %+v", err) return nil, err } fullToken, err := jwt.ParseWithClaims(fullTokenString, &model.CustomClaimsApi{}, func(token *jwt.Token) (interface{}, error) { return []byte(sk), nil }) if err != nil { g.Log().Warningf(ctx, "sApiAuth.ParseToken full token err: %+v", err) return nil, err } claims, ok = fullToken.Claims.(*model.CustomClaimsApi) if !ok || !fullToken.Valid { return nil, errors.New("token invalid") } if claims.ExpiresAt == nil || claims.ExpiresAt.Unix() < time.Now().Unix() { return nil, errors.New("token expired") } if claims.UUID != shortClaims.UUID { return nil, errors.New("token invalid") } return claims, nil } // 删除Token func (s *sQueryToken) DeleteToken(ctx context.Context, token string) (err error) { sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "49c54195e73304e74a8429b178965865").String() t, err := jwt.ParseWithClaims(token, &model.ApiTokenClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(sk), nil }) if err != nil { return err } shortClaims, ok := t.Claims.(*model.ApiTokenClaims) if !ok { return errors.New("token invalid") } return bcache.DelApiAuthToken(ctx, shortClaims.UUID) } // 获取token func (s *sQueryToken) GetAccessToken(ctx context.Context) (token string, err error) { headerToken := g.RequestFromCtx(ctx).Header.Get("X-Access-Token") if headerToken == "" { return "", gerror.NewCode(gcode.CodeNotImplemented) } g.Log().Error(ctx, headerToken) token = strings.TrimPrefix(headerToken, "Bearer ") if token == "" { token = headerToken } return token, nil } // 根据Token获取当前登录客户信息 func (s *sQueryToken) GetCurrentCustomer(ctx context.Context) (claims *model.CustomClaimsApi, err error) { token, err := s.GetAccessToken(ctx) g.Log().Debugf(ctx, "sQueryToken.GetCurrentCustomer token: %+v, err: %+v", token, err) if err != nil { return nil, err } claims, err = s.ParseToken(ctx, token) g.Log().Debugf(ctx, "sApiAuth.GetCurrentCustomer claims: %+v, err: %+v", claims, err) if err != nil { g.Log().Warningf(ctx, "sApiAuth.GetCurrentCustomer err: %+v", err) return nil, err } return claims, nil }