添加依赖 go get github.com/spf13/viper
,支持 JSON, TOML, YAML, HCL
等格式的配置文件。在项目根目录下面新建 conf
目录,然后新建 application.yml
文件,写入内容如下:
server:
port: 9988 #启动应用程序的端口号
datasource: #数据库配置信息
driverName: mysql
host: 127.0.0.1
port: "3306"
database: gin_demo
username: root
password: rx123456
charset: utf8
loc: Asia/Shanghai
创建 common/database.go
文件,使用 gorm 初始化数据库配置:
package common
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"net/url"
)
var DB *gorm.DB
func InitDB() *gorm.DB {
//从配置文件中读取数据库配置信息
host := viper.GetString("datasource.host")
port := viper.Get("datasource.port")
database := viper.GetString("datasource.database")
username := viper.GetString("datasource.username")
password := viper.GetString("datasource.password")
charset := viper.GetString("datasource.charset")
loc := viper.GetString("datasource.loc")
args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=true&loc=%s",
username,
password,
host,
port,
database,
charset,
url.QueryEscape(loc))
fmt.Println(args)
db, err := gorm.Open(mysql.Open(args), &gorm.Config{})
if err != nil {
fmt.Println(err)
panic("failed to connect database, err: " + err.Error())
}
DB = db
return db
}
新建 router/routes.go
文件:
package router
import (
"github.com/gin-gonic/gin"
"middleware"
)
func CollectRoute(r *gin.Engine) *gin.Engine {
r.Use(middleware.CORSMiddleware(), middleware.RecoverMiddleware()) //使用中间件
r.POST("/api/auth/register", controller.Register) //注册
r.POST("/api/auth/login", controller.Login) //登录
r.GET("/api/auth/userinfo", middleware.AuthMiddleware(), controllers.UserDetail) //获取详情
return r
}
新建 response/response.go
文件:
package response
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 封装的响应体
func Response(ctx *gin.Context, httpStatus int, code int, data gin.H, msg string) {
ctx.JSON(httpStatus, gin.H{
"code": code,
"data": data,
"msg": msg,
})
}
func Success(ctx *gin.Context, data gin.H, msg string) {
Response(ctx, http.StatusOK, 200, data, msg)
}
func Fail(ctx *gin.Context, data gin.H, msg string) {
Response(ctx, http.StatusOK, 400, data, msg)
}
新建 util/util.go
文件
package util
import (
"math/rand"
"time"
)
// 生成随机字符串
func RandomString(n int) string {
var letters = []byte("asdfghjklzxcvbnmqwertyuiopASDFGHJKLZXCVBNMQWERTYUIOP")
result := make([]byte, n)
rand.Seed(time.Now().Unix())
for i := range result {
result[i] = letters[rand.Intn(len(letters))]
}
return string(result)
}
CREATE TABLE `user_infos` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`telephone` varchar(11) NOT NULL DEFAULT '',
`password` varchar(255) NOT NULL DEFAULT '',
`created_at` datetime(3) DEFAULT NULL,
`updated_at` datetime(3) DEFAULT NULL,
`deleted_at` datetime(3) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
新建 model/User.go
文件:
package model
import "gorm.io/gorm"
type UserInfo struct {
gorm.Model //继承gorm的Model,里面包含了ID、CreatedAt、UpdatedAt、DeletedAt
Name string `gorm:"type:varchar(20);not null"`
Telephone string `gorm:"varchar(11);not null;unique"`
Password string `gorm:"size:255;not null"`
}
DTO就是数据传输对象(Data Transfer Object)的缩写;用于展示层与服务层之间的数据传输对象。
新建 response/user_dto.go
文件:
package response
import (
model2 "gin-demo/model"
)
type UserDto struct {
Name string `json:"name"`
Telephone string `json:"telephone"`
}
// DTO就是数据传输对象(Data Transfer Object)的缩写;用于 展示层与服务层之间的数据传输对象
func ToUserDto(user model2.UserInfo) UserDto {
return UserDto{
Name: user.Name,
Telephone: user.Telephone,
}
}
新建 middleware/RecoveryMiddleware.go
文件:
package middleware
import (
"fmt"
response2 "gin-demo/response"
"github.com/gin-gonic/gin"
)
func RecoverMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
response2.Fail(c, nil, fmt.Sprint(err))
c.Abort()
return
}
}()
}
}
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。新建 middleware/CORSMiddleware.go
文件:
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 跨域中间件
func CORSMiddleware() gin.HandlerFunc { //CORS是跨源资源分享(Cross-Origin Resource Sharing)中间件
return func(ctx *gin.Context) {
//指定允许其他域名访问
//ctx.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:8080")
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*") //跨域:CORS(跨来源资源共享)策略
//预检结果缓存时间
ctx.Writer.Header().Set("Access-Control-Max-Age", "86400")
//允许的请求类型(GET,POST等)
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "*")
//允许的请求头字段
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "*")
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(200)
} else {
ctx.Next()
}
}
}
新建 middleware/AuthMiddleware.go
文件:
package middleware
import (
common2 "gin-demo/common"
model2 "gin-demo/model"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
// token认证中间件(权限控制)
func AuthMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
auth := "jiangzhou"
// 获取authorization header
tokenString := ctx.GetHeader("Authorization") //postman测试:在Headers中添加: key:Authorization;value:jiangzhou:xxx(token值)
//fmt.Println(tokenString)
//fmt.Println(strings.HasPrefix(tokenString,auth+""))
// 无效的token
//if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer ") { //验证token的前缀为:
if tokenString == "" || !strings.HasPrefix(tokenString, auth+":") { //验证token的前缀为:
ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
ctx.Abort()
return
}
index := strings.Index(tokenString, auth+":") //找到token前缀对应的位置
tokenString = tokenString[index+len(auth)+1:] //截取真实的token(开始位置为:索引开始的位置+关键字符的长度+1(:的长度为1))
//fmt.Println("截取之后的数据:",tokenString)
token, claims, err := common2.ParseToken(tokenString)
if err != nil || !token.Valid { //解析错误或者过期等
ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
ctx.Abort()
return
}
// 验证通过后获取claim 中的userId
userId := claims.UserId
//判定
var user model2.UserInfo
common2.DB.First(&user, userId)
if user.ID == 0 { //如果没有读取到内容,说明token值有误
ctx.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "权限不足"})
ctx.Abort()
return
}
ctx.Set("user", user) //将key-value值存储到context中
ctx.Next()
}
}
新建 common/jwt.go
文件:
package common
import (
model2 "gin-demo/model"
"github.com/dgrijalva/jwt-go"
"time"
)
var jwtKey = []byte("a_secret_key") //证书签名秘钥(该秘钥非常重要,如果client端有该秘钥,就可以签发证书了)
type Claims struct {
UserId uint
jwt.StandardClaims
}
// 分发证书
func ReleaseToken(user model2.UserInfo) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour) //截止时间:从当前时刻算起,7天
claims := &Claims{
UserId: user.ID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(), //过期时间
IssuedAt: time.Now().Unix(), //发布时间
Issuer: "jiangzhou", //发布者
Subject: "user token", //主题
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) //生成token
tokenString, err := token.SignedString(jwtKey) //签名
if err != nil {
return "", err
}
return tokenString, nil
}
// 解析证书
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtKey, nil
})
return token, claims, err
}
新建 controllers/UserController.go
文件:
package controllers
import (
"fmt"
common2 "gin-demo/common"
model2 "gin-demo/model"
response2 "gin-demo/response"
util2 "gin-demo/util"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"net/http"
)
// 注册
func UserRegister(ctx *gin.Context) {
var requestUser model2.UserInfo
ctx.Bind(&requestUser)
name := requestUser.Name
telephone := requestUser.Telephone
password := requestUser.Password
// 数据验证
if len(telephone) != 11 {
//422 Unprocessable Entity 无法处理的请求实体
response2.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "手机号必须为11位")
fmt.Println(telephone, len(telephone))
return
}
if len(password) < 6 {
response2.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "密码不能少于6位")
return
}
// 如果名称没有传,给一个10位的随机字符串
if len(name) == 0 {
name = util2.RandomString(10)
}
// 判断手机号是否存在
if isTelephoneExist(common2.DB, telephone) {
response2.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "用户已经存在")
return
}
// 创建用户
//返回密码的hash值(对用户密码进行二次处理,防止系统管理人员利用)
hashPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
response2.Response(ctx, http.StatusInternalServerError, 500, nil, "加密错误")
return
}
newUser := model2.UserInfo{
Name: name,
Telephone: telephone,
Password: string(hashPassword),
}
common2.DB.Create(&newUser) // 新增记录
// 发放token
token, err := common2.ReleaseToken(newUser)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "系统异常"})
return
}
// 返回结果
response2.Success(ctx, gin.H{"token": token}, "注册成功")
}
func UserLogin(ctx *gin.Context) {
var requestUser model2.UserInfo
ctx.Bind(&requestUser)
//name := requestUser.Name
telephone := requestUser.Telephone
password := requestUser.Password
// 数据验证
if len(telephone) != 11 {
//422 Unprocessable Entity 无法处理的请求实体
response2.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "手机号必须为11位")
fmt.Println(telephone, len(telephone))
return
}
if len(password) < 6 {
response2.Response(ctx, http.StatusUnprocessableEntity, 422, nil, "密码不能少于6位")
return
}
// 依据手机号,查询用户注册的数据记录
var user model2.UserInfo
common2.DB.Where("telephone=?", telephone).First(&user)
if user.ID == 0 {
ctx.JSON(http.StatusUnprocessableEntity, gin.H{"code": 422, "msg": "用户不存在"})
return
}
// 判断密码收否正确
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "密码错误"})
return
}
// 发放token
token, err := common2.ReleaseToken(user)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "系统异常"})
return
}
// 返回结果
response2.Success(ctx, gin.H{"token": token}, "登录成功")
}
func UserDetail(ctx *gin.Context) {
user, _ := ctx.Get("user")
response2.Success(ctx, gin.H{
"user": response2.ToUserDto(user.(model2.UserInfo))}, "响应成功")
}
func isTelephoneExist(db *gorm.DB, telephone string) bool {
var user model2.UserInfo
db.Where("telephone=?", telephone).First(&user)
//如果没有查询到数据,对于uint数据,默认值为:0
if user.ID != 0 {
return true
}
return false
}
在项目根目录下执行 go build
,然后会生成 gin-demo
的文件,然后可以将这个二进制文件拷贝到任意目录下,另外需要将项目下面的 conf
目录也拷贝过去。
然后执行 ./gin-demo
即可运行服务:
以上代码参考:https://gitee.com/rxbook/gin-demo
准备了一个简单的前端页面,代码在https://gitee.com/rxbook/vue-demo1
,本地运行:
#安装依赖
npm install
#运行
npm run serve
发布构建:
npm run build
构建完成后会生成 dist
目录,然后在nginx中配置虚拟主机:
server {
listen 80;
server_name vue-demo1.cc;
root /home/rx/web_front/vue-demo1/dist;
location ^~ /api/ {
proxy_pass http://127.0.0.1:9988;
}
}