背景:
初学golang,想和同学前后端分离做个小APP,我负责后端。
需要用到短信服务来做验证码登陆,选择了MobTech的SMS服务(一个月免费1W条,MOB牛逼!)
问题:
以前做的短信登陆逻辑如图:
MOB文档中给出了两种策略:
我感觉第一种后端不涉及验证过程的策略可能存在安全隐患,因为校验短信验证码的动作发生在前端,之后由开发者在前端回调后台的业务接口诸如登陆和注册。
那么在调用这一业务接口时,肯定需要前端引入某种加密策略,来确保对该接口的请求一定是前端经过完整的验证码流程后发起的,只有这样才能保证用户身份的正确。
在这种情况下,如果前端的加密策略泄露,可能导致不法分子伪装其他用户的身份进行登陆或其他权限相关操作,就糟糕了。
以上仅是我这个初学者结合以往经验的一点点猜测,既然这种模式存在于官方文档中,那肯定有足够安全的实现策略,不过我现在还不知道: (
我暂时就先用自己理解上觉得更安全、更顺手的第二种策略来实现。
在此先附上MobTech的SMS服务接入文档:
Mob文档中心 / SMSSDK
注册账号并创建应用、企业认证过程这里就不提了。
前端根据不同实现可以参考上述文档快速接入SDK。
后端如果要采用第二种策略,需要进行以下操作:
- 填入部署后端应用的服务器的ip
- 对Mob提供的验证码校验接口进行封装
package sms
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
//短信服务功能封装
//默认区号为中国(+86)
const DEFAULT_ZONE = "86"
//MOB提供的验证码校验接口
const VERIFY_URL = "https://webapi.sms.mob.com/sms/verify"
const APP_KEY = "xxxxxxxx"
const APP_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxxx"
//校验收到的验证码
func VerifyCode(phone, code string) (bool, error) {
//********************************************************
//* 通过json的方式进行post请求的传参 在这里是不适用的!!! *
//********************************************************
//contentType := "application/json"
//data := mapToJSON(&map[string]interface{}{
// "phone": phone,
// "code": code,
// "appkey": "31db8a6381fb0",
// "zone": DEFAULT_ZONE,
//})
//resp, err := http.Post(VERIFY_URL, contentType, strings.NewReader(data))
urlValues := url.Values{
"phone": {phone},
"code": {code},
"appkey": {APP_KEY},
"zone": {DEFAULT_ZONE},
}
reqBody:= urlValues.Encode()
resp, err := http.Post(VERIFY_URL, "text/html",strings.NewReader(reqBody))
if err != nil {
fmt.Printf("post failed, err:%v\n", err)
return false, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return false, err
}
//json序列化成map 解析MOB验证接口的返回值
var tempMap map[string]interface{}
err = json.Unmarshal([]byte(b), &tempMap)
if err != nil {
fmt.Printf("parse resp failed, err:%v\n", err)
return false, err
}
fmt.Println("接口返回的状态码是:",tempMap["status"])
fmt.Println("接口返回的message是:",tempMap["error"])
//根据接口返回状态码 判断验证码结果
switch int(tempMap["status"].(float64)) {
case 200:
return true, nil
default:
return false,errors.New(tempMap["error"].(string))
}
}
//map转json
func mapToJSON(tempMap *map[string]interface{}) string {
data, err := json.Marshal(tempMap)
if err != nil {
panic(err)
}
return string(data)
}
注意,这里有一个坑!
在golang里,post请求传参有三种常见的方式:
-
conten-type : text/html 这种实际上是把参数拼接到url上)
-
conten-type : application/x-www-form-urlencoded
-
conten-type : application/json
对应到golang代码中有以下三种写法:
-
urlValues := url.Values{ "name":{"zhaofan"}, "age":{"23"}, } reqBody:= urlValues.Encode() resp, _ := http.Post("http://xxxxxx", "text/html",strings.NewReader(reqBody)) body,_:= ioutil.ReadAll(resp.Body) fmt.Println(string(body))
-
urlValues := url.Values{} urlValues.Add("name","zhaofan") urlValues.Add("age","22") resp, _ := http.PostForm("http://httpbin.org/post",urlValues) body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body))
-
data := mapToJSON(&map[string]interface{}{ "phone": phone, "code": code, "appkey": "31db8a6381fb0", "zone": DEFAULT_ZONE, }) resp, _:= http.Post(VERIFY_URL, contentType, strings.NewReader(data)) body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) //map转json func mapToJSON(tempMap *map[string]interface{}) string { data, err := json.Marshal(tempMap) if err != nil { panic(err) } return string(data) }
当我使用第三种JSON传参的时候,MOB的验证接口一直给我报405 AppKey为空
。
改用第一种、第二种传参方式之后都能够正常调用。
估计是mob提供的这个接口在解析参数时不支持json传参。
这也体现出了gin框架在http请求参数解析时的强大之处:
只需要在声明接收参数的结构体时,给对应字段添加几个“注解”
(暂时不知道这个小引号的学名,先这么叫它^^)
再结合func (c *Context) ShouldBind(obj interface{}) error
就能完美实现httpRequest的参数获取。
//接受参数的实体
type Verify struct {
Phone string `form:"phone" json:"phone" binding:"required"`
Code string `form:"code" json:"code" binding:"required"`
}
var verify Verify //用于装载参数的结构体
if err := c.ShouldBind(&verify); err == nil {...}//读取参数后的业务逻辑
- 给前端提供校验接口,可以整合自己的登陆或注册等等业务逻辑
//接受参数的实体
type Verify struct {
Phone string `form:"phone" json:"phone" binding:"required"`
Code string `form:"code" json:"code" binding:"required"`
}
//这是一个功能接口 POST类型 路由为 '/phone' 用于手机或者注册
func phone(c *gin.Context) {
//获取请求参数...业务逻辑
var verify Verify
if err := c.ShouldBind(&verify); err == nil {
fmt.Printf("verify info:%#v\n", verify)
//1、校验验证码
isCorrect,_ := sms.VerifyCode(verify.Phone,verify.Code)
//验证码正确
if isCorrect {
//2、校验手机号是否存在 不存在则先注册再返回token 存在则直接返回token
byPhone, _ := service.GetUserByPhone(verify.Phone)
if byPhone==(bean.User{}) {
//用户不存在
c.JSON(http.StatusOK,response.Ok("新用户注册成功",nil))
}else{
//用户已存在
c.JSON(http.StatusOK,response.Ok("登陆成功",nil))
}
}else{
//验证码不正确
c.JSON(http.StatusOK,response.Fail("验证码错误"))
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}