登录功能是我紧接着注册功能后实现的,很多没有改动的文件和代码这里不重复介绍,只去介绍一些新加的代码和逻辑
- router/auth.router.js
跟注册功能一样,登录功能也需要注册一个单独的路由,其也有一个用于验证的中间件名为verityLogin,毕竟真正登录之前要检查的东西还是蛮多的,包括客户端传递过来的账号密码是否为空,查询数据库看看账号是否存在,密码是否正确等等,如果都写在一个文件中,代码逻辑固然繁琐复杂,可维护性低,所以我们依然应该像之前一样将对应的中间件抽离出去
const Router = require('koa-router')
// 用于在数据库中增加用户的中间件
const { login } = require('../controller/auth.controller')
const {
verityLogin
} = require('../middleware/auth.middleware')
const authRouter = new Router()
// 注册中间件,先执行用于验证的verityLogin中间件再去执行真正包含登录操作的login中间件
authRouter.post('/login', verityLogin, login)
module.exports = authRouter
- middleware/auth.middleware.js
这个文件主要做的就是登录前校验,和注册校验有所不同,登录校验主要检查的点除了用户传递过来的账号有没有注册过还需要检查客户端传递过来的密码是否正确,我们需要根据不同的情况为客户端响应不同的错误信息
这里有一个需要特别注意的地方:就是注册的时候用户的密码是通过了md5加密之后才存储进数据库的,所以我们在根据用户名从数据库查询到用户信息(包含密码)之后,再和用户传递过来的密码作比较时需要先将客户端传递过来的密码通过md5加密后再进行比较,否则比较出的结果肯定是不准确的
const {
NAME_OR_PWD_IS_REQUIRED,
USER_IS_NOT_EXISTS,
PASSWORD_IS_INCORRECT
} = require('../constants/err-types')
const { getUserName } = require('../service/user.service')
const md5Password = require('../utils/password-handle')
const { PUBLIC_KEY } = require('../app/config')
// 登录前校验
const verityLogin = async (ctx, next) => {
// 1. 获取用户名和密码
const { name, password } = ctx.request.body
// 2. 检验用户名或密码是否为空
if (!name || !password) {
// 制造对应的Error对象
const error = new Error(NAME_OR_PWD_IS_REQUIRED)
// 使用上下文ctx上面的app对象触发错误事件
return ctx.app.emit('error', error, ctx)
}
// 3. 检验用户是否存在
const [user] = await getUserName(name)
if (!user) {
const error = new Error(USER_IS_NOT_EXISTS)
return ctx.app.emit('error', error, ctx)
}
// 4. 检验用户密码是否正确
const { password: pwd } = user
// 将从数据库查询到的密码与客户端传递过来的密码(需要经过md5加密算法)进行比较
if (pwd !== md5Password(password)) {
const error = new Error(PASSWORD_IS_INCORRECT)
return ctx.app.emit('error', error, ctx)
}
// 将我们根据用户名在数据库查询到的信息放到ctx对象中的user属性下,这样在下一个中间件中就也能获取到用户信息啦
ctx.user = user
await next()
}
module.exports = {
verityLogin
}
- controller/auth.controller.js
因为登录要用到token作为登录凭证,所以我们自然是需要一个能够颁发且验证token的一个库,jsonwebtoken就是一个很好的选择
我们使用的加密算法是非对称加密的RS256,这种方式比对称加密会更加的安全,但是操作也更复杂一点,需要我们提前创建好私钥和公钥。颁发token的操作很简单,只需要调用jwt.sign函数就可以了,传递进去的第一个参数是存储到token第二部分的用户信息,第二个参数是我们的私钥,第三个参数是一些配置信息,比如说过期时间和使用的加密算法等等
const jwt = require('jsonwebtoken')
const { PRIVATE_KEY } = require('../app/config')
class AuthController {
async login(ctx, next) {
const { id, name } = ctx.user
// 利用jwt对象上的sign方法颁发token
const token = jwt.sign({ id, name }, PRIVATE_KEY, {
// 过期时间的单位是s,所以下面的60 * 5代表的是5分钟后过期
expiresIn: 60 * 5,
// 因为我们使用的已经不是默认的HS256对称加密算法了,所以需要特别指定一下
algorithm: "RS256"
})
// 用户登录成功之后需要响应id、name以及token给客户端
ctx.body = {
id,
name,
token
}
}
}
module.exports = new AuthController()
私钥以及公钥创建步骤:
在你想存放私钥和公钥的文件夹打开git bash,在里面输入如下指令:
- 创建私钥,公钥用于颁发
token
genrsa -out private.key 1024
- 创建公钥,私钥用于验证
token,并且公钥的创建是需要依赖于私钥的,毕竟知道了token是怎么颁发之后才能知道要怎么验证
rsa -in private.key -pubout -out public.key
这里我也简单演示一下使用对称加密算法来颁发token吧,和前面非对称加密的操作很像,但更加简洁,因为不需要生成私钥和公钥,我们只需要自己随便设置一个秘钥就可以进行加密了,同时这个秘钥也可以用来解密。但简便的操作也带来了安全问题:一旦秘钥泄露,别人也可以颁发并伪造token,非常的不安全,所以在项目中一般不使用对称加密
const SECRET_KEY = 'secret'
class AuthController {
async login(ctx, next) {
const { id, name } = ctx.body
// 对称加密我们直接使用默认的HS256加密算法就可以了,所以不需要特别配置
const token = jwt.sign({ id, name }, SECRET_KEY, {
// 单位是s
expiresIn: 10
})
ctx.body = {
id,
name,
token
}
}
}
- app.config.js
来看一下config.js的变化吧,像私钥、公钥这种全局变量,我们一般都是放到配置信息文件中的,只不过在读取文件信息的时候,不能够通过require直接引入,而是要使用文件操作读取,并且要注意,在node中的相对路径并不是相对于当前文件,而是你执行项目时所在的文件,所以我们在读取文件操作的时候最好是采用绝对路径(推荐使用path.resolve方法)
const fs = require('fs')
const path = require('path')
const dotenv = require('dotenv')
const PRIVATE_KEY = fs.readFileSync(path.resolve(__dirname, './keys/private.key'));
const PUBLIC_KEY = fs.readFileSync(path.resolve(__dirname, './keys/public.key'))
// 将.env中的变量以属性的形式放到process.env中
dotenv.config()
module.exports = {
APP_PORT,
MYSQL_HOST,
MYSQL_DATABASE,
MYSQL_USER,
MYSQL_PASSWORD,
MYSQL_CONNECTIONlIMIT,
} = process.env
module.exports.PRIVATE_KEY = PRIVATE_KEY
module.exports.PUBLIC_KEY = PUBLIC_KEY
- app/err-handle.js
因为在执行登录操作前的校验中间件中,不满足条件时需要响应错误信息,所以在这里面还需要增加几种错误的场景,比如用户名不存在或者密码错误,还有我们后续可能会遇到的token过期等等
const {
NAME_OR_PWD_IS_REQUIRED,
USER_ALREADY_EXISTS,
USER_IS_NOT_EXISTS,
PASSWORD_IS_INCORRECT,
NO_AUTHORIZATION
} = require('../constants/err-types')
const errorHandler = (error, ctx) => {
let status, message
switch (error.message) {
case NAME_OR_PWD_IS_REQUIRED:
status = 400 // Bad Request
message = '用户名或密码不能为空'
break
case USER_ALREADY_EXISTS:
status = 409 // Conflict
message = '用户名已经存在'
break
case USER_IS_NOT_EXISTS:
status = 400 // Bad Request
message = '用户名不存在'
break
case PASSWORD_IS_INCORRECT:
status = 400 // Bad Request
message = '密码错误'
break
case NO_AUTHORIZATION:
status = 401 // no_authorization
message = 'token失效,请重新登录!'
break
default:
status = 404
message = 'not found'
}
ctx.status = status
ctx.body = message
}
module.exports = errorHandler
- router/index.js
以前我们利用app.use注册路由的时候,每一个路由都需要写上以下代码:
app.use(router.routes())
app.use(router.allowedMethods())
这样子我们的项目一旦庞大起来,路由一多,就需要重复注册的操作,每一个路由都要对应这两句代码,所以看起来重复的代码就非常多,这里我们可以统一将所有的路由放在一个文件中动态注册
什么叫动态注册呢?其实就是通过文件操作,根据router文件夹下的文件名依次注册路由,所以需要借助文件模块fs中的readdirSync方法来帮助我们读取某个文件夹中的文件信息,然后在读取对应文件的信息(路由)
const fs = require('fs')
// 通过目录动态注册路由
const useRouters = function () {
fs.readdirSync(__dirname).forEach(file => {
// index.js文件不是路由,所以不需要注册
if (file !== 'index.js') {
const router = require(`./${file}`)
// 箭头函数的this执行继承父级作用域的,而父级作用域所对应的函数是被app所调用的,所以this=app
this.use(router.routes())
// 如果某种请求方式服务器没有或者不支持,将会自动返回帮助我们返回请求错误信息
this.use(router.allowedMethods())
}
})
}
module.exports = useRouters
在app/index.js中我们就可以通过调用router/index.js中返回的函数进行动态注册了
// app/index.js
const useRouters = require('../router/index')
app.useRouters = useRouters
app.useRouters()
以上就是登录模块的基本功能了,到目前为止,我们已经可以在登录成功后将服务器生成的token响应给客户端了,但是我们后续肯定会开发很多需要用户登录了之后才有权限体验的功能,比如说发内容、评论等
这些功能都有一个共同之处,就是在执行这些功能所对应的核心中间件之间,必须要做一件事情,就是判断用户到底有没有登录,这个检验的标志就是用户有token且token没有过期,正因为这一步操作在执行很多功能前都要完成,所以我们也可以把它封装成一个中间件函数,后面只要是开发需要权限的功能,就可以提前添加上这个中间件用于校验用户是否具有权限
// auth.middleware.js
const jwt = require('jsonwebtoken')
const {
NO_AUTHORIZATION
} = require('../constants/err-types')
const { PUBLIC_KEY } = require('../app/config')
// 验证用户是否登录
const verifyAuth = async (ctx, next) => {
const token = ctx.headers.authorization?.replace('Bearer ', '')
try {
// jwt.verify可以根据传入的token、公钥和解密算法来校验token是否有问题或者过期
const results = jwt.verify(token, PUBLIC_KEY, {
algorithms: ['RS256']
})
// 如果token没有问题,那么我们是可以拿到存储在token第二部分的用户信息的,将其放到ctx.user中方便后续的中间件获取使用
ctx.user = results
await next()
} catch (err) {
console.log(err);
// token验证有问题是会爆出错误的,所以这里必须要有个try catch
const error = new Error(NO_AUTHORIZATION)
ctx.app.emit('error', error, ctx)
}
}