API介面JWT方式的Token認證(上),伺服器(Laravel)的實現
簡介
最近在開發一個 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 提取使用者資訊,與資料庫進行對比。如果這些都通過的話就可以進行後續操作了。
伺服器程式碼實現
我們從一個空的 Laravel 專案開始著手,這裡假設通過 laravel new project_name 命令安裝了一個空的專案,並且完成了初始化配置,已經可以訪問一些簡單的測試 API了。下面開始搞 JWT。
1. 安裝 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 庫就安裝好可以使用了。
2. 初始化資料庫
首先建立好資料庫,並修改 .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 檔案。
3. 實現註冊 API
users table 建立好後,當然是要生成 user 資料了,可以用 seeder 來生成測試用的 Faker 資料,初始程式碼也基本完成了 users 的 seeder。不過我們直接實現註冊 API,通過 API 來建立資料。
在 Routes/api.php 下增加一條路由,這裡我們使用的是 Dingo/Api :
$api->post('register', 'Auth\[email protected]');
訪問 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,返回給客戶端。
測試一下,註冊成功,返回 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)
4. 實現登入 API
首先在 Routes/api.php 下新增路由:
$api->get('login', 'Auth\[email protected]');
看下 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:
5. 實現 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', '[email protected]');
$api->get('notices', '[email protected]');
});
此 group 下的所有路由,都需要先通過中介軟體處理,這裡用的是 jwt.auth,及只有通過 jwt 認證之後,才能繼續後面的訪問。
這裡只用來測試,NoticeController 下的 index 方法的返回資料直接是一句話:
public function index()
{
return ["content" => "This notice can be seen only after Auth"];
}
測試一下用正確的 token 訪問,在 Header 新增 Authrization 項,記住在 token 前加 Bearer ,可以獲取資料:
測試用非法 token 訪問,返回400錯誤:
測試過期 token 訪問, 返回401錯誤:
同時我們還添加了 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 客戶端的實現。