【Go + Nuxt.js 搭建一个 BBS 系统】8. 搭建用户模块
点击下面链接购买后可获取本课程完整源码,同时提供高大上的在线IDE开发环境,边看教程边动手。
- 购买地址:https://www.shiyanlou.com/courses/1436
- 购买九折优惠邀请码:
ZHwfIjb1
作者简介
大猫猫,互联网公司老码农、不折腾不舒服斯基,多年千万日活服务端研发和架构经验。关注公众号查看更多技术干货:
实验介绍
实验内容
一个论坛系统,用户模块是组基础的,最必不可少的,所以我们先从用户模块开始,一步步的完善论坛系统。接下来我们接着上一个实验继续完善用户模块功能。
示例项目中用到的一些工具类来自于这个项目:https://github.com/mlogclub/simple 感兴趣的可以看下。
知识点
- 将gorm、iris、nuxt.js配合起来使用,完成一个完整的功能模块。
服务端功能开发
服务端开发我们会分为model/service/controller,这样让项目结构清晰明了。这三个模块分别作用如下:
- model:用来定义结构体,结构体和数据库表结构一一对应
- service:用来编写业务逻辑,读写数据库
- controller:对外提供数据接口
main.go
首先我们来看server/main.go中的代码,他是我们整个程序的入口,在main.go中我们将初始化数据库,配置iris路由,他的完整代码如下:
package main
import (
"github.com/jinzhu/gorm"
"github.com/kataras/iris"
"github.com/kataras/iris/mvc"
"github.com/iris-contrib/middleware/cors"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var db *gorm.DB
func main() {
initDB()
app := iris.New()
// 跨域配置
app.Use(cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts.
AllowCredentials: true,
MaxAge: 600,
AllowedMethods: []string{iris.MethodGet, iris.MethodPost, iris.MethodOptions, iris.MethodHead, iris.MethodDelete, iris.MethodPut},
AllowedHeaders: []string{"*"},
}))
app.AllowMethods(iris.MethodOptions)
mvc.Configure(app.Party("/api/user"), func(mvcApp *mvc.Application) {
mvcApp.Handle(new(UserController))
})
_ = app.Run(iris.Addr(":8081"), iris.WithoutServerError(iris.ErrServerClosed))
}
// 初始化数据库链接
func initDB() {
var err error
db, err = gorm.Open("mysql", "root@tcp(localhost:3306)/test_db?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
db.LogMode(true)
}
结构体(user_model)
新建/server/user_model.go文件,在该文件中定义我们用户结构体。用户相关的有两个结构体,分别如下:
- 用户(user):用于保存用户资料
- 用户授权令牌(user_token):用户的登录标识,通过令牌能获取到当前登录用户
/server/user_model.go完整代码如下:
package main
import (
"time"
)
const (
UserRoleAdmin = "管理员"
UserRoleNormal = "普通用户"
)
// 用户表
type User struct {
Id int64 `gorm:"PRIMARY_KEY;AUTO_INCREMENT" json:"id"` // 编号
Username string `gorm:"size:32;not null;unique" json:"username"` // 用户名,添加唯一标识
Password string `gorm:"size:128;not null" json:"password"` // 密码
Nickname string `gorm:"size:32;not null" json:"nickname"` // 昵称
Role string `gorm:"size:32;not null" json:"role"` // 用户角色,管理员、普通用户
CreateTime time.Time `gorm:"not null" json:"createTime"` // 创建时间
}
// 用户授权令牌,用户的登录标识
type UserToken struct {
Id int64 `gorm:"PRIMARY_KEY;AUTO_INCREMENT" json:"id"` // 编号
UserId int64 `gorm:"not null" json:"userId"` // 用户编号
Token string `gorm:"size:32;unique;not null" json:"token" form:"token"` // 令牌
ExpiredAt int64 `gorm:"not null" json:"expiredAt" form:"expiredAt"` // 过期时间戳
CreateTime time.Time `gorm:"not null" json:"createTime"` // 创建时间
}
用户相关的结构体定义完成之后,需要将他们放入gorm的AutoMigrate,这样在启动服务的时候gorm会自动创建用户相关的表,打开server/main.go,修改gorm配置如下:
err = db.AutoMigrate(&User{}, &UserToken{}).Error
if err != nil {
panic(err)
}
业务服务(user_service)
UserService是我们的业务代码,我们将在这里完成用户相关的核心逻辑、并操作数据库完成数据数据的读写操作。主要实现功能如下:
- 用户注册
- 用户登录
- 获取当前登录用户
接下来我们新增文件server/user_service.go,该文件完整代码如下:
package main
import (
"time"
"github.com/kataras/iris/context"
"github.com/mlogclub/simple"
)
var UserService = &userService{}
type userService struct {
}
// 创建用户
func (userService) Create(username, password, nickname, role string) error {
return db.Create(&User{
Username: username,
Password: password,
Nickname: nickname,
CreateTime: time.Now(),
}).Error
}
// 根据id查询用户
func (userService) Get(id int64) *User {
ret := &User{}
if err := db.First(ret, "id = ?", id).Error; err != nil {
return nil
}
return ret
}
// 根据用户名查找
func (userService) GetByUsername(username string) *User {
ret := &User{}
if err := db.Take(ret, "username = ?", username).Error; err != nil {
return nil
}
return ret
}
// 用户登录
func (userService) Login(username, password string) (*User, string) {
// 查找用户
user := UserService.GetByUsername(username)
if user == nil {
return nil, ""
}
// 验证密码
passwordValidated := simple.ValidatePassword(user.Password, password)
if !passwordValidated {
return nil, ""
}
// 生成授权令牌
token := simple.Uuid()
expiredAt := time.Now().Add(time.Hour * 24 * 7) // 7天后过期
db.Create(&UserToken{
UserId: user.Id,
Token: token,
ExpiredAt: simple.Timestamp(expiredAt),
CreateTime: time.Now(),
})
return user, token
}
// 获取当前登录用户
func (u userService) GetCurrent(ctx context.Context) *User {
token := u.GetUserToken(ctx)
if len(token) == 0 {
return nil
}
userToken := &UserToken{}
if err := db.Take(userToken, "token = ?", token).Error; err != nil {
return nil
}
return u.Get(userToken.UserId)
}
// 从请求体中获取UserToken
func (userService) GetUserToken(ctx context.Context) string {
userToken := ctx.FormValue("userToken")
if len(userToken) > 0 {
return userToken
}
return ctx.GetHeader("X-User-Token")
}
控制器(user_controller)
controller中我们对外提供数据接口,我们所有的接口都返回JsonResult对象,该对象最终会被序列化成JSON返回,下面通过JsonResult的代码+注释来了解下JsonResult中每个字段的含义。
type JsonResult struct {
ErrorCode int `json:"errorCode"` // 错误码,当接口发生错误的时候可以指定错误码
Message string `json:"message"` // 错误消息,当接口发生错误的时候返回的错误消息
Data interface{} `json:"data"` // 业务数据
Success bool `json:"success"` // 接口调用是否成功
}
接下来我们定义UserController,创建文件server/user_controller.go代码如下:
package main
import (
"github.com/kataras/iris/context"
"github.com/mlogclub/simple"
)
// controller
type UserController struct {
Ctx context.Context
}
// 用户注册
func (this *UserController) PostAdd() *simple.JsonResult {
var (
username = this.Ctx.FormValue("username")
password = this.Ctx.FormValue("password")
rePassword = this.Ctx.FormValue("rePassword")
nickname = this.Ctx.FormValue("nickname")
)
// 数据校验
if len(username) == 0 || len(password) == 0 || len(nickname) == 0 {
return simple.JsonErrorMsg("请认真填写用户名、密码、昵称")
}
if password != rePassword {
return simple.JsonErrorMsg("两次填写密码不同,请检查后重新填写")
}
// 密码加密
password = simple.EncodePassword(password)
// 判断用户名是否存在
tmp := UserService.GetByUsername(username)
if tmp != nil {
return simple.JsonErrorMsg("用户名【" + username + "】已经存在")
}
// 执行注册操作
err := UserService.Create(username, password, nickname, UserRoleNormal)
if err != nil {
return simple.JsonErrorMsg(err.Error())
}
return simple.JsonSuccess()
}
// 用户登录
func (this *UserController) PostLogin() *simple.JsonResult {
var (
username = this.Ctx.FormValue("username")
password = this.Ctx.FormValue("password")
)
user, token := UserService.Login(username, password)
if user == nil {
return simple.JsonErrorMsg("用户名密码错误")
}
// 登录成功返回用户信息和授权令牌
return simple.NewRspBuilder(user).Put("token", token).JsonResult()
}
// 获取当前登录用户
func (this *UserController) GetCurrent() *simple.JsonResult {
user := UserService.GetCurrent(this.Ctx)
if user != nil {
return simple.JsonData(user)
}
return simple.JsonSuccess()
}
在UserCotroller中我们定义了三个接口,分别为:用户注册、用户登录、获取当前登陆用户。接下来我们将UserController配置到iris路由中,打开server/main.go新增如下代码:
mvc.Configure(app.Party("/api/user"), func(mvcApp *mvc.Application) {
mvcApp.Handle(new(UserController))
})
如果不明白如何使用iris的同学,请认真温习前面实验中关于iris使用方法的讲解
至此我们就完成了用户模块的服务端开发。然后在server目录下执行一下命令来启动接口服务:
➜ server git:(master) ✗ go run *.go
Now listening on: http://localhost:8081
Application started. Press CMD+C to shut down.
Nuxt.js页面功能开发
之前的章节中已经讲解了如何使用Nuxt.js,如果你认真阅读了之前的章节,那么你就能流畅的使用Nuxt.js完成日常的开发工作。接下来就让我们开始实战吧~
Axios插件
在创建Nuxt.js项目的时候我们安装了Axios插件,这里我们需要对Axios插件功能做一个简单的封装。封装主要为了实现以下两个功能:
- 让
Axios每次请求的时候自动对JsonObject参数进行编码。 - 处理统一的返回状态码和返回结果。
这里需要添加一个第三方依赖qs,我们执行以下命令来添加qs依赖:
npm install qs --save
然后我们创建文件site/plugins/axios.js,完整内容如下:
import qs from 'qs'
export default function ({ $axios, $toast, app }) {
$axios.onRequest((config) => {
config.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
config.transformRequest = [
function (data) {
if (process.client && data instanceof FormData) { // 如果是FormData就不转换
return data
}
data = qs.stringify(data)
return data
}
]
})
$axios.onResponse((response) => {
if (response.status !== 200) {
return Promise.reject(response)
}
const jsonResult = response.data
if (jsonResult.success) {
return Promise.resolve(jsonResult.data)
} else {
return Promise.reject(jsonResult)
}
})
}
接下来修改nuxt.config.js文件,修改该文件中的plugins,如下:
/*
** Plugins to load before mounting the App
*/
plugins: [
'~/plugins/axios'
],
用户注册页面
新增文件site/pages/user/reg.vue,该文件完整代码如下:
<template>
<section>
<my-nav/>
<section class="section">
<div class="container">
<div class="field">
<label class="label">用户名</label>
<div class="control">
<input v-model="form.username" class="input" type="text" placeholder="请输入用户名">
</div>
</div>
<div class="field">
<label class="label">密码</label>
<div class="control">
<input v-model="form.password" class="input" type="password" placeholder="请输入密码">
</div>
</div>
<div class="field">
<label class="label">重复密码</label>
<div class="control">
<input v-model="form.rePassword" class="input" type="password" placeholder="请再次输入密码">
</div>
</div>
<div class="field">
<label class="label">昵称</label>
<div class="control">
<input v-model="form.nickname" class="input" type="text" placeholder="请输入昵称">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link" @click="postAdd">注册</button>
</div>
</div>
</div>
</section>
<my-footer/>
</section>
</template>
<script>
import MyNav from '~/components/MyNav'
import MyFooter from '~/components/MyFooter'
export default {
components: {
MyNav, MyFooter
},
head () {
return {
title: '用户注册'
}
},
data () {
return {
form: {
username: '',
password: '',
rePassword: '',
nickname: ''
}
}
},
methods: {
// 提交注册
async postAdd () {
try {
const resp = await this.$axios.post('/api/user/add', this.form)
console.log(resp)
this.$router.push('/user/login') // 注册成功跳转到登陆页
} catch (err) {
alert(err.message || err)
}
}
}
}
</script>
<style>
.container {
min-height: 300px;
}
</style>
然后我们在site目录下执行命令npm run dev来启动前端页面服务,服务启动成功之后,访问路径/user/reg来查看页面效果,效果图如下:

接下来我们就可以完成用户的注册了。
用户登录页面
完成用户注册,接下来我们来完善用户登录功能。新建文件site/pages/user/login.vue,完整代码如下:
<template>
<section>
<my-nav/>
<section class="section">
<div class="container">
<div class="field">
<label class="label">用户名</label>
<div class="control">
<input v-model="form.username" class="input" type="text" placeholder="请输入用户名">
</div>
</div>
<div class="field">
<label class="label">密码</label>
<div class="control">
<input v-model="form.password" class="input" type="password" placeholder="请输入密码">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link" @click="login">登录</button>
</div>
</div>
</div>
</section>
<my-footer/>
</section>
</template>
<script>
import MyNav from '~/components/MyNav'
import MyFooter from '~/components/MyFooter'
export default {
components: {
MyNav, MyFooter
},
head () {
return {
title: '用户登录'
}
},
data () {
return {
form: {
username: '',
password: ''
}
}
},
methods: {
async login () {
try {
const resp = await this.$axios.post('/api/user/login', this.form)
console.log('登录成功', resp)
} catch (e) {
alert(e.message || e)
}
}
}
}
</script>
<style>
.container {
min-height: 300px;
}
</style>
然后我们访问页面/user/login就能够看到页面效果,输入我们刚刚注册的用户名、密码、点击登录按钮,然后我们打开浏览器控制台,会看到登录的用户信息,如下图:

如何记录登录状态
由于我们是前后端分离,Go语言的服务端和Nuxt.js的页面服务无法共享session,所以登录状态我们没法由session来存储,所以我们引入了token机制。在用户登录成功之后,同时为该用户生成一个token,他就相当于sessionId,通过它能够找到对应的用户,上面的登录功能在登录成功后服务端给我们返回了用户信息和token,所以登录成功之后需要由浏览器记录下token,并且每次Nuxt.js请求接口的时候都需要带上该token,这样接口服务在收到带token的请求之后就能够知道该请求是哪个用户发起的。总结流程如下:
- 调用登录接口,验证用户名密码,验证成功后接口返回授权令牌(userToken);
- 前端网页收到授权令牌(userToken)后,将他们存储到cookie中;
- 前端网页在每次请求后台接口的时候检查cookie中是有有
userToken,如果有就带上; - 服务端在收到网页中的接口请求时,检查请求中是否有合法的
userToken,否则就返回错误要求网页进行登录;
所以我们需要借助cookie来存储token,并且在每次Nuxt.js请求接口的时候都需要带上cookie中的token。
利用cookie存储token
Nuxt.js有cookie插件:cookie-universal-nuxt ,接下来我们来使用该插件。
安装cookie-universal-nuxt
npm i --save cookie-universal-nuxt
安装成功之后打开文件nuxt.config.js配置cookie-universal-nuxt,在modules中添加如下配置:
modules: [
// Doc: https://github.com/nuxt-community/modules/tree/master/packages/bulma
'@nuxtjs/bulma',
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
['cookie-universal-nuxt', { alias: 'cookies' }]
]
接下来我们修改site/pages/user/login.vue文件,在登录成功之后将token保存到cookie中,修改之后的完整代码如下:
<template>
<section>
<my-nav/>
<section class="section">
<div class="container">
<div class="field">
<label class="label">用户名</label>
<div class="control">
<input v-model="form.username" class="input" type="text" placeholder="请输入用户名">
</div>
</div>
<div class="field">
<label class="label">密码</label>
<div class="control">
<input v-model="form.password" class="input" type="password" placeholder="请输入密码">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link" @click="login">登录</button>
</div>
</div>
</div>
</section>
<my-footer/>
</section>
</template>
<script>
import MyNav from '~/components/MyNav'
import MyFooter from '~/components/MyFooter'
export default {
components: {
MyNav, MyFooter
},
head () {
return {
title: '用户登录'
}
},
data () {
return {
form: {
username: '',
password: ''
}
}
},
methods: {
async login () {
try {
const resp = await this.$axios.post('/api/user/login', this.form)
console.log('登录成功', resp)
this.$cookies.set('userToken', resp.token, {maxAge: 86400 * 7, path: '/'})
this.$router.push('/')
} catch (e) {
alert(e.message || e)
}
}
}
}
</script>
<style>
.container {
min-height: 300px;
}
</style>
Nuxt.js的请求带上token
上面我们已经对axios做了一个简单的封装,接下来我们继续修改该封装,让它在每次请求的时候都检查cookie中是否有token,如果有就自动带上。修改文件site/plugins/axios.js,修改之后的完整内容如下:
import qs from 'qs'
export default function ({ $axios, $toast, app }) {
$axios.onRequest((config) => {
config.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
const userToken = app.$cookies.get('userToken') // 从cookie中获取token
if (userToken) { // 如果找到了token,那么将token放到请求头中
config.headers.common['X-User-Token'] = userToken
}
config.transformRequest = [
function (data) {
if (process.client && data instanceof FormData) { // 如果是FormData就不转换
return data
}
data = qs.stringify(data)
return data
}
]
})
$axios.onResponse((response) => {
if (response.status !== 200) {
return Promise.reject(response)
}
const jsonResult = response.data
if (jsonResult.success) {
return Promise.resolve(jsonResult.data)
} else {
return Promise.reject(jsonResult)
}
})
}
这样我们在每次使用axios请求的Go语言接口的时候就都会检查并带上cookie中保存的token了。
总结
本实例中我们完成了整个用户的登录和注册模块,通过学习本章内容相信你已经具备开发一个独立模块的能力了。下面我们看下本实例完整源码的目录结构:
.
├── server
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── user_controller.go
│ ├── user_model.go
│ └── user_service.go
└── site
├── README.md
├── assets
│ └── README.md
├── components
│ ├── Logo.vue
│ ├── MyFooter.vue
│ ├── MyNav.vue
│ └── README.md
├── jsconfig.json
├── layouts
│ ├── README.md
│ └── default.vue
├── middleware
│ └── README.md
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── README.md
│ ├── index.vue
│ └── user
│ ├── login.vue
│ └── reg.vue
├── plugins
│ ├── README.md
│ └── axios.js
├── static
│ ├── README.md
│ └── favicon.ico
└── store
└── README.md
文章转载请注明出处,原文链接:https://mlog.club/topic/650
