最近在开发一个 Android 程序,需要做用户登录和认证功能,另外服务器用的是 Laravel 框架搭建的。最终决定用 JWT 实现API接口的认证。
JWT 是 Json Web Tokens 的缩写,与传统 Web 的 Cookies 或者 Session 方式的认证不同的是,JWT 是无状态的,服务器上不需要对 token 进行存储,也不需要和客户端保持连接。而 JWT 的 token 分3个部分,首先是头部 ,表明这是一个JWT,并指明加密方式,第二部分是负载,其中可以包含 账户名、ID、邮箱等用户信息,同时也包含了token到期时间,以 Unix 时间戳的方式记录,头部和负载都会进行 base64 编码,最后一部分是签名,用来验证负载的信息是否正确。
服务器上会保存一个全局 JWT_SECRET ,用于生成 token 和验证 token。在用户登录成功后,服务器从数据库获取用户的相关信息,计算出 token 到期时间,生成头部和负载并编码,再将前面的内容使用 JWT_SECRET 进行加密,生成签名,最后将3个部分合并返回给客户端。
客户端访问需要认证的客户端时,在 Http 请求头部加上 Authrization 字段,内容为 Bearer 加 token。服务器收到请求后,利用 JWT_SECRET 验证 token 是否合法,从负载中提取到期时间确认 token 是否过期,再从 token 提取用户信息,与数据库进行对比。如果这些都通过的话就可以进行后续操作了。
这里指大概介绍一下 JWT 的特点和验证流程,更详细的介绍大家可以自行搜索,或者访问 https://jwt.io/introduction/ 。
先上代码:
https://github.com/zhongchenyu/jokes-laravel
因为后续代码可能会做重构,本文所介绍的代码保存在 demo2 分支,请 checkout demo2
。
我们从一个空的 Laravel 项目开始着手,这里假设通过 laravel new project_name 命令安装了一个空的项目,并且完成了初始化配置,已经可以访问一些简单的测试 API了。下面开始搞 JWT。
首先通过 composer 安装 PHP 的 JWT 库:
composer require tymon/jwt-auth 0.5.*
在 config/app.php 下添加 JWTAuthServiceProvider:
'providers' => [
/*
* Laravel Framework Service Providers...
*/
...
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class
],
也是在 config/app.php 下,注册门面:
'aliases' => [
...
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],
发布 JWT 的配置文件到 config/jwt.php :
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
生成 JWT_SECRET,执行命令php artisan jwt:generate
,
会在 config/jwt.php 下生成'secret' => env('JWT_SECRET', 'UCDncib***wOY6gj0sD'),
,从 .env 的 JWT_SECRET 取值,如果没有再取后面的默认值,因为 config/jwt.php 是要随着 Git 版本发布的,所有最好在不同的环境上分别执行命令来生成 secret ,并且保持到 .env 文件中, .env 文件默认是 gitignore 的。
这样 JWT 库就安装好可以使用了。
首先创建好数据库,并修改 .env 文件中相关的值:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=jokes
DB_USERNAME=homestead
DB_PASSWORD=secret
这是数据库还是空的,没有任何 table,需要先进行数据库迁移。要做用户认证,肯定需要一个用户表,这个其实 Laravel 在已经帮我们做好的,在 database/migrations 下面已经生成了迁移文件:
看一下 create_users_table.php 的代码,创建一个 users 表,包含 id、name、email、password等列。
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
执行php artisan migrate
命令就会在 Jokes 数据库下创建好 users
表了。
mysql> show columns from users;
+----------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| password | varchar(255) | NO | | NULL | |
| remember_token | varchar(100) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+----------------+------------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
对应的 Model 也已经自动创建好了,就是 app/User.php 文件。
users table 创建好后,当然是要生成 user 数据了,可以用 seeder 来生成测试用的 Faker 数据,初始代码也基本完成了 users 的 seeder。不过我们直接实现注册 API,通过 API 来创建数据。
在 Routes/api.php 下增加一条路由,这里我们使用的是 Dingo/Api :
$api->post('register', 'Auth\RegisterController@register');
访问 register 路径,会调用 Auth\RegisterController.php 控制器下的 register 方法,看下代码:
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6' //|confirmed'
]);
}
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
public function register(Request $request)
{
$this->validator($request->all())->validate();
$user = $this->create($request->all());
$token = JWTAuth::fromUser($user);
return ["token" => $token];
}
其实大部分代码 Laravel 已经自动生成好了,但是由于原来的代码是用于 Web 注册的,注册成功后会重定向到登录页面,但是我们是给 API 做认证,所有就把这块代码改一下。
validator 方法是用来验证 Http 请求参数的。
'name' => 'required|string|max:255'
表示 name 是必须的,并且是最大长度255的字符串。
'email' => 'required|string|email|max:255|unique:users'
表示 email 是必须的,为最长255的邮箱格式的字符串,并且要和 users 数据库中的email不重复。
'password' => 'required|string|min:6'
表示 password是必须的,为最短6位的字符串,也可以加上 confirmed,表示需要二次确认,在请求参数中要在加上 password_confirmation ,并且和 password 是相同的才能通过验证,这里我们就先不用这个确认了。
create 方法将注册的用户信息写到数据库的 users table 中,其中 password 是经过加密存储的。
在 register 方法中, 先调用 validator,检查请求参数是否合法,通过后调用 create 将此用户数据写入数据库,在根据用户信息生成一个 token,返回给客户端。
mysql> select id,name,email,password from users where email = '[email protected]' ;
+----+---------+----------------+--------------------------------------------------------------+
| id | name | email | password |
+----+---------+----------------+--------------------------------------------------------------+
| 9 | user666 | [email protected] | $2y$10$YCj55dl7ByyRP4x6znfM0.6Xsj9ScwF6d5czn1t4RZ59bOQgg0ST6 |
+----+---------+----------------+--------------------------------------------------------------+
1 row in set (0.00 sec)
首先在 Routes/api.php 下添加路由:
$api->get('login', 'Auth\AuthenticateController@authenticate');
看下 Auth\AuthenticateController 下的 authenticate 方法:
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
try {
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
return response()->json(['error' => 'could_not_create_token'], 500);
}
$user = User::where('email', $credentials['email'])->first();
$userTransform = new UserTransformer();
return ['user'=> $userTransform->transform($user), 'token' => $token];
}
代码也是基本初始化好了的,但是原来的代码登录后只返回 token,我们修改下,加上返回 User 信息。
先将 请求中的 email 和 password 存到 $credentials 中,再通过$token = JWTAuth::attempt($credentials)
检验 email 和 password,并尝试转换成 token,如果失败,则返回异常,如果成功则将 user 和 token 返回。
测试一下,用刚才的账号登录,成功获取到用户信息和 token:
用户完成注册登录后,获取到 token,接下来就可以访问需要认证的 API 了,这里我们建一个简单的 API 来说明认证的实现方法。
假设用户需要获取通知信息 notices,服务器要求必须在登录后才能获取。
首先添加路由,这里用到了路由组和中间件(middleware):
在 app/kernel.php 文件下添加路由中间件:
protected $routeMiddleware = [
...
'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
];
$api->group(['middleware' => 'jwt.auth', 'providers' => 'jwt'], function ($api) { //
$api->get('user', 'UserController@getUserInfo');
$api->get('notices', 'NoticeController@index');
});
此 group 下的所有路由,都需要先通过中间件处理,这里用的是 jwt.auth,及只有通过 jwt 认证之后,才能继续后面的访问。
这里只用来测试,NoticeController 下的 index 方法的返回数据直接是一句话:
public function index()
{
return ["content" => "This notice can be seen only after Auth"];
}
测试一下用正确的 token 访问,在 Header 添加 Authrization 项,记住在 token 前加 Bearer ,可以获取数据:
同时我们还添加了 user 路由,JWTAuth::parseToken()->authenticate()
通过 token 获取用户:
class UserController extends Controller{
public function getUserInfo(Request $request)
{
$user = JWTAuth::parseToken()->authenticate();
return ( new UserTransformer())->transform($user);
}
}
请求中添加 Authrization头,测试,这个 API 不仅用来获取用户信息,也作为 客户端存储的 token是否有效的检测 API:
至此,服务器上的认证相关的 API 接口就都准备好了,下篇文章将讲 Android 客户端的实现。