1. 背景

前面我使用vue3.0 + golang实现了一版基于内存版本的前后端分离的博客系统,今天我把物理存储加到里面,对vue部分进行了改造,同时设计了三张表,user、post、comment 基于mysql关系表进行存储,同时把用户密码和jwt结合,实现鉴权。 

该博客中所涉及的所有源代码都已上传到github,前面的文章中已经给出地址。 本人不才,渣渣代码欢迎吐槽。

 

 2. 数据库设计 直接使用gorm  AutoMigrate 创建表

 

type Comment struct {
	ID        int       `json:"id"`
	CommentID uint64    `gorm:"not null;unique" json:"comment_id"` // 评论ID
	PostID    uint64    `gorm:"not null" json:"post_id"`
	AuthorID  uint64    `gorm:"not null" json:"author_id"`
	Username  string    `gorm:"size:100;not null;" json:"username"`
	Content   string    `gorm:"size:255;not null;" json:"content"` // 评论内容
	CreatedAt time.Time `gorm:"column:CreatedAt;autoCreateTime" json:"created_at"`
	UpdatedAt time.Time `gorm:"column:UpdatedAt;autoUpdateTime" json:"updated_at"`
}

type Post struct {
	ID        uint64    `gorm:"primary_key;auto_increment" json:"id"`
	PostID    uint64    `gorm:"not null" json:"post_id"` // 帖子ID
	Title     string    `gorm:"size:100;not null;unique" json:"title"`
	Content   string    `gorm:"size:255;not null;" json:"content"`
	AuthorID  uint64    `gorm:"not null" json:"author_id"`
	CreatedAt time.Time `gorm:"column:CreatedAt;autoCreateTime" json:"created_at"`
	UpdatedAt time.Time `gorm:"column:UpdatedAt;autoUpdateTime" json:"updated_at"`
}

type User struct {
	ID          uint64    `gorm:"primary_key;auto_increment" json:"id"`
	UserID      uint64    `gorm:"not null;unique" json:"user_id" ` // 用户ID
	Nickname    string    `gorm:"size:100;not null;unique" json:"nickname"`
	Email       string    `gorm:"size:100;not null;unique" json:"email"`
	Password    string    `gorm:"size:100;not null;" json:"password"`
	Description string    `gorm:"size:255;not null;" json:"description"`
	CreatedAt   time.Time `gorm:"column:CreatedAt;autoCreateTime" json:"created_at"`
	UpdatedAt   time.Time `gorm:"column:UpdatedAt;autoUpdateTime" json:"updated_at"`
}

 3. golang 部分代码

Api设计,路由部分如下

	// Home Route
	s.Router.HandleFunc("/", middlewares.SetMiddlewareJSON(s.Home)).Methods("GET")

	// Login Route
	s.Router.HandleFunc("/api/auth/login", middlewares.SetMiddlewareJSON(s.Login)).Methods("POST")

	//Users routes
	s.Router.HandleFunc("/api/auth/create-user", middlewares.SetMiddlewareJSON(s.CreateUser)).Methods("POST")
	s.Router.HandleFunc("/api/users/{user_id}", middlewares.SetMiddlewareJSON(s.GetUser)).Methods("GET")
	s.Router.HandleFunc("/users", middlewares.SetMiddlewareJSON(s.GetUsers)).Methods("GET")

	s.Router.HandleFunc("/users/{user_id}", middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.UpdateUser))).Methods("PUT")
	s.Router.HandleFunc("/users/{user_id}", middlewares.SetMiddlewareAuthentication(s.DeleteUser)).Methods("DELETE")

	//Posts routes
	s.Router.HandleFunc("/api/posts", middlewares.SetMiddlewareJSON(s.GetPosts)).Methods("GET")
	s.Router.HandleFunc("/api/one_user_posts/{user_id}", middlewares.SetMiddlewareJSON(s.GetPostsByUserId)).Methods("GET")
	s.Router.HandleFunc("/api/posts", middlewares.SetMiddlewareJSON(s.CreatePost)).Methods("POST")
	s.Router.HandleFunc("/api/posts/{post_id}", middlewares.SetMiddlewareAuthentication(s.DeletePost)).Methods("DELETE")
	s.Router.HandleFunc("/posts/{post_id}", middlewares.SetMiddlewareJSON(s.GetPost)).Methods("GET")
	s.Router.HandleFunc("/posts/{post_id}", middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.UpdatePost))).Methods("PUT")

	//Posts routes
	s.Router.HandleFunc("/api/posts/{post_id}/comments",
		middlewares.SetMiddlewareJSON(middlewares.SetMiddlewareAuthentication(s.CreateComments))).Methods("POST")
	s.Router.HandleFunc("/api/comments/{comment_id}/del", middlewares.SetMiddlewareAuthentication(s.DeleteComment)).Methods("POST")

中间件

package middlewares

import (
	"errors"
	responses "github.com/beijingzhangwei/ddyy-b/endpoints/reponses"
	"net/http"

	"github.com/beijingzhangwei/ddyy-b/endpoints/auth"
)

func SetMiddlewareJSON(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		next(w, r)
	}
}

func SetMiddlewareAuthentication(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		err := auth.TokenValid(r)
		if err != nil {
			responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))
			return
		}
		next(w, r)
	}
}

发帖接口如下:

package controllers

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/beijingzhangwei/ddyy-b/endpoints/auth"
	"github.com/beijingzhangwei/ddyy-b/endpoints/models"
	responses "github.com/beijingzhangwei/ddyy-b/endpoints/reponses"
	"github.com/beijingzhangwei/ddyy-b/endpoints/utils/formaterror"
	"github.com/gorilla/mux"
	"io/ioutil"
	"net/http"
	"strconv"
	"time"
)

func (server *Server) CreatePost(w http.ResponseWriter, r *http.Request) {

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		responses.ERROR(w, http.StatusUnprocessableEntity, err)
		return
	}
	post := models.Post{}
	err = json.Unmarshal(body, &post)
	if err != nil {
		responses.ERROR(w, http.StatusUnprocessableEntity, err)
		return
	}
	post.Prepare()
	err = post.Validate()
	if err != nil {
		responses.ERROR(w, http.StatusUnprocessableEntity, err)
		return
	}
	uid, err := auth.ExtractTokenID(r)
	if err != nil {
		responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))
		return
	}
	if uid != post.AuthorID {
		responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)))
		return
	}
	post.PostID = uint64(time.Now().Unix())
	postCreated, err := post.SavePost(server.DB)
	if err != nil {
		formattedError := formaterror.FormatError(err.Error())
		responses.ERROR(w, http.StatusInternalServerError, formattedError)
		return
	}

	w.Header().Set("Lacation", fmt.Sprintf("%s%s/%d", r.Host, r.URL.Path, postCreated.PostID))
	responses.JSON(w, http.StatusOK, postCreated)
}

func (server *Server) GetPosts(w http.ResponseWriter, r *http.Request) {

	post := models.Post{}

	posts, err := post.FindAllPosts(server.DB, 0)
	if err != nil {
		responses.ERROR(w, http.StatusInternalServerError, err)
		return
	}
	responses.JSON(w, http.StatusOK, posts)
}

func (server *Server) GetPostsByUserId(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	// Check if the post id is valid
	uid, err := strconv.ParseUint(vars["user_id"], 10, 64)
	if err != nil {
		responses.ERROR(w, http.StatusBadRequest, err)
		return
	}
	post := models.Post{}

	posts, err := post.FindAllPosts(server.DB, uid)
	if err != nil {
		responses.ERROR(w, http.StatusInternalServerError, err)
		return
	}
	responses.JSON(w, http.StatusOK, posts)
}

func (server *Server) GetPost(w http.ResponseWriter, r *http.Request) {

	vars := mux.Vars(r)
	pid, err := strconv.ParseUint(vars["post_id"], 10, 64)
	if err != nil {
		responses.ERROR(w, http.StatusBadRequest, err)
		return
	}
	post := models.Post{}

	postReceived, err := post.FindPostByID(server.DB, pid)
	if err != nil {
		responses.ERROR(w, http.StatusInternalServerError, err)
		return
	}
	responses.JSON(w, http.StatusOK, postReceived)
}

func (server *Server) UpdatePost(w http.ResponseWriter, r *http.Request) {

	vars := mux.Vars(r)

	// Check if the post id is valid
	pid, err := strconv.ParseUint(vars["post_id"], 10, 64)
	if err != nil {
		responses.ERROR(w, http.StatusBadRequest, err)
		return
	}

	//CHeck if the auth token is valid and  get the user id from it
	uid, err := auth.ExtractTokenID(r)
	if err != nil {
		responses.ERROR(w, http.StatusUnauthorized, errors.New("ExtractTokenID-->Unauthorized"))
		return
	}

	// Check if the post exist
	post := models.Post{}
	err = server.DB.Debug().Model(models.Post{}).Where("post_id = ?", pid).Take(&post).Error
	if err != nil {
		responses.ERROR(w, http.StatusNotFound, errors.New("Post not found"))
		return
	}

	// If a user attempt to update a post not belonging to him
	if uid != post.AuthorID {
		fmt.Println("uid != post.AuthorID ", uid, post.AuthorID)
		responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized: uid != post.AuthorID"))
		return
	}
	// Read the data posted
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		responses.ERROR(w, http.StatusUnprocessableEntity, err)
		return
	}

	// Start processing the request data
	postUpdate := models.Post{}
	err = json.Unmarshal(body, &postUpdate)
	if err != nil {
		responses.ERROR(w, http.StatusUnprocessableEntity, err)
		return
	}

	//Also check if the request user id is equal to the one gotten from token
	if uid != postUpdate.AuthorID {
		responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized: uid != postUpdate.AuthorID"))
		return
	}

	postUpdate.Prepare()
	err = postUpdate.Validate()
	if err != nil {
		responses.ERROR(w, http.StatusUnprocessableEntity, err)
		return
	}

	postUpdate.ID = post.ID
	postUpdate.PostID = post.PostID //this is important to tell the model the post id to update, the other update field are set above

	postUpdated, err := postUpdate.UpdateAPost(server.DB)
	fmt.Println("postUpdate.UpdateAPost 结果:", postUpdated)

	if err != nil {
		formattedError := formaterror.FormatError(err.Error())
		responses.ERROR(w, http.StatusInternalServerError, formattedError)
		return
	}
	responses.JSON(w, http.StatusOK, postUpdated)
}

func (server *Server) DeletePost(w http.ResponseWriter, r *http.Request) {

	vars := mux.Vars(r)

	// Is a valid post id given to us?
	pid, err := strconv.ParseUint(vars["post_id"], 10, 64)
	if err != nil {
		responses.ERROR(w, http.StatusBadRequest, err)
		return
	}

	// Is this user authenticated?
	uid, err := auth.ExtractTokenID(r)
	if err != nil {
		responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))
		return
	}

	// Check if the post exist
	post := models.Post{}
	err = server.DB.Debug().Model(models.Post{}).Where("post_id = ?", pid).Take(&post).Error
	if err != nil {
		responses.ERROR(w, http.StatusNotFound, errors.New("Unauthorized"))
		return
	}

	// Is the authenticated user, the owner of this post?
	if uid != post.AuthorID {
		responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))
		return
	}
	_, err = post.DeleteAPost(server.DB, pid, uid)
	if err != nil {
		responses.ERROR(w, http.StatusBadRequest, err)
		return
	}
	w.Header().Set("Entity", fmt.Sprintf("%d", pid))
	responses.JSON(w, http.StatusNoContent, "")
}

jwt鉴权部分

package auth

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	jwt "github.com/dgrijalva/jwt-go"
)

// CreateToken 用户需要进行身份验证通过后,然后才能: 更新或关闭他们的帐户,创建,更新和删除文章。

func CreateToken(user_id uint64) (string, error) {
	claims := jwt.MapClaims{}
	claims["authorized"] = true
	claims["user_id"] = user_id
	claims["exp"] = time.Now().Add(time.Hour * 3).Unix() //Token expires after 3 hour
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
}

func TokenValid(r *http.Request) error {
	tokenString := ExtractToken(r)
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("ACCESS_SECRET")), nil
	})
	if err != nil {
		return err
	}
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		Pretty(claims)
	}
	return nil
}

func ExtractToken(r *http.Request) string {
	keys := r.URL.Query()
	token := keys.Get("token")
	if token != "" {
		return token
	}
	bearerToken := r.Header.Get("Authorization")
	if len(strings.Split(bearerToken, " ")) == 2 {
		return strings.Split(bearerToken, " ")[1]
	}
	return ""
}

func ExtractTokenID(r *http.Request) (uint64, error) {

	tokenString := ExtractToken(r)
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(os.Getenv("ACCESS_SECRET")), nil
	})
	if err != nil {
		return 0, err
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	if ok && token.Valid {
		uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 64)
		if err != nil {
			return 0, err
		}
		fmt.Println("提取uid=", uid)
		return uid, nil
	}
	return 0, nil
}

//Pretty display the claims licely in the terminal
func Pretty(data interface{}) {
	b, err := json.MarshalIndent(data, "", " ")
	if err != nil {
		log.Println(err)
		return
	}

	fmt.Println(string(b))
}

4 vue 部分代码

<template>
<base-card :expandable="post.comments && post.comments.length > 0">
  <template v-slot:header>
    <h3>
      <!--      NOTE 页面路由设置的站位符号,需要完全匹配-->
     <router-link :to="linkUser(post.post_author.email)">{{postTitle(post) }}</router-link>
    </h3>
    <button v-if="loggedIn && currentUser.email === post.post_author.email" class="delete-button"
            @click.prevent="deletePost">Delete</button>
  </template>
  <div class="text-wrapper">Say:{{post.content }}</div>
  <template v-slot:footer>
    <base-card v-for="comment in post.comments" :key="comment.comment_id" :expandable="false">
      <template v-slot:header>
        <h3>
          <router-link :to="linkUser(comment.comment_author.email)">{{commentTitle(comment)}}</router-link>
        </h3>
        <button v-if="loggedIn && (currentUser.email === post.post_author.email || currentUser.email === comment.comment_author.email) " class="delete-button"
                @click.prevent="deleteComment(comment)">Delete</button>
      </template>
      {{ comment.content }}
    </base-card>
  </template>
  <template v-if="loggedIn" v-slot:actions>
    <add-text-form textRequest="Add comment" :showLabel="false" @text-added="text => addComment(text, post)"></add-text-form>
  </template>
</base-card>
</template>

<script>
import {mapGetters} from "vuex";
import BaseCard from "@/components/UI/BaseCard";
import AddTextForm from "@/components/AddTextForm";
export default {
  components: { BaseCard, AddTextForm },
  props: ["post"],
  name: "SinglePost",
  computed: {
    ...mapGetters({
      loggedIn: "auth/isLoggedIn",
      currentUser: "auth/currentUser"
    })
  },
  methods: {
    postTitle(post) {
      return post.post_author.email + "  发布于" + post.created_at;
    },
    commentTitle(comment) {
      return comment.comment_author.email + "  点评于" + comment.created_at;
    },
    linkUser(email) {
      return {
        name: "User",
        params: {
          email: email
        }
      };
    },
    addComment(text, post) {
      this.$store.dispatch("posts/addComment", {
        postId: post.post_id,
        comment: {
          post_id: post.post_id,
          email: this.currentUser.email,
          author_id: this.currentUser.user_id,
          content: text
        }
      });
    },
    deletePost() {
      this.$store.dispatch("posts/deletePost", {post: this.post});
    },
    deleteComment(comment) {
      this.$store.dispatch("posts/deleteComment", {comment: comment});
    }
  }
};
</script>
<style>
.text-wrapper {
  white-space: pre-wrap;
}
</style>

 整体代码结构(源码:在前面的文章中已经贡献)