挑战30分钟写个用户登录注册系统(后端)- 我的全栈独立开发日记
前言
后端使用ThinkPHP框架+MySQL数据库
本教程的视频版本已发布在B站,欢迎交流~
准备工作
创建项目
使用包管理 composer基于ThinkPHP最新源码创建工程
composer create-project topthink/think user-account
新建文件
app/controller/user/Account.php
, 继承基础控制器类\app\BaseController
给
\app\BaseController
新增两个方法,以方便返回统一的JSON数据public function success($data = null, $msg = 'success') { return json([ 'errcode' => 0, // 错误码。规定:errcode不等于0时表示发生了异常 'msg' => $msg, // 错误信息 'data' => $data, // 返回数据 ]); } public function fail($msg = 'fail', $errcode = 1, $data = null) { return json([ 'errcode' => $errcode, 'msg' => $msg, 'data' => $data ]); }
写一个测试方法
function test()
{
return $this->success([
'content' => '静夜Coding @B站'
]);
}
启动本地服务
cd user-account && php think run
打开API调试工具
默认路由规则根据user.account/test找到对应控制器app/controller/user/Account.php
的test方法
POST localhost:8000/user.account/test
成功返回如下
设计数据库模型
SQL语句创建一个用户表
CREATE TABLE `user`
(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(30) NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
`salt` varchar(10) NOT NULL DEFAULT '' COMMENT '密码salt',
`nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称',
`avatar` varchar(300) NOT NULL DEFAULT '' COMMENT '用户头像',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unq_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
password
字段是基于散列算法对用户输入密码计算之后得到的字符串,基于数据安全考虑,数据库不能存储明文密码。
salt
字段是随机字符串,用户混淆散列算法,提高密码的安全性。
created_at
, updated_at
字段记录了数据插入和更新的时间,这样定义可以让数据库自动记录数据变更时间,免去业务代码的额外工作,而且使用datetime类型而不是timestamp类型,有两个好处:
1)时间戳由于历史设计原因,在有生之年会达到最大值
2)datetime类型对人类友好,方便后期维护,提高效率
unq_username
是唯一键约束,限制了用户名不能重复
注册逻辑
// 注册
function register()
{
$username = $this->request->post('username');
$password = $this->request->post('password');
// 检查用户是否存在
$exist = (new UserModel())->where('username', $username)->find();
if ($exist) {
return $this->fail('用户名已存在');
}
$salt = Str::random(6); // 随机字符串
$passwordEncrypted = md5($password . $salt); // 计算散列值
$newUser = [
'username' => $username,
'password' => $passwordEncrypted,
'salt' => $salt
];
// 新建用户
(new UserModel())->save($newUser);
return $this->success();
}
登录逻辑
// 登录
function login()
{
$username = $this->request->post('username');
$password = $this->request->post('password');
// 检查用户名
$exist = (new UserModel())->where('username', $username)->find();
if (!$exist) {
return $this->fail('用户名或密码不正确');
}
// 校验密码
$correctPassword = $exist->password;
$inputPassword = md5($password . $exist->salt);
if ($correctPassword != $inputPassword) {
return $this->fail('用户名或密码不正确');
}
return $this->success([
'id' => $exist->id,
'username' => $exist->username,
// 生成TOKEN
'token' => base64_encode(json_encode([
'id' => $exist->id,
'username' => $exist->username,
'expired_at' => time() + 3600, // 登录态有效期一个小时
]))
]);
}
首先校验用户名+密码,两者其一不正确,都返回同样的错误用户名或密码不正确
, 这是出于对业务安全的考虑,防止有恶意用户暴力破解,提供他的作案成本,因为代码不会告诉他是具体是用户名还是密码不正确。
我们来看一下token生成逻辑。这个是给前端缓存下来,之后的每次请求都携带上这个token,后端就能根据token解析出正确的用户信息了。
对于现代化的前后端分离项目来说,与传统的web应用依赖浏览器的能力使用cookie维护登录态的方式不同,这种token登录态的方式更加灵活和便于调试。
这里出于教学目的,只是简单是用来两种编码来实现,即base64_encode和json_encode
, 实际生产环境可能会使用别的方式比如JWT-Token, 他们之间的基本原理是相同的。
获取用户信息
登录成功之后拿到了登录态,我们和客户端约定好,把token放在http协议的请求头也就是header里面传给来,再根据对应的解码方法解析出来用户信息。
// 解析token登录态
function checkToken()
{
$token = $this->request->header('token');
if (empty($token)) {
throw new Exception('请求header缺少token');
}
$tokenInfo = json_decode(base64_decode($token), 'true');
if (!isset($tokenInfo['id'])) {
throw new Exception('token解析失败');
}
if ($tokenInfo['expired_at'] < time()) {
throw new Exception('登录态已过期, 请重新登录');
}
return $tokenInfo;
}
解析可能会异常,统一使用异常捕获处理,如果发生了异常,就把错误信息返回给客户端。如果一切正常,就会走到下面逻辑去查询用户信息。
// 获取用户信息
public function getInfo()
{
try {
$tokenInfo = $this->checkToken();
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
$user = (new UserModel())->field([
'id', 'username', 'nickname', 'created_at', 'updated_at'
])->find($tokenInfo['id']);
return $this->success($user);
}
修改用户信息
最后我们写一个接口来修改用户昵称nickname字段。
同样是需要先校验登录态,再执行修改信息的逻辑。
// 修改用户信息
function updateInfo()
{
try {
$tokenInfo = $this->checkToken();
} catch (\Exception $e) {
return $this->fail($e->getMessage());
}
// 修改用户昵称
$nickname = $this->request->post('nickname');
$user = (new UserModel())->find($tokenInfo['id']);
$user->nickname = $nickname;
$user->save();
return $this->success();
}
TODO
我是一名独立开发者,只工作不上班,带你探索业界正流行的现代化全栈项目。
以下是近期会更新的一系列文章,欢迎关注~
- 用户-登录注册-后端
- 用户-登录注册-前端
- 用户-钱包充值-后端
- 用户-钱包充值-前端
- 营销-签到有礼-后端
- 营销-签到有礼-前端
- 营销-签到有礼-后端单元测试
- 营销-限时秒杀-后端
- 营销-限时秒杀-前端
- 营销-优惠券-后端
- 营销-优惠券-前端
本作品采用《CC 协议》,转载必须注明作者和本文链接
你这样搞都要不了30分钟
我以为算上使用 React/Vue + Ts 写页面和前端逻辑。。。
不过不建议用明文格式将用户 ID 写到 Token 里面,除非你不介意别人猜测你的站点到底多少用户。