写系列文章最容易出现的情况是写到一半,然后写不下去了。一个因素是人变懒了;一个因素是这几天在整理用户模块这部分的知识,发现有太多的内容要写,而深入Google之后又发现好多内容是自己未知的,写着写着就不知道该如何写了。
常见的和用户模块相关的操作有:注册登录、信息修改、退出登录
用户表设计
一个系统中,用户模块是最基础也是最重要的。如果只考虑一种登录方式,那用户表可以用一张
users
表搞定一切。但是现实情况是登录方式有很多种,比如:账号密码登录
,微信、QQ、微博登录
,手机号、邮箱登录
。而且一个用户可以使用账号密码登录
之后再关联微信、QQ、微博
,之后可以直接使用微信、QQ、微博
登录;那么如何保证设计用户表
将变得至关重要。
参考:
通过以上部分博文的介绍,我们可以将用户表设计为:用户基础表:user_base
,用户授权表:user_auth
,用户扩展表:user_extends
。
用户基础表:uid、user_role、user_name、avatar、password、signature、birthday、gender等
用户授权表:id、uid、identity_type、identifier、certificate等
用户扩展表:id、uid、device_name、device_id、vendor、client_name、client_version等
以上的用户表的设计可以参考: 用户系统设计与实现
ps:我这个项目的用户表设计当时没有考虑那么多,只考虑了
微信小程序端
的设计,先给自己留个坑以后再将项目完善吧。先介绍目前的实现方式。
我目前的实现步骤如下:
在app/models
包下创建user.js
:用户表的相关字段以及用户表的操作
User.init({
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
nickname: Sequelize.STRING,
email: {
type: Sequelize.STRING(128),
// 添加唯一(unique)约束后插入重复值会报错
unique: true
},
password: {
type: Sequelize.STRING,
set(val) {
// 使用 bcryptjs 库对密码加密处理
const salt = bcrypt.genSaltSync(10)
const psw = bcrypt.hashSync(val, salt)
// 设置 password 的值
this.setDataValue('password', psw)
}
},
openid: {
type: Sequelize.STRING(64),
unique: true
},
gender: Sequelize.INTEGER,
balance: Sequelize.INTEGER,
avatar_url: Sequelize.STRING,
city: Sequelize.STRING,
country: Sequelize.STRING,
province: Sequelize.STRING
}, {
sequelize,
tableName: 'users'
})
module.exports = {
User
}
注册登录
注册
微信小程序端不需要注册,直接使用微信授权登录即可。
这里的注册是指用户在Web、App 端
使用邮箱、用户名、密码完成注册。
在app/api/v1
包下创建user.js
:
/**
* 用户名密码注册
*/
router.post('/register', async (ctx) => {
const v = await new RegisterValidator().validate(ctx)
// 令牌获取 颁布令牌
const user = {
email: v.get('body.email'),
password: v.get('body.password2'),
nickname: v.get('body.nickname')
}
// 使用 Sequlize 保存到数据库
await User.create(user)
success()
})
module.exports = router
调用:
http://192.168.*.***:3000/v1/user/register
,在表单中填写邮箱、用户名、密码,然后将结果传递给api
即可
登录
使用账号(邮箱)密码登录
/**
* 邮箱登录:用户不存在会提示账户不存在,用户存在则验证用户信息,登录成功之后返回 token
* @param {账号} account
* @param {密码} secret
*/
async function emailLogin(account, secret) {
const user = await User.verifyEmailPassword(account, secret)
return token = generateToken(user.id, Auth.USER)
}
微信小程序使用token
登录
在app/service
包下创建wx.js
:获取登录的token
class WXManager {
/**
* 小程序登录
* @param {小程序中传递的 code} code
* @param {小程序中公开的用户信息} userInfo
*/
static async codeToToken(code, userInfo) {
const url = util.format(global.config.wx.loginUrl,
global.config.wx.appId,
global.config.wx.appSecret,
code)
// 调用微信提供的接口
const result = await axios.get(url)
if (result.status !== 200) {
throw new global.errs.AuthFailed('openid获取失败')
}
// 微信中最终判断是通过 errcode 判断
const errcode = result.data.errcode
const errmsg = result.data.errmsg
if (errcode){
throw new global.errs.AuthFailed('openid获取失败:'+errmsg)
}
// openid
// 档案 user uid openid 长
// openid
// 用户是否存在,例如 token 过期的情况
let user = await User.getUserByOpenid(result.data.openid)
if(!user){
user = await User.registerByOpenid(result.data.openid, userInfo)
}
return generateToken(user.id, Auth.USER)
}
}
module.exports = {
WXManager
}
(1)账号密码登录调用:
http://192.168.*.***:3000/v1/token
,在表单中填写邮箱、密码,然后将结果传递给api
即可;(2)小程序登录调用:先进行小程序登录,然后调用http://192.168.*.***:3000/v1/token
,将小程序提供的code
发送给api
即可
不管是账号密码登录
还是微信小程序登录
,都返回了token
主要是用于 API 鉴权、登录状态保持
在core
包下创建util.js
:生成token
的工具类
/**
* 使用 JWT 生成 token
* @param {用户 id} uid
* @param {用户权限} scope
*/
const generateToken = function (uid, scope) {
const secretKey = global.config.security.secretKey
const expiresIn = global.config.security.expiresIn
// 在 token 中写入 uid、scope 数据
const token = jwt.sign({
uid,
scope
}, secretKey, {
expiresIn: expiresIn
})
return token
}
module.exports = {
generateToken,
}
关于
JWT
的介绍可以查看我之前写的文章:全栈项目|小书架|服务器开发-JWT 详解,或者这篇文章:JWT 超详细分析
修改信息
用户信息的修改需要调用
update
信息即可,如果是修改密码
还需要将本地的登录状态
清除,重新登录。
具体的修改信息,后续接口实现了再补上。
退出登录
这篇文章介绍的不错:How to log out when using JWT
文中描述的主要方式有下面几种:
- 为令牌设置合理的过期时间
- 退出登录后,客户端删除
token
- 数据库存储不再有效且在过期时间内的
token
- 在服务端使用 Redis-维基百科 维护黑名单列表
维护黑名单的方式有人认为会导致黑名单列表过长,这里可以通过判断token
是否过期,过期则自动删除。
本项目只是从客户端删除token
的方式实现退出登录。
其他的处理方式参考:
咨询请加微信:轻撩即可。