Go alisms 阿里云短信服务构建

概述

本文介绍如何使用 Go 语言构建基于 gRPC 的阿里云短信服务微服务。该服务提供短信验证码发送、验证码校验和短信查询功能,使用 Redis 缓存验证码,支持高并发场景。

PreRequirements

安装依赖工具

1
2
3
# 安装 protoc 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

go.mod

1
2
3
4
5
6
7
8
9
10
11
module github.com/noahzaozao/alisms_service

go 1.21

require (
github.com/aliyun/alibaba-cloud-sdk-go v1.62.700
github.com/redis/go-redis/v9 v9.5.1
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.34.0
gopkg.in/yaml.v3 v3.0.1
)

proto/alisms/alisms.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
syntax = "proto3";

package alisms;

option go_package = "github.com/noahzaozao/alisms_service/proto/alisms";

message SMSVerficationCodeData {
string sign_name = 1;
string phone_numbers = 2;
string template_code = 3;
string template_param = 4;
string sms_up_extend_code = 5;
string out_id = 6;
}

message SMSVerficationCodeCheckData {
string phone_numbers = 1;
string vcode = 2;
}

message SMSVerficationResponseData {
int64 return_code = 1;
string message = 2;
string data = 3;
}

message SMSVerficationQueryData {
string phone_numbers = 1;
string send_date = 2;
string page_size = 3;
string current_page = 4;
string biz_id = 5;
}

message SMSVerficationQueryResponseData {
int64 return_code = 1;
string message = 2;
string data = 3;
}

service AuthService {
rpc SMSVerficationCode(SMSVerficationCodeData) returns (SMSVerficationResponseData) {}
rpc SMSVerficationCodeCheck(SMSVerficationCodeCheckData) returns (SMSVerficationResponseData) {}
rpc SMSVerficationQuery(SMSVerficationQueryData) returns (SMSVerficationQueryResponseData) {}
}

cache/cache.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package cache

import (
"context"
"fmt"
"log"
"sync"
"time"

"github.com/noahzaozao/alisms_service/config"
"github.com/redis/go-redis/v9"
)

type CacheManager struct {
config config.CacheConfig
client *redis.Client
ctx context.Context
}

var instance *CacheManager
var once sync.Once

func CacheMgr() *CacheManager {
once.Do(func() {
instance = &CacheManager{
ctx: context.Background(),
}
})
return instance
}

// Init 初始化缓存配置
func (cacheMgr *CacheManager) Init(cacheConfig config.CacheConfig) error {
cacheMgr.config = cacheConfig
if cacheMgr.config.Type == "redis" {
client, err := cacheMgr.Conn()
if err != nil {
return err
}
cacheMgr.client = client
log.Println("Cache connected successfully")
} else {
return fmt.Errorf("unsupported cache type: %s", cacheMgr.config.Type)
}
return nil
}

// Conn 获取缓存连接
func (cacheMgr *CacheManager) Conn() (*redis.Client, error) {
connStr := fmt.Sprintf("%s:%s", cacheMgr.config.Host, cacheMgr.config.Port)
client := redis.NewClient(&redis.Options{
Addr: connStr,
Password: cacheMgr.config.Password,
DB: cacheMgr.config.DB,
})

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

_, err := client.Ping(ctx).Result()
if err != nil {
return nil, err
}
return client, nil
}

// SetVerificationCode 存储验证码
func (cacheMgr *CacheManager) SetVerificationCode(phone, code string, expiration time.Duration) error {
key := fmt.Sprintf("sms:vcode:%s", phone)
return cacheMgr.client.Set(cacheMgr.ctx, key, code, expiration).Err()
}

// GetVerificationCode 获取验证码
func (cacheMgr *CacheManager) GetVerificationCode(phone string) (string, error) {
key := fmt.Sprintf("sms:vcode:%s", phone)
return cacheMgr.client.Get(cacheMgr.ctx, key).Result()
}

// DeleteVerificationCode 删除验证码
func (cacheMgr *CacheManager) DeleteVerificationCode(phone string) error {
key := fmt.Sprintf("sms:vcode:%s", phone)
return cacheMgr.client.Del(cacheMgr.ctx, key).Err()
}

// Close 关闭连接
func (cacheMgr *CacheManager) Close() error {
if cacheMgr.client != nil {
return cacheMgr.client.Close()
}
return nil
}

config/config.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package config

type SMSConfig struct {
ACCESS_KEY_ID string `yaml:"ACCESS_KEY_ID"`
ACCESS_KEY_SECRET string `yaml:"ACCESS_KEY_SECRET"`
Region string `yaml:"region"` // 默认: cn-hangzhou
}

type CacheConfig struct {
Type string `yaml:"type"`
Host string `yaml:"host"`
Port string `yaml:"port"`
DB int `yaml:"db"`
Password string `yaml:"password"`
}

type SettingConfig struct {
SECRET_KEY string `yaml:"SECRET_KEY"`
DEBUG string `yaml:"DEBUG"`
DEFAULT_CHARSET string `yaml:"DEFAULT_CHARSET"`
Port string `yaml:"port"` // gRPC 服务端口
SMSConfig SMSConfig `yaml:"SMSConfig"`
CACHES []CacheConfig `yaml:"CACHES"`
}

service/sms_service.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package service

import (
"context"
"encoding/json"
"fmt"
"log"
"time"

"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
"github.com/noahzaozao/alisms_service/cache"
"github.com/noahzaozao/alisms_service/config"
pb "github.com/noahzaozao/alisms_service/proto/alisms"
)

type SMSService struct {
config config.SettingConfig
cacheMgr *cache.CacheManager
smsClient *dysmsapi.Client
}

func NewSMSService(cfg config.SettingConfig, cacheMgr *cache.CacheManager) (*SMSService, error) {
region := cfg.SMSConfig.Region
if region == "" {
region = "cn-hangzhou"
}

client, err := dysmsapi.NewClientWithAccessKey(
region,
cfg.SMSConfig.ACCESS_KEY_ID,
cfg.SMSConfig.ACCESS_KEY_SECRET,
)
if err != nil {
return nil, fmt.Errorf("failed to create SMS client: %w", err)
}

return &SMSService{
config: cfg,
cacheMgr: cacheMgr,
smsClient: client,
}, nil
}

// SendVerificationCode 发送验证码
func (s *SMSService) SendVerificationCode(ctx context.Context, req *pb.SMSVerficationCodeData) (*pb.SMSVerficationResponseData, error) {
// 生成6位随机验证码
code := generateCode(6)

// 构建模板参数
templateParam := map[string]string{
"code": code,
}
paramBytes, _ := json.Marshal(templateParam)

// 调用阿里云API
request := dysmsapi.CreateSendSmsRequest()
request.Scheme = "https"
request.SignName = req.SignName
request.PhoneNumbers = req.PhoneNumbers
request.TemplateCode = req.TemplateCode
request.TemplateParam = string(paramBytes)
if req.SmsUpExtendCode != "" {
request.SmsUpExtendCode = req.SmsUpExtendCode
}
if req.OutId != "" {
request.OutId = req.OutId
}

response, err := s.smsClient.SendSms(request)
if err != nil {
log.Printf("Failed to send SMS: %v", err)
return &pb.SMSVerficationResponseData{
ReturnCode: -1,
Message: fmt.Sprintf("发送失败: %v", err),
Data: "",
}, nil
}

// 如果发送成功,存储验证码到Redis(5分钟过期)
if response.Code == "OK" {
if err := s.cacheMgr.SetVerificationCode(req.PhoneNumbers, code, 5*time.Minute); err != nil {
log.Printf("Failed to cache verification code: %v", err)
}
}

responseData, _ := json.Marshal(response)
return &pb.SMSVerficationResponseData{
ReturnCode: parseReturnCode(response.Code),
Message: response.Message,
Data: string(responseData),
}, nil
}

// CheckVerificationCode 校验验证码
func (s *SMSService) CheckVerificationCode(ctx context.Context, req *pb.SMSVerficationCodeCheckData) (*pb.SMSVerficationResponseData, error) {
storedCode, err := s.cacheMgr.GetVerificationCode(req.PhoneNumbers)
if err != nil {
return &pb.SMSVerficationResponseData{
ReturnCode: -1,
Message: "验证码不存在或已过期",
Data: "",
}, nil
}

if storedCode != req.Vcode {
return &pb.SMSVerficationResponseData{
ReturnCode: -1,
Message: "验证码错误",
Data: "",
}, nil
}

// 验证成功后删除验证码(防止重复使用)
_ = s.cacheMgr.DeleteVerificationCode(req.PhoneNumbers)

return &pb.SMSVerficationResponseData{
ReturnCode: 0,
Message: "验证成功",
Data: "",
}, nil
}

// QuerySMSDetails 查询短信发送详情
func (s *SMSService) QuerySMSDetails(ctx context.Context, req *pb.SMSVerficationQueryData) (*pb.SMSVerficationQueryResponseData, error) {
request := dysmsapi.CreateQuerySendDetailsRequest()
request.Scheme = "https"
request.PhoneNumber = req.PhoneNumbers
request.SendDate = req.SendDate
if req.PageSize != "" {
request.PageSize = req.PageSize
} else {
request.PageSize = "10"
}
if req.CurrentPage != "" {
request.CurrentPage = req.CurrentPage
} else {
request.CurrentPage = "1"
}
if req.BizId != "" {
request.BizId = req.BizId
}

response, err := s.smsClient.QuerySendDetails(request)
if err != nil {
log.Printf("Failed to query SMS details: %v", err)
return &pb.SMSVerficationQueryResponseData{
ReturnCode: -1,
Message: fmt.Sprintf("查询失败: %v", err),
Data: "",
}, nil
}

responseData, _ := json.Marshal(response)
return &pb.SMSVerficationQueryResponseData{
ReturnCode: parseReturnCode(response.Code),
Message: response.Message,
Data: string(responseData),
}, nil
}

// generateCode 生成随机验证码
func generateCode(length int) string {
code := ""
for i := 0; i < length; i++ {
code += fmt.Sprintf("%d", time.Now().UnixNano()%10)
}
return code
}

// parseReturnCode 解析返回码
func parseReturnCode(code string) int64 {
if code == "OK" {
return 0
}
return -1
}

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package main

import (
"flag"
"fmt"
"log"
"net"
"os"

"github.com/noahzaozao/alisms_service/cache"
"github.com/noahzaozao/alisms_service/config"
"github.com/noahzaozao/alisms_service/service"
pb "github.com/noahzaozao/alisms_service/proto/alisms"
"google.golang.org/grpc"
"gopkg.in/yaml.v3"
)

var (
configPath = flag.String("config", "./config.yaml", "配置文件路径")
port = flag.String("port", "50051", "gRPC服务端口")
)

func main() {
flag.Parse()

// 加载配置文件
cfg, err := loadConfig(*configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}

// 初始化Redis缓存
if len(cfg.CACHES) < 1 {
log.Fatal("CACHES config not found")
}

cacheMgr := cache.CacheMgr()
if err := cacheMgr.Init(cfg.CACHES[0]); err != nil {
log.Fatalf("Failed to init cache: %v", err)
}
defer cacheMgr.Close()

// 创建SMS服务
smsService, err := service.NewSMSService(cfg, cacheMgr)
if err != nil {
log.Fatalf("Failed to create SMS service: %v", err)
}

// 启动gRPC服务
grpcPort := cfg.Port
if grpcPort == "" {
grpcPort = *port
}

lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort))
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}

s := grpc.NewServer()
pb.RegisterAuthServiceServer(s, &AuthServiceServer{
smsService: smsService,
})

log.Printf("gRPC server listening on :%s", grpcPort)
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}

// AuthServiceServer gRPC服务实现
type AuthServiceServer struct {
pb.UnimplementedAuthServiceServer
smsService *service.SMSService
}

func (s *AuthServiceServer) SMSVerficationCode(ctx context.Context, req *pb.SMSVerficationCodeData) (*pb.SMSVerficationResponseData, error) {
return s.smsService.SendVerificationCode(ctx, req)
}

func (s *AuthServiceServer) SMSVerficationCodeCheck(ctx context.Context, req *pb.SMSVerficationCodeCheckData) (*pb.SMSVerficationResponseData, error) {
return s.smsService.CheckVerificationCode(ctx, req)
}

func (s *AuthServiceServer) SMSVerficationQuery(ctx context.Context, req *pb.SMSVerficationQueryData) (*pb.SMSVerficationQueryResponseData, error) {
return s.smsService.QuerySMSDetails(ctx, req)
}

// loadConfig 加载配置文件
func loadConfig(path string) (*config.SettingConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}

var cfg config.SettingConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}

return &cfg, nil
}

config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
config:
SECRET_KEY: "your-secret-key"
DEBUG: "true"
DEFAULT_CHARSET: "utf-8"
port: "50051"
SMSConfig:
ACCESS_KEY_ID: "your-access-key-id"
ACCESS_KEY_SECRET: "your-access-key-secret"
region: "cn-hangzhou"
CACHES:
- type: "redis"
host: "127.0.0.1"
port: "6379"
db: 0
password: ""

build.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash

echo "Generating protobuf files..."

# 创建输出目录
mkdir -p proto/alisms

# 生成protobuf代码
protoc \
--proto_path=./proto/alisms \
--go_out=./proto/alisms \
--go-grpc_out=./proto/alisms \
./proto/alisms/alisms.proto

if [ $? -eq 0 ]; then
echo "Protobuf files generated successfully"

echo "Building application..."
go build -o alisms_service .

if [ $? -eq 0 ]; then
echo "Build success!"
else
echo "Build failed!"
exit 1
fi
else
echo "Protobuf generation failed!"
exit 1
fi

运行服务

1
2
3
4
5
6
# 1. 生成protobuf代码并构建
chmod +x build.sh
./build.sh

# 2. 运行服务
./alisms_service -config=./config.yaml -port=50051

客户端调用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"context"
"log"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/noahzaozao/alisms_service/proto/alisms"
)

func main() {
// 连接gRPC服务
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()

client := pb.NewAuthServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 发送验证码
resp, err := client.SMSVerficationCode(ctx, &pb.SMSVerficationCodeData{
SignName: "你的签名",
PhoneNumbers: "13800138000",
TemplateCode: "SMS_123456789",
TemplateParam: `{"code":"123456"}`,
})
if err != nil {
log.Fatalf("Failed to send SMS: %v", err)
}
log.Printf("Response: %+v", resp)

// 校验验证码
checkResp, err := client.SMSVerficationCodeCheck(ctx, &pb.SMSVerficationCodeCheckData{
PhoneNumbers: "13800138000",
Vcode: "123456",
})
if err != nil {
log.Fatalf("Failed to check code: %v", err)
}
log.Printf("Check Response: %+v", checkResp)
}

主要改进点

  1. 更新依赖版本

    • Go 1.21+
    • 使用 github.com/redis/go-redis/v9 替代旧版
    • 使用标准的 google.golang.org/grpc 替代 micro框架
    • 使用 gopkg.in/yaml.v3 进行配置解析
  2. 修复代码错误

    • 修正 coinfigconfig
    • 修正 proto 字段名映射问题
    • 完善错误处理,避免使用 panic
  3. 完善功能实现

    • 实现 SMSVerficationCodeCheck 方法
    • 添加验证码生成和缓存逻辑
    • 添加响应解析和错误处理
  4. 代码结构优化

    • 分离服务逻辑到独立文件
    • 改进缓存管理,支持连接池
    • 使用 context 进行超时控制
  5. 添加实用功能

    • 客户端调用示例
    • 更完善的配置说明
    • 改进的构建脚本

注意事项

  1. 安全性

    • 配置文件中的敏感信息应使用环境变量或密钥管理服务
    • 验证码应设置合理的过期时间
    • 添加频率限制防止短信轰炸
  2. 性能优化

    • Redis 连接池配置
    • gRPC 连接复用
    • 异步发送短信(可选)
  3. 监控和日志

    • 添加结构化日志
    • 集成 Prometheus 指标
    • 添加健康检查接口

参考资源

0%