【Go + Nuxt.js 搭建一个 BBS 系统】8. 搭建用户模块

0 / 334

点击下面链接购买后可获取本课程完整源码,同时提供高大上的在线IDE开发环境,边看教程边动手。

作者简介

大猫猫,互联网公司老码农、不折腾不舒服斯基,多年千万日活服务端研发和架构经验。关注公众号查看更多技术干货:

码农俱乐部

实验介绍

实验内容

一个论坛系统,用户模块是组基础的,最必不可少的,所以我们先从用户模块开始,一步步的完善论坛系统。接下来我们接着上一个实验继续完善用户模块功能。

示例项目中用到的一些工具类来自于这个项目: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"`                        // 创建时间
}

用户相关的结构体定义完成之后,需要将他们放入gormAutoMigrate,这样在启动服务的时候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的请求之后就能够知道该请求是哪个用户发起的。总结流程如下:

  1. 调用登录接口,验证用户名密码,验证成功后接口返回授权令牌(userToken);
  2. 前端网页收到授权令牌(userToken)后,将他们存储到cookie中;
  3. 前端网页在每次请求后台接口的时候检查cookie中是有有userToken,如果有就带上;
  4. 服务端在收到网页中的接口请求时,检查请求中是否有合法的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