简易 Token 验证的实现
简易 Token 验证的实现
前言
在我们的服务器和客户端的交互中,由于我们的业务中使用 RESTful API 的形式和客户端交互,而 API 又是无状态的,无法帮助我们识别这一次和上一次的请求由谁发出、是否合法,因此我们需要想一个办法来确认用户身份,检查是否请求合法,经调研,较为流行的解决方式是使用 Token 进行验证。
我将介绍如何设计实现一个简单的 Token 验证逻辑,本文的说法仅是基于我自己的一点想法和参考来的知识,如有谬误麻烦不吝指出。
参考资料
基于 Token 的 WEB 后台认证机制:https://www.cnblogs.com/xiekeli/p/5607107.html
Token 设计
有了密码认证为什么需要 Token?
Token 就像一把钥匙,当用户登录之后,服务器就把这把钥匙随着返回的 json 包发送给用户,用户在接下来的请求中,涉及到需要验证身份和权限的,就按照和服务器的约定,把这把钥匙放在请求包合适的地方,随着请求包一起发送,服务器检查 Token 是否合法有效以确认身份,然后决定要执行请求的操作还是拒绝服务并返回错误。这样就用可过期的 Token 替代了每次需要验证身份时都需要发送的重要的、不变的密码。
使用 Token 还有一些其他的优点,例如参考资料中提到的 Token 机制相对于 Cookie 机制有支持跨域访问、无状态 (也称:服务端可扩展行)、更适用 CDN、去耦、更适用于移动应用、CSRF、性能、不需要为登录页面做特殊处理、基于标准化的优点。
Token 里有什么?
Token 的目的是用于表明身份,所以它应该包含一些独特的、只属于此用户的、不容易伪造的信息。例如,创建此 Token 的 Unix 时间戳、用户的唯一 id、唯一账号、经过特殊算法生成的用户识别码等等。但也不能包含一些敏感信息,比如用户的密码。如果用户的明文密码存在于 Token 中,那么有心人劫取并解析 Token 后,就可以直接登录了。
安全性
由上所述,安全性是我们需要考虑的很重要的一部分,我们面临的安全风险主要有跨站脚本攻击(XSS(Cross Site Scripting)Attacks)、请求篡改、重放攻击(Replay Attacks)、中间人攻击(MITM(Man-In-The-Middle)Attacks)。
由于我们使用 RESTful API 的形式,应该不需要考虑 XSS 攻击的事情,只要注意客户端传上来的 json 包内容合法安全即可。如果你有网页需要显示,在 PHP 中使用 htmlspecialchars
函数来避免 XSS 攻击。
请求篡改和中间人攻击的问题,我们可以通过利用 SSL/TLS 来加密数据包,也就是使用 HTTPS。
参考资料中介绍的重放攻击概念如下:
所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程。比如在浏览器端通过用户名 / 密码验证获得签名的 Token 被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的 Token 模拟正常请求,而服务器端对此完全不知道,因为 JWT 机制是无状态的。
解决方式有:
- 时间戳 + 共享秘钥
- 时间戳 + 共享秘钥 + 黑名单
具体可以查看参考资料。
安全只能是相对而言,我们既然是实现简易的 Token 验证,那我认为达到防君子不防小人的效果应该算可以接受了,我们应该根据自己的需要来增强自身安全性,盲目追求安全是不可取的。
比较简单的增强安全性的方式是,给 Token 定义一个过期时间,若 Token 过期将被废弃,若没有过期时间,我认为 Token 就是另一种形式的密码而已。服务器可以参考包含在 Token 中的的过期时间决定是否返回「Token 过期」的错误消息,但要注意这时 Token 不能是明文的,且加密 / 混淆算法需要不可 / 难以破解,否则过期时间可能被伪造。因此我建议生成 Token 过期时间后,将它存于数据库中,验证时不参考 Token 中包含的过期时间(如果有的话)。
Token 明文当然也可,只要能够保证 Token 内容有识别意义且难以被伪造,但我们一般将 Token 信息(一般是一个数组)进行 base64 编码(我们很容易进行解码),以便于传输。
Token 过期后,我们可以要求用户重新登录来刷新 Token,或者提供接口让客户端自动刷新 Token。自动刷新 Token 需要一个 Refresh Token(刷新 Token),它一般和 Token 生成方式类似,但有效期更长(也可以永久有效)且只能用于刷新 Token,不能用于业务验证。
本文中,Token 和 Refresh Token 将是一个包含了 Token 过期时间、由 PHP 函数 uniqid
生成的 uniqid、用户唯一账号信息,并 base64 编码后的字符串,Token 过期时间为 7 天,Refresh Token 过期时间为 14 天,这些时间理论上越短越安全。
注意,以上的简易设计无法解决「重放攻击」,防范方式参考上文。
接口预设
注册接口
客户端将需要注册的账号密码随请求包发往服务器,若注册成功,服务器将为此用户初始化一组 Token、Refresh Token(刷新 Token)、Expire Time(Token 过期时间),并存于数据库。
登录接口
客户端将需要登录的账号密码随请求包发往服务器,若登录成功,服务器将返回用户此时的 Token、Refresh Token(刷新 Token)、Expire Time(Token 过期时间)。
更新用户 Token 接口
根据请求包中的用户刷新 Token 检查是否匹配和过期,若匹配成功且不过期,刷新用户的 Token、Refresh Token(刷新 Token)、Expire Time(Token 过期时间)并返回。
其他逻辑
- 服务器应在每次验证 Token 时检查 Token 是否过期
- 若刷新 Token 有过期时间,在验证刷新 Token 时也要检查,若过期应当要求用户重新登录且刷新用户的 Token、Refresh Token(刷新 Token)、Expire Time(Token 过期时间)
具体代码
在 PHP Laravel 环境下。
/**
* 生成用户 Token、刷新 Token、Token 过期时间
*
* @param User $user
* @return array $tokenInfo
*/
public function refreshToken(User $user)
{
// config('app.token_expires_seconds') 是我们自己定义的 Token 过期时间
$tokenExpireTime = date('Y-m-d H:i:s', time() + config('app.token_expires_seconds'));
$accessTokenInfo = [
'uniqid' => uniqid('', true),
'account' => $user->account,
'tokenExpireTime' => $tokenExpireTime
];
$refreshTokenInfo = [
'uniqid' => uniqid('', true),
'account' => $user->account,
'tokenExpireTime' => $tokenExpireTime
];
$accessToken = base64_encode(implode(',', $accessTokenInfo));
$refreshToken = base64_encode(implode(',', $refreshTokenInfo));
$user->access_token = $accessToken;
$user->access_refresh_token = $refreshToken;
$user->access_token_expires_in = $tokenExpireTime;
$user->save();
$tokenInfo = [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expire_time' => $tokenExpireTime
];
return $tokenInfo;
}
/**
* 注册账号
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function create(Request $request)
{
$account = $request->input('account');
$password = $request->input('password');
$user = new User;
$user->account = $account;
$user->password = Hash::make($password);
$user->save();
$this->refreshToken($user);
return response()->json([
'error_code' => 200,
'data' => [
'user_id' => $user->id,
'account' => $account
]
]);
}
/**
* 登录账号
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function login(Request $request)
{
$account = $request->input('account');
$password = $request->input('password');
$user = User::where('account', $account)->first();
if (!$user) {
return response()->json([
'error_code' => 403,
'error_message' => 'User not exist.'
]);
}
if (!Hash::check($password, $user->password)) {
return response()->json([
'error_code' => 401,
'error_message' => 'Wrong password.'
]);
}
return response()->json([
'error_code' => 200,
'data' => [
'user_id' => $user->id,
'account' => $account,
'access_token' => $user->access_token,
'refresh_token' => $user->access_refresh_token,
'expire_time' => $user->access_token_expires_in
]
]);
}
/**
* 更新用户 Token
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function updateAccessToken(Request $request, User $user)
{
$refreshToken = $request->header('Authorization');
// Refresh token 验证
if ($refreshToken != $user->access_refresh_token) {
return response()->json([
'error_code' => 401,
'error_message' => 'Wrong access refresh token.'
]);
}
// 检查 Refresh token 过期(14 天过期)
if (strtotime($user->access_token_expires_in)
+ config('app.token_expires_seconds') < time()) {
$this->refreshToken($user);
return response()->json([
'error_code' => 403,
'error_message' => 'Refresh token expired.'
]);
}
$tokenInfo = $this->refreshToken($user);
return response()->json([
'error_code' => 200,
'data' => [
'user_id' => $user->id,
'access_token' => $tokenInfo['access_token'],
'refresh_token' => $tokenInfo['refresh_token'],
'expire_time' => $tokenInfo['expire_time']
]
]);
}
/**
* 检查 Token
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function login(Request $request)
{
$token = $request->header('Authorization');
// Token 验证
if ($token != $user->access_token) {
return response()->json([
'error_code' => 401,
'error_message' => 'Wrong access token.'
]);
}
// 检查 Access token 过期(7 天过期)
if (strtotime($user->access_token_expires_in) < time()) {
return response()->json([
'error_code' => 403,
'error_message' => 'Access token expired.'
]);
}
}
改进方式
- 使用各语言 JWT 库进行 Token 验证
- 使用 HTTPS
- 更好的加密解密算法
本文发布于 ladit.me/simple_token_design
简易 Token 验证的实现的更多相关文章
- 服务器通过微信公众号Token验证测试的代码(Python版)
我在阿里云租了一个云服务器,然后想把这个作为我的微信公众号的后台,启用微信公众号开发者需要正确的响应微信服务器的Token验证,为此把这个验证的Python代码贴出来,只要在服务器上运行这段代码,注意 ...
- php开发公众号 token验证失败 其中一个原因
断断续续,弄了好几天,索性一狠心花了三个小时,总算找出问题了. "token验证失败" 可能原因有很多种,其他网友已经几乎穷尽了,但是我所遇到的在网络上没有看到,所以这里记录下. ...
- .NET 微信Token验证和消息接收和回复
public class wxXmlModel { public string ToUserName { get; set; } public string FromUserName { get; s ...
- 【JWT】JWT+HA256加密 Token验证
目录 Token验证 传统的Token验证 JWT+HA256验证 回到顶部 Token验证 最近了解下基于 Token 的身份验证,跟大伙分享下.很多大型网站也都在用,比如 Facebook,Twi ...
- php:微信公众号token验证失败原因、验证码显示不出来的问题
ob_clean(); 问题描述: 用微信官方提供的demo验证token是成功的,但是放到自己网站的框架上进行token验证老是提示"token验证失败",经过检查(用生成日志的 ...
- 基于.Net Framework 4.0 Web API开发(4):ASP.NET Web APIs 基于令牌TOKEN验证的实现
概述: ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作.但是在使用API的时候总会遇到跨域请求的问题, ...
- Token验证失败
Token验证失败 微信 微信公众平台开发 Token校验失败 URL Token原文 http://www.cnblogs.com/txw1958/p/token-verify.html Token ...
- 微信公众平台Token验证失败的解决办法
微信公众平台Token验证失败的解决办法 1.可查看url和token是否正确 2.查看服务器端口是否为80端口 3.你可以通过记录log日志来判断是否接受到微信提交过来的信息 1.$fp=fopen ...
- 微信订阅号开发之token验证后,自动回复消息功能做好,发送消息没有返回
相信很多人会跟我一样,token验证之后,发送消息给订阅号,没有消息返回. 以下,说一下我辛苦调试得到的解决办法: 首先,token验证: 自己写的token一直验证失败,找了好久,没有发现bug.实 ...
随机推荐
- vector、deque、stack、queue、list以及set的使用
注意:以下测试案例都要加上相应的头文件,必要时要加上algorithm文件. 1.vector 连续存储结构,每个元素在内存上是连续的:支持高效的随机访问和在尾端插入/删除操作,但其他位置的插入/删除 ...
- Java的赋值、浅克隆和深度克隆的区别
赋值 直接 = ,克隆 clone 假如说你想复制一个简单变量.很简单: int a= 5; int b= a; b = 6; 这样 a == 5, b == 6 不仅仅是int类型,其它七种原始数 ...
- SpringBoot---基本配置
1.首先在pom.xml添加对HTML的相关依赖 /** * pom.xml文件 */ <dependencies> <dependency> <groupId>o ...
- ElasticSearch聚合分析
聚合用于分析查询结果集的统计指标,我们以观看日志分析为例,介绍各种常用的ElasticSearch聚合操作. 目录: 查询用户观看视频数和观看时长 聚合分页器 查询视频uv 单个视频uv 批量查询视频 ...
- [EOJ439] 强制在线
Description 见EOJ439 Solution 先考虑不强制在线怎么做. 按询问区间右端点排序,从左往右扫,维护所有后缀的答案. 如果扫到 \(a[i]\),那么让统计个数的 \(cnt[a ...
- HTML DOM querySelector() 方法
Document 对象 实例 获取文档中 id="demo" 的元素: document.querySelector("#demo"); <!DOCTY ...
- 多啦爱梦~多啦A梦CSS3测试源代码
先直接看图片,感觉一下!一直以来,我们都在说浏览器对CSS3支持度这个问题.可是,鉴于知识认识水平问题,又没几个人真正了解CSS3是什么东西,和它在网站显示上的重要性.现在好了,日本某位大神写了个CS ...
- yapi部署文档
windows 下 yapi部署文档 安装nodejs 安装mongodb 安装yapi 介绍 随着 web 技术的发展,前后端分离成为越来越多互联网公司构建应用的方式.前后端分离的优势是一套 Api ...
- Python全栈学习_day003知识点
今日大纲: . 基础数据类型 总览 . int . bool . str . for循环 1. 基础数据类型 总览 int: 用于计算,计数等 str:'这些内容',用户少量数据的存储,便于操作 bo ...
- HTML meta头部小结
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...