GoFiber 从零系列(一):项目创建&配置文件&gorm-mysql
GoFiber 从零系列(二):热加载/热更新 && 日志系统 && 验证入参
GoFiber 从零系列(五):腾讯云SMS && goredis && 简单的短信登录功能
# GoFiber 从零系列(六):项目部署 && docker && jenkins
自行百度安装redis
点击 (redis token 挤出登录) 文档里有相关说明
安装插件
go get -u github.com/gomodule/redigo/redis
使用goredis
修改配置app.ini文件
[tencent_sms]
SMS_APPID = 腾讯云sms相关
SIGN_NAME = 腾讯云sms相关
TEMP_ID = 腾讯云sms相关
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200
配置加载方式修改
- 通过映射关系,将结构体映射添加配置
修改 /pkg/setting/setting.go
package setting
import (
"time"
"github.com/go-ini/ini"
"github.com/jinpikaFE/go_fiber/pkg/logging"
)
var (
Cfg *ini.File
RunMode string
HTTPPort int
ReadTimeout time.Duration
WriteTimeout time.Duration
PageSize int
JwtSecret string
SecretId string
SecretKey string
CosUrl string
)
// 配置声明 新添加
var RedisSetting = &Redis{}
var SmsStrSetting = &SmsStr{}
func init() {
var err error
Cfg, err = ini.Load("conf/app.ini")
if err != nil {
logging.Fatal("Fail to parse 'conf/app.ini': %v", err)
}
LoadBase()
LoadServer()
LoadApp()
LoadTenCentCos()
// 进行映射 新添加
mapTo("redis", RedisSetting)
mapTo("tencent_sms", SmsStrSetting)
}
// 结构体 新添加
type Redis struct {
Host string
Password string
MaxIdle int
MaxActive int
IdleTimeout time.Duration
}
type SmsStr struct {
SMS_APPID string
SIGN_NAME string
TEMP_ID string
}
func LoadBase() {
RunMode = Cfg.Section("").Key("RUN_MODE").MustString("dev")
}
func LoadServer() {
sec, err := Cfg.GetSection("server")
if err != nil {
logging.Fatal("Fail to get section 'server': %v", err)
}
HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
}
func LoadApp() {
sec, err := Cfg.GetSection("app")
if err != nil {
logging.Fatal("Fail to get section 'app': %v", err)
}
JwtSecret = sec.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)")
PageSize = sec.Key("PAGE_SIZE").MustInt(10)
}
func LoadTenCentCos() {
sec, err := Cfg.GetSection("tencent_cos")
if err != nil {
logging.Fatal("Fail to get section 'tencent_cos': %v", err)
}
SecretId = sec.Key("SECRET_ID").MustString("")
SecretKey = sec.Key("SECRET_KEY").MustString("")
CosUrl = sec.Key("COS_URL").MustString("")
}
// mapTo map section 映射的方法 新添加
func mapTo(section string, v interface{}) {
err := Cfg.Section(section).MapTo(v)
if err != nil {
logging.Error("Cfg.MapTo %s err: %v", section, err)
}
}
映射后的配置使用方式
setting.RedisSetting.MaxIdle // 获取
添加文件/pkg/gredis/redis.go
package gredis
import (
"encoding/json"
"time"
"github.com/gomodule/redigo/redis"
"github.com/jinpikaFE/go_fiber/pkg/logging"
"github.com/jinpikaFE/go_fiber/pkg/setting"
)
var RedisConn *redis.Pool
func Setup() error {
RedisConn = &redis.Pool{
MaxIdle: setting.RedisSetting.MaxIdle,
MaxActive: setting.RedisSetting.MaxActive,
IdleTimeout: setting.RedisSetting.IdleTimeout,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", setting.RedisSetting.Host)
if err != nil {
logging.Error(err)
return nil, err
}
if setting.RedisSetting.Password != "" {
if _, err := c.Do("AUTH", setting.RedisSetting.Password); err != nil {
c.Close()
logging.Error(err)
return nil, err
}
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
if err != nil {
logging.Error(err)
}
return err
},
}
return nil
}
// 设置reids time失效时间 seconds
func Set(key string, data interface{}, time int, isString bool) error {
conn := RedisConn.Get()
defer conn.Close()
value := data
var err error
if !isString {
value, err = json.Marshal(data)
if err != nil {
return err
}
}
_, err = conn.Do("SET", key, value)
if err != nil {
return err
}
_, err = conn.Do("EXPIRE", key, time)
if err != nil {
return err
}
return nil
}
// 判断是否存在
func Exists(key string) bool {
conn := RedisConn.Get()
defer conn.Close()
exists, err := redis.Bool(conn.Do("EXISTS", key))
if err != nil {
return false
}
return exists
}
// 获取redis
func Get(key string) ([]byte, error) {
conn := RedisConn.Get()
defer conn.Close()
reply, err := redis.Bytes(conn.Do("GET", key))
if err != nil {
return nil, err
}
return reply, nil
}
// 删除
func Delete(key string) (bool, error) {
conn := RedisConn.Get()
defer conn.Close()
return redis.Bool(conn.Do("DEL", key))
}
func LikeDeletes(key string) error {
conn := RedisConn.Get()
defer conn.Close()
keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
if err != nil {
return err
}
for _, key := range keys {
_, err = Delete(key)
if err != nil {
return err
}
}
return nil
}
使用腾讯云sms
配置文件添加配置,前面已经添加
添加/pkg/tencent/sms.go
- 缺少插件自行安装
- 腾讯云相关配置 在注释有说明
package tencent
import (
"encoding/json"
"fmt"
"github.com/jinpikaFE/go_fiber/pkg/logging"
"github.com/jinpikaFE/go_fiber/pkg/setting"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" // 引入sms
)
var (
Request *sms.SendSmsRequest
Client *sms.Client
)
func SetupSms() {
/* 必要步骤:
* 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
* 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
* 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
* 以免泄露密钥对危及你的财产安全。
* SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */
credential := common.NewCredential(
fmt.Sprintf("%s", setting.SecretId),
fmt.Sprintf("%s", setting.SecretKey),
)
/* 非必要步骤:
* 实例化一个客户端配置对象,可以指定超时时间等配置 */
cpf := profile.NewClientProfile()
/* SDK默认使用POST方法。
* 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */
cpf.HttpProfile.ReqMethod = "POST"
/* SDK有默认的超时时间,非必要请不要进行调整
* 如有需要请在代码中查阅以获取最新的默认值 */
// cpf.HttpProfile.ReqTimeout = 5
/* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */
cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com"
/* SDK默认用TC3-HMAC-SHA256进行签名,非必要请不要修改这个字段 */
cpf.SignMethod = "HmacSHA1"
/* 实例化要请求产品(以sms为例)的client对象
* 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */
client, _ := sms.NewClient(credential, "ap-guangzhou", cpf)
/* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
* 你可以直接查询SDK源码确定接口有哪些属性可以设置
* 属性可能是基本类型,也可能引用了另一个数据结构
* 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */
request := sms.NewSendSmsRequest()
/* 基本类型的设置:
* SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。
* SDK提供对基本类型的指针引用封装函数
* 帮助链接:
* 短信控制台: https://console.cloud.tencent.com/smsv2
* 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */
/* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
// 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
request.SmsSdkAppId = common.StringPtr(setting.SmsStrSetting.SMS_APPID)
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */
// 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
request.SignName = common.StringPtr(setting.SmsStrSetting.SIGN_NAME)
/* 模板 ID: 必须填写已审核通过的模板 ID */
// 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
request.TemplateId = common.StringPtr(setting.SmsStrSetting.TEMP_ID)
Request = request
Client = client
}
type SendStatusSetStu struct {
Code string
Fee int
Message string
IsoCode string
PhoneNumber string
SerialNo string
}
type SmsResStu struct {
SendStatusSet []SendStatusSetStu
RequestId string
}
// 发送短信
func SendSms(tempParam string, phoneNum string) (*SmsResStu, error) {
/* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空*/
Request.TemplateParamSet = common.StringPtrs([]string{tempParam, "5"})
/* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
* 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
Request.PhoneNumberSet = common.StringPtrs([]string{phoneNum})
// 通过client对象调用想要访问的接口,需要传入请求对象
response, err := Client.SendSms(Request)
// 处理异常
if _, ok := err.(*errors.TencentCloudSDKError); ok {
logging.Error("An API error has returned: %s", err)
return nil, err
}
// 非SDK异常,直接失败。实际代码中可以加入其他的处理。
if err != nil {
logging.Error(err)
return nil, err
}
b, _ := json.Marshal(response.Response)
result := &SmsResStu{}
if errJson := json.Unmarshal(b, &result); errJson != nil {
return nil, errJson
}
return result, nil
}
添加获取验证码路由 /controllers/login.go 中添加函数
func GetCaptcha(c *fiber.Ctx) error {
appF := app.Fiber{C: c}
// 短信验证码发送
loginMobile := &models.LoginMobile{}
captcha := fmt.Sprintf("%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
if err := c.BodyParser(loginMobile); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "参数解析错误", nil)
}
if !untils.VerifyMobileFormat(loginMobile.Mobile) {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "请输入正确的手机号", nil)
}
result, smsErr := tencent.SendSms(captcha, loginMobile.Mobile)
if smsErr != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "短信发送错误", smsErr)
}
if result.SendStatusSet[0].Code == "Ok" {
// 暂存redis 失效时间5分钟,即300s
redisErr := gredis.Set(loginMobile.Mobile, captcha, 300, true)
if redisErr != nil {
logging.Error(redisErr)
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "redis错误", redisErr)
}
return appF.Response(fiber.StatusOK, fiber.StatusOK, "短信发送成功", result)
}
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "短信发送失败", result)
}
添加路由
// 放在apiv1.Use(jwt.Jwt)上方,不需要jwt拦截
apiv1.Post("/captcha", controller.GetCaptcha)
登录功能修改
修改 /models/login.go
package models
import (
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/jinpikaFE/go_fiber/pkg/logging"
"github.com/jinpikaFE/go_fiber/pkg/untils"
)
type LoginAccount struct {
Username string `validate:"required" query:"username" json:"username" xml:"username" form:"username"`
Password string `validate:"required" query:"password" json:"password" xml:"password" form:"password"`
}
type LoginMobile struct {
Mobile string `validate:"required" query:"mobile" json:"mobile" xml:"mobile" form:"mobile"`
Captcha string `validate:"required" query:"captcha" json:"captcha" xml:"captcha" form:"captcha"`
}
type LoginWx struct {
Appid string `validate:"required" query:"appid" json:"appid" xml:"appid" form:"appid"`
Appsecret string `validate:"required" query:"appsecret" json:"appsecret" xml:"appsecret" form:"appsecret"`
Code string `validate:"required" query:"code" json:"code" xml:"code" form:"code"`
}
type Type struct {
LoginType string `validate:"required,oneof=1 2 3" query:"loginType" json:"loginType" xml:"loginType" form:"loginType"`
}
type Login struct {
LoginAccount
LoginWx
Type
}
func GetToken(login *Login, user *User) string {
claims := jwt.MapClaims{
"username": login.Username,
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// logging.Error(user.Openid, user.Openid == nil)
if user.Openid == nil {
if user.Mobile == nil {
if login.Username != *user.Username || untils.GetSha256(login.Password) != user.Password {
return ""
}
} else {
claims = jwt.MapClaims{
"openid": user.Mobile,
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
}
} else {
claims = jwt.MapClaims{
"openid": user.Openid,
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString([]byte("secret"))
if err != nil {
logging.Error(err)
return ""
}
return t
}
/controllers/login.go 文件修改
- 三种登录方式 账号密码、微信、手机号 自行取舍
package controller
import (
"encoding/json"
"fmt"
"math/rand"
"net/url"
"time"
"github.com/gofiber/fiber/v2"
"github.com/jinpikaFE/go_fiber/models"
"github.com/jinpikaFE/go_fiber/pkg/app"
"github.com/jinpikaFE/go_fiber/pkg/gredis"
"github.com/jinpikaFE/go_fiber/pkg/logging"
"github.com/jinpikaFE/go_fiber/pkg/tencent"
"github.com/jinpikaFE/go_fiber/pkg/untils"
"github.com/jinpikaFE/go_fiber/pkg/valodates"
"github.com/vicanso/go-axios"
)
// ResponseHTTP represents response body of this API
type ResponseHTTP struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Message string `json:"message"`
}
func Login(c *fiber.Ctx) error {
appF := app.Fiber{C: c}
types := &models.Type{}
if err := c.BodyParser(types); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "参数解析错误", nil)
}
// 账号登录
if types.LoginType == "1" {
loginAccount := &models.LoginAccount{}
if err := c.BodyParser(loginAccount); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "参数解析错误", nil)
}
// 入参验证
errors := valodates.ValidateStruct(*loginAccount)
if errors != nil {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "检验参数错误", errors)
}
userSt := &models.User{}
userSt.Username = &loginAccount.Username
res, errs := models.GetUser(userSt)
if errs != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "查询失败", errs)
}
if !(res.ID > 0) {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "用户不存在", nil)
}
login := &models.Login{}
login.Username = loginAccount.Username
login.Password = loginAccount.Password
token := models.GetToken(login, res)
redisErr := gredis.Set("token", token, 300, true)
if redisErr != nil {
logging.Error(redisErr)
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "redis错误", redisErr)
}
if token == "" {
return appF.Response(fiber.StatusUnauthorized, fiber.StatusUnauthorized, "账户或者密码错误", nil)
}
loginres := map[string]interface{}{"token": token, "username": loginAccount.Username}
return appF.Response(fiber.StatusOK, fiber.StatusOK, "SUCCESS", loginres)
}
// 微信登录
if types.LoginType == "2" {
loginWx := &models.LoginWx{}
if err := c.BodyParser(loginWx); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "参数解析错误", nil)
}
// 入参验证
errors := valodates.ValidateStruct(*loginWx)
if errors != nil {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "检验参数错误", errors)
}
// 使用axios进行请求
queryParams := url.Values{}
queryParams.Add("appid", loginWx.Appid)
queryParams.Add("secret", loginWx.Appsecret)
queryParams.Add("js_code", loginWx.Code)
queryParams.Add("grant_type", "authorization_code")
axiosConfig := &axios.InstanceConfig{}
axiosConfig.BaseURL = "https://api.weixin.qq.com"
resp, err := untils.Request(axiosConfig).Get("/sns/jscode2session", queryParams)
if err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "获取openid失败", err)
}
result := make(map[string]interface{})
if errJson := json.Unmarshal(resp.Data, &result); errJson != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "解析code2Session数据失败", errJson)
}
logging.Error(result, result["openid"])
// body, err := ioutil.ReadAll(response.Body)
// if err == nil {
// logging.Error(string(body))
// }
userSt := &models.User{}
openid := result["openid"].(string)
userSt.Openid = &openid
resUser, errs := models.GetUser(userSt)
if errs != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "查询失败", errs)
}
userWx := &models.User{}
userWx.Openid = &openid
if !(resUser.ID > 0) {
// 不存在就创建
nickName := "微信用户"
userWx.NickName = &nickName
if err := models.AddUser(*userWx); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "添加失败", err)
}
}
// 登录
token := models.GetToken(&models.Login{}, userWx)
redisErr := gredis.Set("token", token, 300, true)
if redisErr != nil {
logging.Error(redisErr)
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "redis错误", redisErr)
}
loginres := map[string]interface{}{"token": token, "openid": openid}
return appF.Response(fiber.StatusOK, fiber.StatusOK, "SUCCESS", loginres)
}
// 手机号登录
if types.LoginType == "3" {
loginMobile := &models.LoginMobile{}
if err := c.BodyParser(loginMobile); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "参数解析错误", nil)
}
// 入参验证
errors := valodates.ValidateStruct(*loginMobile)
if errors != nil {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "检验参数错误", errors)
}
userSt := &models.User{}
userSt.Mobile = &loginMobile.Mobile
resUser, errs := models.GetUser(userSt)
if errs != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "查询失败", errs)
}
userMobile := &models.User{}
userMobile.Mobile = &loginMobile.Mobile
if !gredis.Exists(loginMobile.Mobile) {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "验证码过期请重新获取", nil)
}
// 取出reids中的缓存值进行比较
reply, replyErr := gredis.Get(loginMobile.Mobile)
if replyErr != nil {
logging.Error(replyErr)
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "redis错误", replyErr)
}
if loginMobile.Captcha == string(reply) {
if !(resUser.ID > 0) {
// 不存在就创建
userMobile.NickName = &loginMobile.Mobile
if err := models.AddUser(*userMobile); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "添加失败", err)
}
}
// 登录
token := models.GetToken(&models.Login{}, userMobile)
redisErr := gredis.Set("token", token, 300, true)
if redisErr != nil {
logging.Error(redisErr)
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "redis错误", redisErr)
}
loginres := map[string]interface{}{"token": token, "mobile": &loginMobile.Mobile}
return appF.Response(fiber.StatusOK, fiber.StatusOK, "SUCCESS", loginres)
}
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "验证码错误", nil)
}
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "未知登录类型", nil)
}
func GetCaptcha(c *fiber.Ctx) error {
appF := app.Fiber{C: c}
// 短信验证码发送
loginMobile := &models.LoginMobile{}
captcha := fmt.Sprintf("%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
if err := c.BodyParser(loginMobile); err != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "参数解析错误", nil)
}
if !untils.VerifyMobileFormat(loginMobile.Mobile) {
return appF.Response(fiber.StatusBadRequest, fiber.StatusBadRequest, "请输入正确的手机号", nil)
}
result, smsErr := tencent.SendSms(captcha, loginMobile.Mobile)
if smsErr != nil {
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "短信发送错误", smsErr)
}
if result.SendStatusSet[0].Code == "Ok" {
redisErr := gredis.Set(loginMobile.Mobile, captcha, 300, true)
if redisErr != nil {
logging.Error(redisErr)
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "redis错误", redisErr)
}
return appF.Response(fiber.StatusOK, fiber.StatusOK, "短信发送成功", result)
}
return appF.Response(fiber.StatusInternalServerError, fiber.StatusInternalServerError, "短信发送失败", result)
}