引入MongoDB
- 你可以点击这里查看本文的GithubREADME项目链接也是这个哦
在K8S中创建MongoDB
- 无需手动本地安装,直接用我们之前的dockerimage->pod.container的技术
- pod的container配置image,K8S将给我们自动pull进来
apiVersion:apps/v1kind:Deploymentmetadata:name:auth-mongo-deplspec:replicas:1selector:matchLabels:app:auth-mongotemplate:metadata:labels:app:auth-mongospec:containers:-name:auth-mongo#pullimageimage:mongo---apiVersion:v1kind:Servicemetadata:name:auth-mongo-srvspec:selector:app:auth-mongoports:-name:dbprotocol:TCP#port是每个Node在Kubernetes的cluster中的port#targetPort是Pod的portport:27017targetPort:27017
cdticketing/infra/k8s/skaffolddevkubectlgetpods
- skaffold一直在watch
./infra/k8s/*
变化,所以直接skaffold自动化启动即可
⬆backtotop
连接到MongoDB
//index.tsimportexpressfrom"express";import"express-async-errors";import{json}from"body-parser";//引入importmongoosefrom"mongoose";import{currentUserRouter}from"./routes/current-user";import{signinRouter}from"./routes/signin";import{signoutRouter}from"./routes/signout";import{signupRouter}from"./routes/signup";import{errorHandler}from"./middleware/error-handler";import{NotFoundError}from"./errors/not-found-error";constapp=express();app.use(json());app.use(currentUserRouter);app.use(signinRouter);app.use(signoutRouter);app.use(signupRouter);app.all("*",async(req,res)=>{thrownewNotFoundError();});app.use(errorHandler);conststart=async()=>{try{//连接到K8S内部的srv的clusterDomain和serviceIPPortawaitmongoose.connect("mongodb://auth-mongo-srv:27017/auth",{useNewUrlParser:true,useUnifiedTopology:true,useCreateIndex:true,});console.log("ConnectedtoMongoDb");}catch(err){console.log(err);}app.listen(3000,()=>{console.log("Listeningonport3000!");});};start();
⬆backtotop
用户登录的工作流
⬆backtotop
让TypeScript和Mongoose搭配
目前遇到的问题#1withTSMongoose
- 创建一个Document
Typescript能确保我们提供正确的属性,但MongoDB要确保约束我们传入的属性是比较困难的
newUser({email:"test@test.com",password:"lk325kj2"});
目前遇到的问题#2withTSMongoose
我们传递给User构造函数的属性不一定与用户可用的属性匹配
如下:MongoDB不仅要存我们给定的字段,还要自动生成其他字段
constuser=newUser({email:"test@test.com",password:"lk325kj2"});console.log(user);//{email:'..',password:'..',createdAt:'..',updatedAt:'..'}
⬆backtotop
创建UserModel
//user.tsimportmongoosefrom"mongoose";//创建一个新的schemaconstuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});//.model()方法会生成shcema的副本,在调用.model()方之前,请确保已添加了要使用的的所有shcema。//集合User在数据库中的name是UserconstUser=mongoose.model("User",userSchema);export{User};
⬆backtotop
用户属性的类型检查
解决遇到的问题#1withTSMongoose
Typescript能确保我们提供正确的属性,但MongoDB要确保约束我们传入的属性是比较困难的约束newUser(attrs)的attrs属性即可
//user.tsimportmongoosefrom"mongoose";//一个interface,描述了在//创建用户(传值到数据库)的时候需要做到那些约束interfaceUserAttrs{email:string;password:string;}//!!!!!注意这不是TypeScriptconstuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});constUser=mongoose.model("User",userSchema);//以后在要创建User的场景中,用buildUser进行创建constbuildUser=(attrs:UserAttrs)=>{returnnewUser(attrs);};export{User,buildUser};
⬆backtotop
给Model增加静态属性
- 因为我们需要在创建的时候进行传入值的类型约束,所以需要给model抽象出一个build方法的interface,每次通过build来创建,build里面有类型约束
- Q:怎么给model抽象出一个build方法的interface?
- A:先继承Model,在继承后的UserModel静态属性中新增build,最后因为UserModel仍然是一个抽象的,需要传给
mongoose.model<Doc,Model>
让他理解,所以UserModel应该是一个interface
//user.tsimportmongoosefrom"mongoose";//一个interface,描述了在//创建用户(传值到数据库)的时候需要做到哪些传参约束interfaceUserAttrs{email:string;password:string;}//一个interface,描述了//对于UserModel的属性的约束interfaceUserModelextendsmongoose.Model<any>{build(attrs:UserAttrs):any;}constuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});userSchema.statics.build=(attrs:UserAttrs)=>{returnnewUser(attrs);};constUser=mongoose.model<any,UserModel>("User",userSchema);export{User};
⬆backtotop
约束UserDocument中的属性
解决遇到的问题#2withTSMongoose
我们传递给User构造函数的属性不一定与用户可用的属性匹配
MongoDB不仅要存我们给定的字段,还要自动生成其他字段约束UserDocument中的字段即可
importmongoosefrom"mongoose";//一个interface,描述了在//创建用户(传值到数据库)的时候需要做到哪些传参约束interfaceUserAttrs{email:string;password:string;}//一个interface,描述了//对于UserModel用户集合的属性的约束interfaceUserModelextendsmongoose.Model<UserDoc>{build(attrs:UserAttrs):UserDoc;}//一个interface,描述了//对于UserDocument单个独立的用户的属性的约束interfaceUserDocextendsmongoose.Document{email:string;password:string;}constuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});userSchema.statics.build=(attrs:UserAttrs)=>{returnnewUser(attrs);};constUser=mongoose.model<UserDoc,UserModel>("User",userSchema);export{User};
⬆backtotop
model的泛型是什么意思
- 有对Document和Model(因为Model中有N个Document,所以要依赖于Document)的约束
- 返回值是Model约束后的结果,返回的是
Modelxx集合
//index.d.tsexportfunctionmodel<TextendsDocument,UextendsModel<T>>(name:string,schema?:Schema,collection?:string,skipInit?:boolean):U;
⬆backtotop
创建用户
//signup.tsimportexpress,{Request,Response}from"express";import{body,validationResult}from"express-validator";import{User}from"../models/user";import{RequestValidationError}from"../errors/request-validation-error";constrouter=express.Router();router.post("/api/users/signup",[body("email").isEmail().withMessage("Emailmustbevalid"),body("password").trim().isLength({min:4,max:20}).withMessage("Passwordmustbebetween4and20characters"),],async(req:Request,res:Response)=>{consterrors=validationResult(req);if(!errors.isEmpty()){thrownewRequestValidationError(errors.array());}const{email,password}=req.body;//查找有无用户创建constexistingUser=awaitUser.findOne({email});if(existingUser){console.log("Emailinuse");returnres.send({});}constuser=User.build({email,password});awaituser.save();res.status(201).send(user);});export{routerassignupRouter};
⬆backtotop
新增400请求错误Error
//bad-request-error.tsimport{CustomError}from"./custom-error";exportclassBadRequestErrorextendsCustomError{statusCode=400;constructor(publicmessage:string){super(message);Object.setPrototypeOf(this,BadRequestError.prototype);}serializeErrors(){return[{message:this.message}];}}
//signup.tsif(existingUser){thrownewBadRequestError("Emailinuse");}
⬆backtotop
记得给Password加hash
- 创建用户
- 密码验证
⬆backtotop
增加PasswordHashing功能
- 密码加密要用到
crypto
,因为是异步进行的,所以需要promisify化
,这里我们来复习下Promisify、PromisifyAll、Promise.all
吧
//1.promisifyfunctiontoPrimisify(fn){returnfunction(...args){returnnewPromise(function(resolve,reject){fn(...args,(err,data)=>{err?reject(err):resolve(data)})})}letread=toPrimisify(fs.readFile);read('./1.txt','utf8').then(res=>{console.log(res)});//2.promisifyAllfunctiontoPromisifyAll(obj){Object.keys(obj).forEach((item,index)=>{if(typeofobj[item]=='function')obj[item'Async']=toPrimisify(obj[item])})}toPromisifyAll(fs);fs.readFileAsync('./2.txt','utf8').then(res=>{console.log(res)});//3.promise.allfunctionpromiseAll(promises){returnnewPromise(function(resolve,reject){if(!Array.isArray(promises)){returnreject(newTypeError("argumentmustbeanarray"))}varcountNum=0;varpromiseNum=promises.length;varresolvedvalue=newArray(promiseNum);for(vari=0;i<promiseNum;i){(function(i){Promise.resolve(promises[i]).then(function(value){countNum;resolvedvalue[i]=value;if(countNum===promiseNum){returnresolve(resolvedvalue)}},function(reason){returnreject(reason))})(i)}})}varp1=Promise.resolve(1),p2=Promise.resolve(2),p3=Promise.resolve(3);promiseAll([p1,p2,p3]).then(function(value){console.log(value)})
- 同时,也要用到nodejs的内置类Buffer
JavaScript语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像TCP流或文件流时,必须使用到二进制数据。因此在Node.js中,定义了一个Buffer类,该类用来创建一个专门存放二进制数据的缓存区。
在Node.js中,Buffer类是随Node内核一起发布的核心库。Buffer库为Node.js带来了一种存储原始数据的方法,可以让Node.js处理二进制数据,每当需要在Node.js中处理I/O操作中移动的数据时,就有可能使用Buffer库。原始数据存储在Buffer类的实例中。一个Buffer类似于一个整数数组,但它对应于V8堆内存之外的一块原始内存。
//password.tsimport{scrypt,randomBytes}from"crypto";import{promisify}from"util";//convertcallbackscryptfunctiontoasyncawaituseconstscryptAsync=promisify(scrypt);exportclassPassword{staticasynctoHash(password:string){constsalt=randomBytes(8).toString("hex");constbuf=(awaitscryptAsync(password,salt,64))asBuffer;return`${buf.toString("hex")}.${salt}`;}}
⬆backtotop
比较HashedPassword
//password.tsimport{scrypt,randomBytes}from"crypto";import{promisify}from"util";//convertcallbackscryptfunctiontoasyncawaituseconstscryptAsync=promisify(scrypt);exportclassPassword{staticasynccompare(storedPassword:string,suppliedPassword:string){const[hashedPassword,salt]=storedPassword.split(".");constbuf=(awaitscryptAsync(suppliedPassword,salt,64))asBuffer;returnbuf.toString("hex")===hashedPassword;}}
⬆backtotop
MongoosePre-SaveHooks
- 这个hooks主要是在做
[xxxaction]
操作之前,需要做的事情 - 我们这里当然是在保存password之前,进行toHash操作
//user.tsuserSchema.pre("save",asyncfunction(done){if(this.isModified("password")){consthashed=awaitPassword.toHash(this.get("password"));this.set("password",hashed);}done();//completeasyncwork});
⬆backtotop