KOA 入门,完善用户注册和登录逻辑

在上次简单实现用户注册和登录逻辑的时候「传送门」,提到了很多优化的地方,在这里我们来解决优化这些问题。

在开始之前,因为上次我们测试的时候创建了多个相同账号名字的账号,所以我们先进行强制同步模型来清空上次创建的相关数据。调用 node ./src/db/connect.js 来处理相关逻辑。

这里提个小小的优化,每次我们都去执行 node ./src/db/connect.js 可能会有点麻烦,并且同步模式每次切换还要去代码上做相对应的修改。所以这里我们可以在 package.json 中的 scripts 中配置对应的指令,根据对应指令来进行对应的同步模式。首先,

// 根目录 > package.json
{
    // 其他内容
    "scripts": {
        // ... 其他已有配置
        // 新增内容
        "sync": "node ./src/db/connect.js",
        "sync:force": "cross-env TYPE=force node ./src/db/connect.js",
        "sync:alter": "cross-env TYPE=alter node ./src/db/connect.js"
    }
}

这里需要安装一个 cross-env 的第三方包,这个包的作用就是可以在 npm 上轻松的使用环境变量。

npm i cross-env -D

配置 scripts 后,将 connect.js 修改成:

// src > db > connect.js

// ...

// 获取传的参数,也就是 cross-env TYPE=xxx
const type = process.env.TYPE

/** model 同步数据库 */
if (!type) {
    // 只有数据库中不存在与模型同名的数据表时才同步
    sequelize.sync().then(() => {
        console.log('同步数据库成功')
        process.exit()
    })
} else if (type === 'force') {
    // 强制同步,会将原先的表格以及数据清空,重新创建新的表格
    sequelize.sync({ force: true }).then(() => {
        console.log('同步数据库成功')
        process.exit()
    })
} else if (type === 'alter') {
    // 修改同名数据表结构(这个选项表示 Sequelize 会检查数据库表和模型之间的差异,并尝试自动更新数据库表以匹配模型定义)
    sequelize.sync({ alter: true }).then(() => {
        console.log('同步数据库成功')
        process.exit()
    })
}

然后,我们就可以直接通过 npm run 的方式进行对应的同步了

npm run sync
npm run sync:force
npm run sync:alter

注册前判断当前用户是否已存在

验证用户是否已存在,其实这个功能我们已经实现过。查询用户是否存在,和用户登录逻辑其实有一部分相通的地方。都是去数据库查询用户信息,只不过一个通过 userName,而另一个则是通过 userNamepassword。所以我们先改一下 service 中的内容。

// src > db > service > user.js
async function findOneUser({userName, password}) {
    // 默认查询条件 userName, 当存在 password 的时候再传入 password
    const where = {
        userName
    }
    if (password) where.password = password

    const res = await User.findOne({
        attributes: ['userName', 'introduce', 'email', 'id'],       // 获取哪些属性
        where
    })
    
    if (res) return res
    return 0
}

然后后面流程就简单了,写接口,controller

// src > routes > user.js
router.get('/nameAvailable', async (ctx, next) => {
    const { userName } = ctx.query

    ctx.body = await checkNameAvailable(userName)
})

// src > controller > user.js
async function checkNameAvailable(userName) {
    const res = await findOneUser({ userName })

    if (!res) {
        return {
            code: 0,
            message: '用户名可用'
        }
    } else {
        return {
            code: 3,
            message: '用户名已注册'
        }
    }
}

以上就完成了查询用户名是否可用的接口,我们可以在输入用户名的输入框中绑定 blur 事件去触发查询当前用户名是否允许注册。

当然,这里在注册接口前我们也最好去查询一次用户名是否可用。

async function register({ userName, password, email, introduce }) {
    const availableRes = await findOneUser({ userName })
    // 新增
    if (availableRes) return { code: 3, message: '用户名已注册' }

    try {
        const res = await createUser({
            userName,
            password,
            email,
            introduce,
        })
        
        return {
            code: 0,
            data: res
        }
    } catch (err) {
        console.log(err)
        return {
            code: 1,
            message: '创建用户失败'
        }
    }
}

用户密码加密

首先我们下载 bcrypt 来对密码进行加密

npm install bcrypt

因为 bcrypt 加密后并没有提供解密的方法,相当于密码加密后不能转回原先的明文密码。相当于它是一种单向的加密算法,提供了一个接口让我们对比输入的密码与存储的哈希值来验证。所以这里我们在 service 中改一下。

  • 注册接口在注册 createUser 的时候对密码进行加密,数据库存储的是加密后的哈希值。
  • 登录查询 findOneUser,先通过用户名去查找对应的账号,再通过对比输入密码与哈希值的方式来进行密码配对。

直接展示修改后的两个方法

// src > service > user.js

const bcrypt = require('bcrypt')
const saltRounds = 10

/** 创建用户 */
async function createUser({ userName, password, email, introduce }) {
    // 密码加密
    const hashedPassword = await bcrypt.hash(password, saltRounds)
    
    const res = await User.create({
        userName,
        password: hashedPassword,
        email,
        introduce,
    })

    const data = res.dataValues
    delete data.password
    return data
}

/** 查询 */
async function findOneUser({userName, password}) {
    const attributes = ['userName', 'introduce', 'email', 'id']
    if (password) attributes.push('password')
    
    const res = await User.findOne({
        attributes,       // 获取哪些属性
        where: {
            userName
        }
    })
    
    if (password && res) {
        const isPasswordValid = await bcrypt.compare(password, res.dataValues.password)
        if (isPasswordValid) {
            // 删除密码,不展示返回
            delete res.dataValues.password
            return res.dataValues
        }
        return 0
    }

    if (res) return res.dataValues
    return 0
}

然后我们可以测试下注册和登录

数据库中数据,密码已经不是明文

登录的话也是没问题的

「失败」

身份认证

身份认证一般有两种 sessionJWT。在这里我们采用 session 的认证机制。

首先下载相关依赖

npm i koa-generic-session koa-redis

因为这里使用了 redis 来存储 session,所以要在本地下载 redis「下载地址」

下载完成后,我们就可以在 app.js 中配置 session

// src > app.js
// ... 其他引用
const session = require('koa-generic-session')
const redisStore = require('koa-redis')

// ... 其他代码

// session 设置
app.keys = ['SESSION_SECRET']   // session 密钥
app.use(session({
  key: 'koa:sess',                // cookie 名称
  maxAge: 24 * 60 * 60 * 1000,    // session 过期时间
  signed: true,                   // 是否签名 cookie
  httpOnly: true,                 // 是否只允许 http 访问
  store: redisStore({             // 使用 Redis 存储 session
     all: `localhost:6379`
  })
}))

// ... 其他代码

在配置中,其中 session 密钥cookie 名称 按照自己的需求随意修改。redis 配置也需要按照本地情况填写。koa-session 也提供了集群的相关配置,具体可以看官方的配置。「传送门」

配置完后,要在接口中使用才可以。这里我们改一下登录接口

// src > routes > api > user.js
router.post('/login', async(ctx, next) => {
    const { userName, password } = ctx.request.body
    // 传 ctx 到 controller
    ctx.body = await userLogin(ctx, { userName, password })
})

// src > controller > user.js
async function userLogin(ctx, { userName, password }) {
    const res = await findOneUser({ userName, password })
    if (res) {
        // 设置 session
        ctx.session.userInfo = res
        return {
            code: 0,
            data: res,
            message: '登录成功'
        }
    } else {
        return {
            code: 2,
            message: '登录失败'
        }
    }
}

然后我们在登录的时候就能看到 响应头 中已经有了 Set-Cookie 属性

这时候在终端进入 redis-cli,输入 keys * 也可看到存储的 session 信息

后面判断用户身份就容易多了,接口调用的时候查询一下 session.userInfo 是否存在即可。

posted @ 2024-11-24 17:29  limoonrise  阅读(55)  评论(0)    收藏  举报