现在大多数app或wap都实现了通过手机号获取验证码进行验证登录,下面来看下用go来实现手机号发送短信验证码登录的过程,基于的框架是gin 。

首先是短信服务商的申请,比如腾讯云、阿里云、网易易盾等,腾讯云自己申请个微信公众号就行,然后申请相关的短信签名、和短信模板,腾讯有100条试用喔。

具体的代码实现

配置腾讯云短信服务的发送短信相关配置,具体可以参考腾讯云短信服务的api 文档,进行配置

1sms:
2  secret-key:  #秘钥,可在控制台查询
3  secret-id:  #秘钥id ,可在控制台查询
4  sms-sdk-app-id:  #应用id
5  Sign-name:  #申请的签名
6  template-id:  #模板id

go 这里采用的是viper进行加载配置,相关的加载配置代码如下

定义相关的配置结构体,并加载到整个项目的总的options 配置结构体中

 1// sms 发送短信的配置options
 2type SmsOptions struct {
 3 SecretKey   string `json:"secret-key,omitempty" mapstructure:"secret-key"`
 4 SecretId    string `json:"secret-id,omitempty" mapstructure:"secret-id"`
 5 SmsSdkAppId string `json:"sms-sdk-app-id,omitempty" mapstructure:"sms-sdk-app-id"`
 6 SignName    string `json:"sign-name,omitempty" mapstructure:"sign-name"`
 7 TemplateId  string `json:"template-id,omitempty" mapstructure:"template-id"`
 8}
 9
10func NewSmsOptions() *SmsOptions {
11 return &SmsOptions{
12  SecretKey:   "",
13  SecretId:    "",
14  SmsSdkAppId: "",
15  SignName:    "",
16  TemplateId:  "",
17 }
18}
19
20// 这为项目总的一个options配置,项目启动的时候会将yaml中的加载到option中
21type Options struct {
22 GenericServerRunOptions *genericoptions.ServerRunOptions `json:"server" mapstructure:"server"`
23 MySQLOptions *genericoptions.MySQLOptions `json:"mysql" mapstructure:"mysql"`
24 InsecuresServing *genericoptions.InsecureServerOptions `json:"insecure" mapstructure:"insecure"`
25 Log *logger.Options `json:"log" mapstructure:"log"`
26 RedisOptions *genericoptions.RedisOptions `json:"redis" mapstructure:"redis"`
27 SmsOptions *genericoptions.SmsOptions `json:"sms" mapstructure:"sms"`
28} 
29 
30func NewOptions() *Options  {
31 o:=Options{
32  GenericServerRunOptions: genericoptions.NewServerRunOptions(),
33  MySQLOptions: genericoptions.NewMySQLOptions(),
34  InsecuresServing: genericoptions.NewInsecureServerOptions(),
35  RedisOptions: genericoptions.NewRedisOptions(),
36  Log: logger.NewOptions(),
37  SmsOptions: genericoptions.NewSmsOptions(),
38 }
39 return &o
40}

viper加载配置的代码如下

 1func AddConfigToOptions(options *options.Options) error {
 2    viper.SetConfigName("config")
 3    viper.AddConfigPath("config/")
 4    viper.SetConfigType("yaml")
 5    err := viper.ReadInConfig()
 6    if err != nil {
 7        return err
 8    }
 9
10
11    optDecode := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(mapstructure.StringToTimeDurationHookFunc(), StringToByteSizeHookFunc()))
12
13
14    err = viper.Unmarshal(options, optDecode)
15    fmt.Println(options)
16    if err != nil {
17        return err
18    }
19    return nil
20}
21
22
23func StringToByteSizeHookFunc() mapstructure.DecodeHookFunc {
24    return func(f reflect.Type,
25    t reflect.Type, data interface{}) (interface{}, error) {
26        if f.Kind() != reflect.String {
27            return data, nil
28        }
29        if t != reflect.TypeOf(datasize.ByteSize(5)) {
30            return data, nil
31        }
32        raw := data.(string)
33        result := new(datasize.ByteSize)
34        result.UnmarshalText([]byte(raw))
35        return result.Bytes(), nil
36    }
37}

下面是发送验证码的实现

 1type SmsClient struct {
 2    Credential *common.Credential
 3    Region     string
 4    Cpf        *profile.ClientProfile
 5    Request    SmsRequest
 6}
 7
 8type Option func(*SmsClient)
 9
10func NewSmsClient(options ...func(client *SmsClient)) *SmsClient {
11    client := &SmsClient{
12        Region: "ap-guangzhou",
13        Cpf:    profile.NewClientProfile(),
14    }
15    for _, option := range options {
16        option(client)
17    }
18    return client
19
20}
21
22func WithRequest(request SmsRequest) Option {
23    return func(smsClient *SmsClient) {
24        smsClient.Request = request
25    }
26}
27
28func WithCredential(options options.SmsOptions) Option {
29    return func(smsClient *SmsClient) {
30        smsClient.Credential = common.NewCredential(options.SecretId, options.SecretKey)
31    }
32}
33func WithCpfReqMethod(method string) Option {
34    return func(smsClient *SmsClient) {
35        smsClient.Cpf.HttpProfile.ReqMethod = method
36    }
37}
38func WithCpfReqTimeout(timeout int) Option {
39    return func(smsClient *SmsClient) {
40        smsClient.Cpf.HttpProfile.ReqTimeout = timeout
41    }
42}
43func WithCpfSignMethod(method string) Option {
44    return func(smsClient *SmsClient) {
45        smsClient.Cpf.SignMethod = method
46    }
47}
48
49func (s *SmsClient) Send() bool {
50    sendClient, _ := sms.NewClient(s.Credential, s.Region, s.Cpf)
51    _, err := sendClient.SendSms(s.Request.request)
52    if _, ok := err.(*errors.TencentCloudSDKError); ok {
53        logger.Warnf("An API error has returned: %s", err)
54        return false
55    }
56
57    if err != nil {
58        logger.Warnf("发送短信失败:%s,requestId:%s", err)
59        return false
60    }
61    logger.Info("发送短信验证码成功")
62    return true
63}

定义发送的client,这里采用function option 的编程模式来初始化发送的client.和发送的request,request的代码如下

 1type SmsRequest struct {
 2    request *sms.SendSmsRequest
 3}
 4
 5func NewSmsRequest(options *options.SmsOptions, withOptions ...func(smsRequest *SmsRequest)) *SmsRequest {
 6    request := sms.NewSendSmsRequest()
 7
 8    request.SmsSdkAppId = &options.SmsSdkAppId
 9    request.SignName = &options.SignName
10    request.TemplateId = &options.TemplateId
11    smsRequest := &SmsRequest{request: request}
12    for _, option := range withOptions {
13        option(smsRequest)
14    }
15    return smsRequest
16
17}
18
19type RequestOption func(*SmsRequest)
20
21func WithPhoneNumberSet(phoneSet []string) RequestOption {
22    return func(smsRequest *SmsRequest) {
23        smsRequest.request.PhoneNumberSet = common.StringPtrs(phoneSet)
24    }
25}
26
27func WithTemplateParamSet(templateSet []string) RequestOption {
28    return func(smsRequest *SmsRequest) {
29        smsRequest.request.TemplateParamSet = common.StringPtrs(templateSet)
30    }
31}

创建发送验证码的控制层,发送成功,并将此处的电话号码和验证码保存到redis缓存中,用来登录时候的验证码有效性的校验

 1func (u *userService) SendPhoneCode(ctx context.Context, phone string) bool {
 2    // 获取配置参数
 3    smsSetting := global.TencenSmsSetting
 4    phoneSet := []string{phone}
 5    // 随机生成6位的验证码
 6    var randCode string = fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
 7    templateSet := []string{randCode, "60"}
 8    smsRequest := tencenSms.NewSmsRequest(smsSetting, tencenSms.WithPhoneNumberSet(phoneSet), tencenSms.WithTemplateParamSet(templateSet))
 9    smsClient := tencenSms.NewSmsClient(tencenSms.WithRequest(*smsRequest), tencenSms.WithCredential(*smsSetting))
10    go smsClient.Send()
11    // 将验证码和手机号保存到redis中
12    _ = u.cache.UserCaches().SetSendPhoneCodeCache(ctx, phone, randCode)
13    return true
14
15}

后面是通过手机验证码进行登录的流程

 1func (u *userService) LoginByPhoneCode(ctx context.Context, phone string, phoneCode string) (*model.User,error) {
 2    // 从缓存中获取该手机号对应的验证码是否匹配
 3    cacheCode, err :=u.cache.UserCaches().GetSendPhoneCodeFromCache(ctx,phone)
 4    if err != nil {
 5        return nil, errors.WithCode(code.ErrUserPhoneCodeExpire,err.Error())
 6    }
 7    if cacheCode!=phoneCode {
 8        return nil,errors.WithCode(code.ErrUserPhoneCodeMiss,"")
 9    }
10    return &model.User{
11        Nickname: "lala",
12    }, nil
13}