最近在開發(fā)一個 Android 程序,需要做用戶登錄和認(rèn)證功能,另外服務(wù)器用的是 Laravel 框架搭建的。最終決定用 JWT 實現(xiàn)API接口的認(rèn)證。
JWT 是 Json Web Tokens 的縮寫,與傳統(tǒng) Web 的 Cookies 或者 Session 方式的認(rèn)證不同的是,JWT 是無狀態(tài)的,服務(wù)器上不需要對 token 進(jìn)行存儲,也不需要和客戶端保持連接。而 JWT 的 token 分3個部分,首先是頭部 ,表明這是一個JWT,并指明加密方式,第二部分是負(fù)載,其中可以包含 賬戶名、ID、郵箱等用戶信息,同時也包含了token到期時間,以 Unix 時間戳的方式記錄,頭部和負(fù)載都會進(jìn)行 base64 編碼,最后一部分是簽名,用來驗證負(fù)載的信息是否正確。
服務(wù)器上會保存一個全局 JWT_SECRET ,用于生成 token 和驗證 token。在用戶登錄成功后,服務(wù)器從數(shù)據(jù)庫獲取用戶的相關(guān)信息,計算出 token 到期時間,生成頭部和負(fù)載并編碼,再將前面的內(nèi)容使用 JWT_SECRET 進(jìn)行加密,生成簽名,最后將3個部分合并返回給客戶端。
客戶端訪問需要認(rèn)證的客戶端時,在 Http 請求頭部加上 Authrization 字段,內(nèi)容為 Bearer 加 token。服務(wù)器收到請求后,利用 JWT_SECRET 驗證 token 是否合法,從負(fù)載中提取到期時間確認(rèn) token 是否過期,再從 token 提取用戶信息,與數(shù)據(jù)庫進(jìn)行對比。如果這些都通過的話就可以進(jìn)行后續(xù)操作了。
這里指大概介紹一下 JWT 的特點和驗證流程,更詳細(xì)的介紹大家可以自行搜索,或者訪問 https://jwt.io/introduction/ 。
先上代碼:
https://github.com/zhongchenyu/jokes-laravel
因為后續(xù)代碼可能會做重構(gòu),本文所介紹的代碼保存在 demo2 分支,請 checkout demo2
。
我們從一個空的 Laravel 項目開始著手,這里假設(shè)通過 laravel new project_name 命令安裝了一個空的項目,并且完成了初始化配置,已經(jīng)可以訪問一些簡單的測試 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, ],
發(fā)布 JWT 的配置文件到 config/jwt.php :
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
生成 JWT_SECRET,執(zhí)行命令php artisan jwt:generate
,
會在 config/jwt.php 下生成'secret' => env('JWT_SECRET', 'UCDncib***wOY6gj0sD'),
,從 .env 的 JWT_SECRET 取值,如果沒有再取后面的默認(rèn)值,因為 config/jwt.php 是要隨著 Git 版本發(fā)布的,所有最好在不同的環(huán)境上分別執(zhí)行命令來生成 secret ,并且保持到 .env 文件中, .env 文件默認(rèn)是 gitignore 的。
這樣 JWT 庫就安裝好可以使用了。
首先創(chuàng)建好數(shù)據(jù)庫,并修改 .env 文件中相關(guān)的值:
DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=jokesDB_USERNAME=homesteadDB_PASSWORD=secret
這是數(shù)據(jù)庫還是空的,沒有任何 table,需要先進(jìn)行數(shù)據(jù)庫遷移。要做用戶認(rèn)證,肯定需要一個用戶表,這個其實 Laravel 在已經(jīng)幫我們做好的,在 database/migrations 下面已經(jīng)生成了遷移文件:
看一下 create_users_table.php 的代碼,創(chuàng)建一個 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(); }); }
執(zhí)行php artisan migrate
命令就會在 Jokes 數(shù)據(jù)庫下創(chuàng)建好 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)
對應(yīng)的 Model 也已經(jīng)自動創(chuàng)建好了,就是 app/User.php 文件。
users table 創(chuàng)建好后,當(dāng)然是要生成 user 數(shù)據(jù)了,可以用 seeder 來生成測試用的 Faker 數(shù)據(jù),初始代碼也基本完成了 users 的 seeder。不過我們直接實現(xiàn)注冊 API,通過 API 來創(chuàng)建數(shù)據(jù)。
在 Routes/api.php 下增加一條路由,這里我們使用的是 Dingo/Api :
$api->post('register', 'Auth\RegisterController@register');
訪問 register 路徑,會調(diào)用 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 已經(jīng)自動生成好了,但是由于原來的代碼是用于 Web 注冊的,注冊成功后會重定向到登錄頁面,但是我們是給 API 做認(rèn)證,所有就把這塊代碼改一下。
validator 方法是用來驗證 Http 請求參數(shù)的。
'name' => 'required|string|max:255'
表示 name 是必須的,并且是最大長度255的字符串。
'email' => 'required|string|email|max:255|unique:users'
表示 email 是必須的,為最長255的郵箱格式的字符串,并且要和 users 數(shù)據(jù)庫中的email不重復(fù)。
'password' => 'required|string|min:6'
表示 password是必須的,為最短6位的字符串,也可以加上 confirmed,表示需要二次確認(rèn),在請求參數(shù)中要在加上 password_confirmation ,并且和 password 是相同的才能通過驗證,這里我們就先不用這個確認(rèn)了。
create 方法將注冊的用戶信息寫到數(shù)據(jù)庫的 users table 中,其中 password 是經(jīng)過加密存儲的。
在 register 方法中, 先調(diào)用 validator,檢查請求參數(shù)是否合法,通過后調(diào)用 create 將此用戶數(shù)據(jù)寫入數(shù)據(jù)庫,在根據(jù)用戶信息生成一個 token,返回給客戶端。
測試一下,注冊成功,返回 token:
mysql> select id,name,email,password from users where email = 'user6@user.com' ;+----+---------+----------------+--------------------------------------------------------------+| id | name | email | password |+----+---------+----------------+--------------------------------------------------------------+| 9 | user666 | user6@user.com | $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,并嘗試轉(zhuǎn)換成 token,如果失敗,則返回異常,如果成功則將 user 和 token 返回。
測試一下,用剛才的賬號登錄,成功獲取到用戶信息和 token:
用戶完成注冊登錄后,獲取到 token,接下來就可以訪問需要認(rèn)證的 API 了,這里我們建一個簡單的 API 來說明認(rèn)證的實現(xiàn)方法。
假設(shè)用戶需要獲取通知信息 notices,服務(wù)器要求必須在登錄后才能獲取。
首先添加路由,這里用到了路由組和中間件(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 認(rèn)證之后,才能繼續(xù)后面的訪問。
這里只用來測試,NoticeController 下的 index 方法的返回數(shù)據(jù)直接是一句話:
public function index() { return ["content" => "This notice can be seen only after Auth"]; }
測試一下用正確的 token 訪問,在 Header 添加 Authrization 項,記住在 token 前加 Bearer ,可以獲取數(shù)據(jù):
測試用非法 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:
至此,服務(wù)器上的認(rèn)證相關(guān)的 API 接口就都準(zhǔn)備好了,下篇文章將講 android 客戶端的實現(xiàn)。