书接上回,上一回我们按照“低耦合高内聚”的组织架构方针对项目的整体结构进行了优化,本回将会继续编写业务,那就是用户的登录逻辑,将之前用户管理模块中添加的用户账号进行账号和密码的校验,校验通过后留存当前登录用户的信息,过程中使用图形验证码强制进行人机交互,防止账号的密码被暴力破解。
登录逻辑
首先在逻辑层handler包中,创建用户模块文件user.go:
package handler
import (
"github.com/kataras/iris/v12"
)
//用户登录模板
func User_signin(ctx iris.Context) {
ctx.View("/signin.html")
}
这里通过上下文管理器结构体ctx渲染用户登录模板。
随后,在views模板目录中,添加用户登录模板signin.html:
<div class="row justify-content-center">
<div class="col-md-10 col-lg-8 article">
<div class="article-body page-body mx-auto" style="max-width: 400px;">
<h1 class="text-center mb-4">登 录</h1>
<div class="socialaccount_ballot">
<div class="text-center mb-3">
<ul class="list-unstyled">
<li><a title="GitHub" class="socialaccount_provider github btn btn-secondary btn-lg w-100" href="JavaScript:void(0)">Connect With <strong>Meta Mask</strong></a></li>
</ul>
</div>
<div class="text-center text-muted my-3">— or —</div>
</div>
<div class="form-group">
<div id="div_id_login" class="form-group">
<label for="id_login" class="requiredField"> Username<span class="asteriskField">*</span></label>
<div class=""><input type="text" placeholder="请输入用户名" v-model="username" autofocus="" class="textinput textInput form-control"></div>
</div>
</div>
<div class="form-group">
<div id="div_id_password" class="form-group">
<label for="id_password" class="requiredField"> Password<span class="asteriskField">*</span></label>
<div class="">
<input type="password" placeholder="请输入密码" v-model="password" minlength="8" maxlength="99" class="textinput textInput form-control">
</div>
</div>
</div>
<div class="text-center"><button class="btn btn-primary btn-lg text-wrap px-5 mt-2 w-100" name="jsSubmitButton">点击登录</button></div></div>
</div>
</div>
这里的表单中有两个字段,分别是Username和Password。
随后通过Vue.js进行数据双向绑定逻辑:
const App = {
data() {
return {
username: "",
password: "",
};
},
created: function() {
},
methods: {
},
};
const app = Vue.createApp(App);
app.config.globalProperties.axios = axios;
app.mount("#app");
随后添加登录模板路由逻辑:
app.Get("/signin/", handler.User_signin)
访问 http://localhost:5000/signin/ 如图所示:
随后编写登录后台业务user.go:
//登录动作
func Signin(ctx iris.Context) {
db := database.Db()
defer func() {
_ = db.Close()
}()
Username := ctx.PostValue("username")
Password := ctx.PostValue("password")
user := &model.User{}
db.Where(&model.User{Username: Username, Password: mytool.Make_password((Password))}).First(&user)
ret := make(map[string]interface{}, 0)
if user.ID == 0 {
ret["errcode"] = 1
ret["msg"] = "登录失败,账号或者密码错误"
ctx.JSON(ret)
return
}
ret["errcode"] = 0
ret["msg"] = "登录成功"
ret["username"] = user.Username
ctx.JSON(ret)
}
这里通过db.Where函数进行用户名和密码的检索,注意密码需要通过mytool.Make\_password函数转换为密文。
随后通过判断主键ID的值来判定账号的合法性,这里注意返回值字典的值通过接口(interface)声明初始化,如此字典的value就可以兼容不同的数据类型。
在和前端联调之前,编写测试脚本tests.go:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
func main() {
formValues := url.Values{}
formValues.Set("username", "123")
formValues.Set("password", "123")
formDataStr := formValues.Encode()
formDataBytes := []byte(formDataStr)
formBytesReader := bytes.NewReader(formDataBytes)
resp, err := http.Post("http://localhost:5000/signin/", "application/x-www-form-urlencoded;charset=utf-8", formBytesReader)
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
这里模拟表单数据,向后端Iris发起Post请求,程序返回:
{"errcode":0,"msg":"登录成功","username":"123"}
登录成功后,返回当前登录用户账号。
反之:
{"errcode":1,"msg":"登录失败,账号或者密码错误"}
返回错误码以及提示信息。
接着前端编写异步请求逻辑:
//登录请求
signin:function(){
this.myaxios("http://localhost:5000/signin/","post",{"username":this.username,"password":this.password}).then(data => {
console.log(data)
alert(data.msg);
});
}
如图所示:
至此,登录逻辑就完成了。
图像验证码
图形验证码的主要作用是强制当前用户进行人机交互,藉此来抵御人工智能的自动化攻击,可以防止恶意破解密码、刷票、论坛灌水,有效防止黑客对某一个特定注册用户用特定程序暴力破解进行不断的登录尝试。
首先在项目内安装三方的验证码校验包:
go get -u github.com/dchest/captcha
随后在工具类中添加验证码生成逻辑mytool.go:
package mytool
import (
"crypto/md5"
"fmt"
"io"
"github.com/dchest/captcha"
"github.com/kataras/iris/v12"
)
const (
StdWidth = 80
StdHeight = 40
)
func GetCaptchaId(ctx iris.Context) {
m := make(map[string]interface{}, 0)
m["errcode"] = 0
m["msg"] = "获取成功"
m["captchaId"] = captcha.NewLen(4)
ctx.JSON(m)
return
}
这里定义常量StdWidth和StdHeight,意为图片宽和高,然后通过captcha.NewLen(4)函数获取验证码的标识,这里NewLen(4)标识生成四位验证码,如果不需要定制化操作,也可以使用captcha.New()返回默认长度的验证码。
接着添加路由:
app.Post("/captcha/", mytool.GetCaptchaId)
继续使用tests.go脚本进行测试:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
func main() {
formValues := url.Values{}
formValues.Set("username", "123")
formValues.Set("password", "1243")
formDataStr := formValues.Encode()
formDataBytes := []byte(formDataStr)
formBytesReader := bytes.NewReader(formDataBytes)
resp, err := http.Post("http://localhost:5000/captcha/", "application/x-www-form-urlencoded;charset=utf-8", formBytesReader)
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
程序返回:
{"captchaId":"qGlf8P9RJtJJzc3Q1v4z","errcode":0,"msg":"获取成功"}
这里就获取到captchaId的值为qGlf8P9RJtJJzc3Q1v4z,随后编写逻辑,使用captchaId渲染具体的图片缓冲区:
func GetCaptchaImg(ctx iris.Context) {
captcha.Server(StdWidth, StdHeight).
ServeHTTP(ctx.ResponseWriter(), ctx.Request())
}
这里通过captcha.Server将图片渲染出来,配置路由:
app.Get("/captcha/*/", mytool.GetCaptchaImg)
注意必须通过Get方式进行请求,因为需要浏览器对图片进行访问,如图所示:
随后,编写前端逻辑,首先登录页面初始化时,生成验证码id:
//获取验证码标识
get_cid:function(){
this.myaxios("http://localhost:5000/captcha/","post").then(data => {
console.log(data)
this.cid = data.captchaId;
});
}
随后添加验证码字段,将验证码展示在表单中:
<div class="form-group">
<div id="div_id_password" class="form-group">
<label for="id_password" class="requiredField"> 验证码<span class="asteriskField">
<img v-if="cid" :src="'http://localhost:5000/captcha/'+cid+'.png'" />
</span></label>
<div class="">
<input type="text" placeholder="请输入验证码" v-model="code" minlength="8" maxlength="99" class="textinput textInput form-control">
</div>
</div>
</div>
这里通过拼接将获取到的验证码id绑定在图片标签中,如图所示:
接着,改写用户登录逻辑user.go:
ret := make(map[string]interface{}, 0)
cid := ctx.PostValue("cid")
code := ctx.PostValue("code")
if captcha.VerifyString(cid, code) == false {
ret["errcode"] = 2
ret["msg"] = "登录失败,验证码错误"
ctx.JSON(ret)
return
}
这里增加两个参数,验证码标识以及用户输入的表单验证码,通过captcha.VerifyString函数进行比对,如果二者吻合那么证明用户输入正确,否则输入错误。
同样地,前端应对增加表单请求字段:
//登录请求
signin:function(){
this.myaxios("http://localhost:5000/signin/","post",{"username":this.username,"password":this.password,"cid":this.cid,"code":this.code}).then(data => {
console.log(data)
alert(data.msg);
});
}
如图所示:
至此,集成图像验证码的登录逻辑就完成了。
结语
每一次captcha.NewLen(4)返回的验证码标识都是唯一的,所以也就避免了多个账号并发登录所带来的覆盖问题,同时验证码本体和其生命周期都存储在Iris服务的内存中,既灵活又方便。登录成功以后,下一步就面临用户信息留存方案的选择。该项目已开源在Github:https://github.com/zcxey2911/IrisBlog ,与君共觞,和君共勉。