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>
整体代码结构(源码:在前面的文章中已经贡献)