资讯专栏INFORMATION COLUMN

聊聊毕业设计系列 --- 系统实现

null1145 / 1855人阅读

摘要:七牛云接入本系统的图片,音视频是放在七牛云,所以需要接入七牛云。在服务端通过接口请求来获取七牛云上传,客户端获取到七牛云,通过不同方案将带上。

效果展示

github

moment-server github地址

moment github地址

moment-manage github地址

articles

聊聊毕业设计系列 --- 项目介绍

聊聊毕业设计系列 --- 系统实现

前言

在上一篇文章中,主要是对项目做了介绍,并且对系统分析和系统设计做了大概的介绍。那么接下来这篇文章会对系统的实现做介绍,主要是选择一些比较主要的模块或者说可拿出来与大家分享的模块。好了,接入正题吧~~

MongoDB

服务端这边使用的是Express框架,数据库使用的是MongoDB,通过Mongoose模块来操作数据库。这边主要是想下对MongoDB做个介绍,当然看官了解的话直接往下划~~

在项目开始前要确保电脑是否安装mongoDB,下载点我,图像化工具Robo 3T 点我,下载好具体怎么配置还请问度娘或Google吧,本文不做介绍了哈。注意:安装完mongoDB的时候进行项目时要把lib目录下的mongod服务器打开哈~~

MongoDB 是一个基于分布式文件存储的数据库,是一个介于关系型数据库和非关系型数据库之间的开源产品,它是功能最为丰富的非关系型数据库,也是最像关系型数据库的。但是和关系型数据库不同,MongoDB没有表和行的概念,而是一个面向集合、文档的数据库。其中的文档是一个键值对,采用BSON(Binary Serialized Document Format),BSON是一种类似于JSON的二进制形式的存储格式,并且BSON具有表示数据类型的扩展,因此支持的数据非常丰富。MongoDB有两个很重要的数据类型就是内嵌文档和数组,而且在数组内可以嵌入其他文档,这样一条记录就能表示非常复杂的关系。

Mongoose是在node.js异步环境下对MongoDB进行简便操作的对象模型工具,能从数据库提取任何信息,可以用面向对象的方法来读写数据,从而使操作MongoDB数据库非常便捷。Mongoose中有三个非常重要的概念,便是Schema(模式),Model(模型),Entity(实体)。

Schema: 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力,创建它的过程如同关系型数据库建表的过程,如下:

//Schema
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const UserSchema = new Schema({
    token: String,
    is_banned: {type: Boolean, default: false}, //是否禁言
    enable: { type: Boolean, default: true }, //用户是否有效
    is_actived: {type: Boolean, default: false}, //邮件激活
    username: String,
    password: String,
    email: String,  //email唯一性
    code: String,
    email_time: {type: Date},
    phone: {type: String},
    description: { type: String, default: "这个人很懒,什么都没有留下..." },
    avatar: { type: String, default: "http://p89inamdb.bkt.clouddn.com/default_avatar.png" },
    bg_url: { type: String, default: "http://p89inamdb.bkt.clouddn.com/FkagpurBWZjB98lDrpSrCL8zeaTU"},
    ip: String,
    ip_location: { type: Object },
    agent: { type: String }, // 用户ua
    last_login_time: { type: Date },
    .....
});

Model: 由Schema发布生成的模型,具有抽象属性和行为的数据库操作对象

//生成一个具体User的model并导出
const User = mongoose.model("User", UserSchema);  //第一个参数是集合名,在数据库中会把Model名字字母全部变小写和在后面加复数s

//执行到这个时候你的数据库中就有了 users 这个集合

module.exports = User;

Entity: 由Model创建的实体,他的操作也会影响数据库,但是它操作数据库的能力比Model弱

const newUser = new UserModel({          //UserModel 为导出来的 User
            email: req.body.email,
            code: getCode(),
            email_time: Date.now()
        }); 

Mongoose中有一个东西个人感觉非常主要,那便是populate,通过populate他可以很方便的与另一个集合建立关系。如下,user集合可以与article集合、user集合本身进行关联,根据其内嵌文档的特性,这样子他便可以内嵌子文档,子文档中有可以内嵌子文档,这样子它返回的数据就会异常的丰富。

const user = await UserModel.findOne({_id: req.query._id, is_actived: true}, {password: 0}).populate({
                path: "image_article",
                model: "ImageArticle",
                populate: {
                    path: "author",
                    model: "User"
                }
            }).populate({
                path: "collection_film_article",
                model: "FilmArticle",
            }).populate({
                path: "following_user",
                model: "User",
            }).populate({
                path: "follower_user",
                model: "User",
            }).exec();

服务端主要是操作数据库,对数据库进行增删改查(CRUD)等操作。项目中的接口,Mongoose的各种方法这边就不对其做详细介绍,大家可以查看Mongoose文档。

用户身份认证实现 介绍

本系统的用户身份认证机制采用的是JSON Web Token(JWT),它是一种轻量的认证规范,也用于接口的认证。我们知道,HTTP协议是一种无状态的协议,这便意味着每个请求都是独立的,当用户提供了用户名和密码来对我们的应用进行用户认证,那么在下一次请求的时候,用户需要再进行一次用户的认证才可以,因为根据HTTP协议,我们并不能知道是哪个用户发出的请求,本系统采用了token的鉴权机制。这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。

在用户身份认证这一块有很多方法,最常见的像cookie ,session。那么他们三之间又有什么区别,这里有两篇文章介绍的挺全面。

正确理解HTTP短连接中的Cookie、Session和Token

小白必读:闲话HTTP短连接中的Session和Token

token 与 session的区别在于,它不同于传统的session认证机制,它不需要在服务端去保留用户的认证信息或其会话的信息。系统一旦比较大,都会采用机器集群来做负载均衡,这需要多台机器,由于session是保存在服务端,那么就要 去考虑用户到底是在哪一台服务器上进行登录的,这便是一个很大的负担。

那么就有人想问了,你这个系统这么小,为什么不使用传统的session机制呢?哈~因为之前自己的项目一般都是使用session做登录,没使用过token,想尝试尝试入入坑~~哈哈哈~

实现思路

JWT主要的实现思路如下:

在用户登录成功的时候创建token保存于数据库中,并返回给客户端。

客户端之后的每一次请求都要带上token,在请求头里加入Authorization,并加上token.

在服务端进行验证token的有效性,在有效期内返回200状态码,token过期则返回401状态码

如下图所示:


JWT请求图

在node中主要用了jsonwebtoken这个模块来创建JWT,jsonwebtoken的使用请查看jsonwebtoken文档。项目中创建token的中间件createToken如下

/**
 * createToken.js
 */
const jwt = require("jsonwebtoken");  // 引入jsonwebtoken模块
const secret = "我是密钥"

//登录时:核对用户名和密码成功后,应用将用户的id(user_id)作为JWT Payload的一个属性
module.exports = function(user_id){
    const token = jwt.sign({
        user_id: user_id
    }, secret, {  //密钥
        expiresIn: "24h" //过期时间设置为24h。那么decode这个token的时候得到的过期时间为:创建token的时间+设置的值
    });
    return token;
};

return 出来的 token 类似eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0.Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM。我们仔细看这字符串,分为三段,分别被 "." 隔开。现在我们分别对前两段进行base64解码如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  ===> {"alg":"HS256","typ":"JWT"}  其中 alg是加密算法名字,typ是类型

eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0  ===>  {"user_id":"admin","iat":1534684070,"exp":1534770470}  其中 name是我们储存的内容,iat创建的时间戳,exp到期时间戳。

Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM  ===> 最后一段是由前面两段字符串,HS256加密后得到。所以前面的任何一个字段修改,都会导致加密后的字符串不匹配。

当我们根据用户的id创建获取到token之后,我们需要把token返回到客户端,客户端对其在本地(localStorage)保存, 客户端之后的每一次请求都要带上token,在请求头里加入Authorization,并加上token,服务端进行验证token的有效性。那么我们如何验证token的有效性呢? 所以我们需要checkToken这个中间件来检测token的有效性。

/**
 * checkToken
 */
const jwt = require("jsonwebtoken");
const secret = "我是密钥"

module.exports = async ( req, res, next ) => {
    const authorization = req.get("Authorization");
    if (!authorization) {
        res.status(401).end();  //接口需要认证但是有没带上token,返回401未授权状态码
        return
    }
    const token = authorization.split(" ")[1];
    try {
        let tokenContent = await jwt.verify(token, secret);   //如果token过期或验证失败,将抛出错误
        next();     //执行下一个中间件
    } catch (err) {
        console.log(err)
        res.status(401).end();  //token过期或者验证失败返回401状态码
    }
}

那么现在咱们只要在需要用户认证的接口上,在操作数据之前,加上checkToken中间件即可,如下调用:

//更新用户信息
router.post("/updateUserInfo", checkToken, User.updateUserInfo)  

//如果checkToken检测不成功,它便返回401状态码,不会对User.updateUserInfo做任何操作, 只有检测token成功,才能处理User.updateUserInfo

我们如何保证每次请求都能在请求头里加入Authorization,并加上token,这就要用到Axios的请求拦截,并且也用到了它的响应拦截,因为在服务端返回401状态码之后应要执行登出操作,清楚本地token的存储,具体代码如下:

//request拦截器
instance.interceptors.request.use(
    config => {
        //每次发送请求之前检测本地是否存有token,都要放在请求头发送给服务器
        if(localStorage.getItem("token")){
            if (config.url.indexOf("upload-z0.qiniup.com/putb64") > -1){
                config.headers.Authorization = config.headers["UpToken"];  //加上七牛云上传token
            }
            else {
                config.headers.Authorization = `token ${localStorage.getItem("token")}`.replace(/(^")|("$)/g, "");  //加上系统接口token
            }
        }
        console.log("config",config)
        return config;
    },
    err => {
        console.log("err",err)
        return Promise.reject(err);
    }
);

//response拦截器
instance.interceptors.response.use(
    response => {
        return response;
    },
    error => { //默认除了2XX之外的都是错误的,就会走这里
        if(error.response){
            switch(error.response.status){
                case 401:
                    console.log(error.response)
                    store.dispatch("ADMIN_LOGINOUT"); //可能是token过期,清除它
                    router.replace({ //跳转到登录页面
                        path: "/login",
                        query: { redirect: "/dashboard" } // 将跳转的路由path作为参数,登录成功后跳转到该路由
                    });
            }
        }
        return Promise.reject(error.response);
    }
);

其中的if else 是因为本系统的图片,音视频是放在七牛云,上传需要七牛云上传base64图片的时候token是放在请求头的,正常的图片上传不是放在请求头,所以这边对token做了区分,如何接入七牛云也会在下面模块介绍到。

七牛云接入

本系统的图片,音视频是放在七牛云,所以需要接入七牛云。七牛云分了两种情况,正常图片和音视频的上传和base64图片的上传,因为七牛云在对他们两者上传的Content-Typedomain(域)有所不同,正常图片和音视频的Content-Type是headers: {"Content-Type":"multipart/form-data"}domain是domain="https://upload-z0.qiniup.com",而base64图片的上传则是headers:{"Content-Type":"application/octet-stream"}domain是domain="https://upload-z0.qiniup.com/putb64/-1",所以他们请求的时候token放的地方不同,base64就像上面所说的放在请求头Authorization中,而正常的放在form-data中。在服务端通过接口请求来获取七牛云上传token,客户端获取到七牛云token,通过不同方案将token带上。

base64的上传: headers:{"Content-Type":"application/octet-stream"}domain="https://upload-z0.qiniup.com/putb64/-1",token放在请求头Authorization中。

正常图片和音视频的上传: headers: {"Content-Type":"multipart/form-data"}domain="https://upload-z0.qiniup.com",token 放在 form-data中。

服务端通过qiniu这个模块进行创建token,服务端代码如下:

/**
 * 构建一个七牛云上传凭证类
 * @class QN
 */
const qiniu = require("qiniu")  //导入qiniu模块
const config = require("../config")
class QN {
    /**
     * Creates an instance of qn.
     * @param {string} accessKey -七牛云AK
     * @param {string} secretKey -七牛云SK
     * @param {string} bucket -七牛云空间名称
     * @param {string} origin -七牛云默认外链域名,(可选参数)
     */
    constructor (accessKey, secretKey, bucket, origin) {
        this.ak = accessKey
        this.sk = secretKey
        this.bucket = bucket
        this.origin = origin
    }
    /**
     * 获取七牛云文件上传凭证
     * @param {number} time - 七牛云凭证过期时间,以秒为单位,如果为空,默认为7200,有效时间为2小时
     */
    upToken (time) {
        const mac = new qiniu.auth.digest.Mac(this.ak, this.sk)
        const options = {
            scope: this.bucket,
            expires: time || 7200
        }
        const putPolicy = new qiniu.rs.PutPolicy(options)
        const uploadToken = putPolicy.uploadToken(mac)
        return uploadToken
    }
}

exports.QN = QN;

exports.upToken = () => {
    return new QN(config.qiniu.accessKey, config.qiniu.secretKey, config.qiniu.bucket, config.qiniu.origin).upToken()  //每次调用都创建一个token
}
//获取七牛云token接口
const {upToken} = require("../utils/qiniu")

app.get("/api/uploadToken", (req, res, next) => {
        const token = upToken()
        res.send({
            status: 1,
            message: "上传凭证获取成功",
            upToken: token,
        })
    })

由于正常图片和音视频的上传和base64图片的上传,因为七牛云在对他们两者上传的Content-Typedomain(域)有所不同,所以的token请求存放的位置有所不同,因此要区分,客户端调用上传代码如下:

//根据获取到的上传凭证uploadToken上传文件到指定域
    //正常图片和音视频的上传
    uploadFile(formdata, domain="https://upload-z0.qiniup.com",config={headers:{"Content-Type":"multipart/form-data"}}){
        console.log(domain)
        console.log(formdata)
        return instance.post(domain, formdata, config)
    },
    //base64图片的上传
    //根据获取到的上传凭证uploadToken上传base64到指定域
    uploadBase64File(base64, token, domain = "https://upload-z0.qiniup.com/putb64/-1", config = {
        headers: {
            "Content-Type": "application/octet-stream",
        },
    }){
        const pic = base64.split(",")[1];
        config.headers["UpToken"] = `UpToken ${token}`
        return instance.post(domain, pic, config)
    },
function upload(Vue, data, callbackSuccess, callbackFail) {
    //获取上传token之后处理
    Vue.prototype.axios.getUploadToken().then(res => {
        if (typeof data === "string"){  //如果是base64
            const token = res.data.upToken
            Vue.prototype.axios.uploadBase64File(data, token).then(res => {
                if (res.status === 200){
                    callbackSuccess && callbackSuccess({
                        data: res.data,
                        result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`
                    })
                }
            }).catch((error) => {
                callbackFail && callbackFail({
                    error
                })
            })
        }
        else if (data instanceof FormData){  //如果是FormData
            data.append("token", res.data.upToken)
            data.append("key", `moment${Date.now()}${Math.floor(Math.random() * 100)}`)
            Vue.prototype.axios.uploadFile(data).then(res => {
                if (res.status === 200){
                    callbackSuccess && callbackSuccess({
                        data: res.data,
                        result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`
                    })
                }
            }).catch((error) => {
                callbackFail && callbackFail({
                    error
                })
            })
        }
        else {
            const formdata = new FormData()  //如果不是formData 就创建formData
            formdata.append("token", res.data.upToken)
            formdata.append("file", data.file || data)
            formdata.append("key", `moment${Date.now()}${Math.floor(Math.random() * 100)}.${data.file.type.split("/")[1]}`)
            // 获取到凭证之后再将文件上传到七牛云空间
            console.log("formdata",formdata)
            Vue.prototype.axios.uploadFile(formdata).then(res => {
                console.log("res",res)
                if (res.status === 200){
                    callbackSuccess && callbackSuccess({
                        data: res.data,
                        result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` //返回的图片链接
                    })
                }
            }).catch((error) => {
                console.log(error)
                callbackFail && callbackFail({
                    error
                })
            })
        }

    })
}

export default upload
路由权限模块

系统的后台管理面向的是合作作者和管理员,涉及到两种角色,故此要做权限管理。不同的权限对应着不同的路由,同时侧边栏的菜单也需根据不同的权限,异步生成,不同于以往的服务端直接返回路由表,由前端动态生成,接下来介绍下登录和权限验证的思路:

登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后前端会根据token再去拉取一个getAdminInfo的接口来获取用户的详细信息(如用户权限,用户名等等信息)。

权限验证:通过token获取用户对应的role,动态根据用户的role算出其对应有权限的路由,通过vue-router的beforeEach进行全局前置守卫再通过router.addRoutes动态挂载这些路由。

代码有点多,这边就直接放流程图哈~~


权限路由流程图

最近正好也在公司做中后台项目,公司的中后台项目的这边是由服务端生成路由表,前端进行直接渲染,毕竟公司的一整套业务比较成熟。但是我们会在想能不能由前端维护路由表,这样不用到时候项目迭代,前端每增加页面都要让服务端兄弟配一下路由和权限,当然前提可能是项目比较小的时候。

账号模块

账号模块是业务中最为基础的模块,承担着整个系统所有的账号相关的功能。系统实现了用户注册、用户登录、密码修改、找回密码功能。

系统的账号模块使用了邮件服务,针对普通用户的注册采用了邮件服务来发送验证码,以及密码的修改等操作都采用了邮件服务。在node.js中主要采用了Nodemailer,Nodemailer是一个简单易用的Node.js邮件发送组件,它的使用可以摸我摸我摸我,通过此模块进行邮件的发送。你们可能会问,为什么不用短信服务呢?哈~因为短信服务要钱,哈哈哈

/*
* email 邮件模块
*/

const nodemailer = require("nodemailer");
const smtpTransport = require("nodemailer-smtp-transport");
const config = require("../config")

const transporter = nodemailer.createTransport(smtpTransport({
    host: "smtp.qq.com",
    secure: true,
    port: 465,  // SMTP 端口
    auth: {
        user: config.email.account,
        pass: config.email.password  //这里密码不是qq密码,是你设置的smtp授权码
    }
}));

let clientIsValid = false;
const verifyClient = () => {
    transporter.verify((error, success) => {
        if (error) {
            clientIsValid = false;
            console.warn("邮件客户端初始化连接失败,将在一小时后重试");
            setTimeout(verifyClient, 1000 * 60 * 60);
        } else {
            clientIsValid = true;
            console.log("邮件客户端初始化连接成功,随时可发送邮件");
        }
    });
};
verifyClient();

const sendMail = mailOptions => {
    if (!clientIsValid) {
        console.warn("由于未初始化成功,邮件客户端发送被拒绝");
        return false;
    }
    mailOptions.from = ""ShineTomorrow" "
    transporter.sendMail(mailOptions, (error, info) => {
        if (error) return console.warn("邮件发送失败", error);
        console.log("邮件发送成功", info.messageId, info.response);
    });
};

exports.sendMail = sendMail;

账号的注册先是填写email,填写好邮箱之后会通过Nodemailer发送一封含有有效期的验证码邮件,之后填写验证码、昵称和密码即可完成注册,并且为了安全考虑,对密码采用了安全哈希算法(Secure Hash Algorithm)进行加密。账号的登录以账号或者邮箱号加上密码进行登录,并且采用上文所说的JSON Web Token(JWT)身份认证机制,从而实现用户和用户登录状态数据的对应。


我的邮件长这样

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97123.html

相关文章

  • 聊聊毕业设计系列 --- 项目介绍

    摘要:又将整个文艺类阅读系统的业务划分为两大部分,分别是面向管理员和合作作者的后台管理系统和面向用户的移动端,系统的需求分析将围绕这两部分进行展开。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...

    Pink 评论0 收藏0
  • 聊聊毕业设计系列 --- 项目介绍

    摘要:又将整个文艺类阅读系统的业务划分为两大部分,分别是面向管理员和合作作者的后台管理系统和面向用户的移动端,系统的需求分析将围绕这两部分进行展开。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...

    villainhr 评论0 收藏0
  • 聊聊毕业设计系列 --- 系统实现

    摘要:七牛云接入本系统的图片,音视频是放在七牛云,所以需要接入七牛云。在服务端通过接口请求来获取七牛云上传,客户端获取到七牛云,通过不同方案将带上。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...

    qpal 评论0 收藏0
  • php资料集

    摘要:简单字符串缓存实战完整实战种设计模式设计模式是面向对象的最佳实践成为专业程序员路上用到的各种优秀资料神器及框架成为一名专业程序员的道路上,需要坚持练习学习与积累,技术方面既要有一定的广度,更要有自己的深度。 微型新闻系统的开发(PHP 5.4 + MySQL 5.5) 微型新闻系统的开发(PHP 5.4 + MySQL 5.5) 九个很有用的 PHP 代码 php 代码 国内值得关注的...

    RobinQu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<