服务端编程——登录阶段(含JsonWebToken使用)
登录阶段
用户要传入的信息
-
首先肯定要传入account和secret 也就是账号和密码,但在小程序里登录时不止有一种方式,比如可以直接从微信进去 这样就不需要密码了,因此密码要设置为可选填项
- 添加对token的api以及TokenValidator校验器
-
其次还要传入type,也就是进入途径,由于js没有枚举器,所以在api/v1/lib下新建文件enum.js来模拟枚举
- 其实里面就是一个对象LoginType,其中有各种进入方式(type)对应的值
- 设置一个isLoginType函数来判断当前的type是否属于里面的值 也就是是否合法;这里要遍历的是对象中的key值,还不太方便... 但可以将函数也定义在enum.js中,并将其放到LoginType对象里,之后都通过对象来调用,就能直接用this拿到所有key值:
function isThisType (val) {
for (let key in this) {
if (this[key] === val) {
return true
}
}
return false
}
const loginType = {
USER_MINI_PROGRAM: 100,
USER_EMAIL: 101,
USER_MOBILE: 102,
ADMIN_EMAIL: 200,
isThisType
}
module.exports = {
loginType
}
验证阶段
用户发送完相应请求后,要做以下几件事情:
- 检验各项信息是否都按照要求填写,格式符不符合规范:设置一个TokenValidator校验器专门校验登录时的信息
- 暂时只包括 account、secret、type(进入方式,刚刚说了验证方法)
- 根据当前进入方式来选择不同的处理函数:用switch...case语句,调用不同的函数
- 如:当前是用邮箱登录的 即type===LoginType.USER_EMAIL,则调用emailLogin函数(同步的),这个函数写在发送api的token.js文件中(当前文件)
- emailLogin中调用Model中定义的方法verifyEmailPassword来验证账号密码是否正确:为什么要在Model中定义而不是直接卸载emailLogin中呢,因为这个方法是验证账号密码的,也就是要操控数据库中的数据,因此还是写sequelize中的Model中比较好(注意也要是同步的)
- verifyEmailPassword中先看数据库中有没有输入的账号(User.findOne()),若不存在直接throw一个NotFound的error,存在的话就看当前输入的密码和数据库中相应密码是否匹配。这里要注意,数据库中保存的是加密的密码 而输入的是明文的,因此要用bcrypt的compareSync方法来判断密码是否匹配正确,不正确就抛出一个AuthorFailed的error
- 注意以上所有函数要加上async和await让其同步,且返回值基本都是Promise,要得到它的确切返回值一定要在回调函数(.then())中拿,或者是在前面加上await把promise中的值计算出来
// /app/api/v1/token.js
router.post('/', async (ctx, next) => {
const v = await new TokenValidator().validate(ctx)
const type = v.get('body.type')
const account = v.get('body.account')
const secret = v.get('body.secret')
switch (type) {
case loginType.USER_EMAIL:
await emailLogin(account, secret)
breakb
case loginType.USER_MINI_PROGRAM:
break
default:
throw new global.errs.ParameterException('没有相应的处理函数')
}
})
async function emailLogin (account, secret) {
const res = await User.verifyEmailPassword(account, secret)
}
// /modules/user.js
class User extends Model {
static async verifyEmailPassword (email, secret) {
// 数据库查询是个异步操作 也要加await
const res = await User.findOne({
where: {
email
}
})
if (!res) {
throw new global.errs.NotFound('账号不存在')
}
const correct = bcrypt.compareSync(secret, res.password)
if (!correct) {
throw new global.errs.AuthorFailed('密码不正确')
}
return res
}
}
jwt——Json Web Token
就是一种便于客户端与服务器通信的令牌,详见
项目中使用
-
koa项目中引入basic-auth包,在/middlewares/auth.js中写一个Auth类用来校验jwt令牌
-
引入base-auth中的basicAuth函数,将当前http请求对象传入即可得到token
- 在koa项目中,中间件中的上下文ctx:ctx.request是被koa封装处理过后的请求对象,而ctx.req才是原生的http请求对象
-
在要验证的地方,将这个Auth类导入,直接将刚刚写的函数当作中间件导入,注意顺序哦,一定要在执行操作之前就将其导入,这样才能起到截断的作用(在router函数中可以导入多个中间件)
-
Auth类中的校验函数里通过当前发送的http请求获取到token后,大致分为三步
- 先看token和token.name(内容)是否存在,若不存在直接抛出Forbidden的error
- 再看token的值是否合法——通过jsonwebtoken中的verify方法,传入两个参数 一个是当前获取到的token,另一个是配置文件中自己写好的secretKey
- 第二步中的verify方法若错误则会抛出异常,所以要用try...catch语句来调用,其中catch中要查看当前error的名字来区分到底是 token过期 还是token不合法
-
token通过校验成功之后,会返回一个对象,内容就是在生成token的时候自己传进去的第一个参数(想要token携带的信息),可以将其保存到ctx中,在后续中间件都可以使用到
get m () {
return async (ctx, next) => {
const errMsg = 'token不合法'
// ctx.request === koa 对请求封装了
// ctx.req === 原生的http请求
const userToken = basicAuth(ctx.req)
if (!userToken || !userToken.name) {
throw new global.errs.Forbidden(errMsg)
}
try {
var decode = jwt.verify(userToken.name, global.config.security.secretKey)
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new global.errs.Forbidden('token已过期')
}
throw new global.errs.Forbidden(errMsg)
}
ctx.auth = {
uid: decode.uid,
scope: decode.scope
}
await next()
}
}