
在JavaScript前端技術大行其道的今天,我們通常只需在后台構建API提供給前端調用,並且后端僅僅設計為給前端移動App調用。用戶認證是Web應用的重要組成部分,基於API的用戶認證有兩個最佳解決方案 —— OAuth 2.0 和 JWT(JSON Web Token)。
1、JWT定義及其組成
JWT(JSON Web Token)是一個非常輕巧的規范。這個規范允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。
一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
載荷(Payload)
我們先將用戶認證的操作描述成一個JSON對象。其中添加了一些其他的信息,幫助今后收到這個JWT的服務器理解這個JWT。
{
"sub": "1",
"iss": "http://localhost:8000/auth/login",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667"
}
這里面的前6個字段都是由JWT的標准所定義的。
- sub: 該JWT所面向的用戶
- iss: 該JWT的簽發者
- iat(issued at): 在什么時候簽發的token
- exp(expires): token什么時候過期
- nbf(not before):token在此時間之前不能被接收處理
- jti:JWT ID為web token提供唯一標識
這些定義都可以在標准中找到。
將上面的JSON對象進行base64編碼可以得到下面的字符串:
eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ
這個字符串我們將它稱作JWT的Payload(載荷)。
如果你使用Node.js,可以用Node.js的包base64url來得到這個字符串:
var base64url = require('base64url')
var header = {
"from_user": "B",
"target_user": "A"
}
console.log(base64url(JSON.stringify(header)))
注:Base64是一種編碼,也就是說,它是可以被翻譯回原來的樣子來的。它並不是一種加密過程。
頭部(Header)
JWT還需要一個頭部,頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象:
{
"typ": "JWT",
"alg": "HS256"
}
在這里,我們說明了這是一個JWT,並且我們所用的簽名算法(后面會提到)是HS256算法。
對它也要進行Base64編碼,之后的字符串就成了JWT的Header(頭部):
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
簽名(簽名)
將上面的兩個編碼后的字符串都用句號.連接在一起(頭部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ
最后,我們將上面拼接完的字符串用HS256算法進行加密。在加密的時候,我們還需要提供一個密鑰(secret):
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
這樣就可以得到我們加密后的內容:
wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4
這一部分又叫做簽名。
最后將這一部分簽名也拼接在被簽名的字符串后面,我們就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWx ob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDUxODg4MTE5LCJleHAiOjE0NTQ1MTYxMTksIm5iZiI6MTQ1MTg4OD ExOSwianRpIjoiMzdjMTA3ZTQ2MDlkZGJjYzljMDk2ZWE1ZWU3NmM2NjcifQ.wyoQ95RjAyQ2FF3aj8EvCSaUmeP0KUqcCJDENNfnaT4
2、集成JWT到Laravel 5
安裝
我們使用Composer安裝jwt擴展包:
composer require tymon/jwt-auth 0.5.*
配置
安裝完成后,需要在config/app.php中注冊相應的服務提供者:
Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class
然后注冊需要用到的對應門面:
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class 'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class
然后發布相應配置文件:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
最后生成密鑰:
php artisan jwt:generate
如果你想要將其添加到.env文件中,在.env中創建JWT_SECRET字段並再次執行生成密鑰的命令。
在config/jwt.php中,你可以配置以下選項:
- ttl:token有效期(分鍾)
- refresh_ttl:刷新token時間(分鍾)
- algo:token簽名算法
- user:指向User模型的命名空間路徑
- identifier:用於從token的sub中獲取用戶
- require_claims:必須出現在token的payload中的選項,否則會拋出
TokenInvalidException異常 - blacklist_enabled:如果該選項被設置為false,那么我們將不能廢止token,即使我們刷新了token,前一個token仍然有效
- providers:完成各種任務的具體實現,如果需要的話你可以重寫他們
- User —— providers.user:基於sub獲取用戶的實現
- JWT —— providers.jwt:加密/解密token
- Authentication —— providers.auth:通過證書/ID獲取認證用戶
- Storage —— providers.storage:存儲token直到它們失效
創建Token
創建用戶token最常用的方式就是通過登錄實現用戶認證,如果成功則返回相應用戶的token。這里假設我們有一個AuthenticateController:
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthenticateController extends Controller
{
public function authenticate(Request $request)
{
// grab credentials from the request
$credentials = $request->only('email', 'password');
try {
// attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
// all good so return the token
return response()->json(compact('token'));
}
}
有時候我們還可以直接通過用戶對象實例創建token:
// grab some user $user = User::first(); $token = JWTAuth::fromUser($user);
此外,還可以使用Tymon\JWTAuth\PayloadFactory實例(或者JWTFactory門面)基於任意數據創建token:
$customClaims = ['foo' => 'bar', 'baz' => 'bob']; $payload = JWTFactory::make($customClaims); $token = JWTAuth::encode($payload);
還可以使用方法鏈的方式:
// add a custom claim with a key of `foo` and a value of ['bar' => 'baz']
$payload = JWTFactory::sub(123)->aud('foo')->foo(['bar' => 'baz'])->make();
$token = JWTAuth::encode($payload);
用戶認證
用戶登錄成功之后,下一步就是發送一個包含token的請求來獲取用戶信息。
要通過http發送一個需要認證通過的請求,需要設置Authorization頭:
Authorization: Bearer {yourtokenhere}
如果用戶名/密碼沒有進行base64編碼那么Apache似乎會摒棄Authorization頭,要修復這一問題你可以添加如下代碼到Apache配置文件:
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
或者將token信息包含到URL中:
http://api.mysite.com/me?token={yourtokenhere}
要從請求中獲取token,你可以這么做:
// this will set the token on the object JWTAuth::parseToken();// and you can continue to chain methods $user = JWTAuth::parseToken()->authenticate();
要獲取該token值,你可以這么調用:
$token = JWTAuth::getToken();
如果token被設置則會返回,否則會嘗試使用方法從請求中解析token,如果token未被設置或不能解析最終返回false。
當然如果需要的話你還可以手動設置token:
JWTAuth::setToken('foo.bar.baz');
從Token中獲取認證用戶:
// somewhere in your controller
public function getAuthenticatedUser()
{
try {
if (! $user = JWTAuth::parseToken()->authenticate()) {
return response()->json(['user_not_found'], 404);
}
} catch (Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
return response()->json(['token_expired'], $e->getStatusCode());
} catch (Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
return response()->json(['token_invalid'], $e->getStatusCode());
} catch (Tymon\JWTAuth\Exceptions\JWTException $e) {
return response()->json(['token_absent'], $e->getStatusCode());
}
// the token is valid and we have found the user via the sub claim
return response()->json(compact('user'));
}
jwt-auth擴展還提供了兩個中間件GetUserFromToken和RefreshToken,前者用於在請求頭和參數中檢查是否包含token,並嘗試對其解碼,后者會再次從請求中解析token,並順序刷新token(同時廢棄老的token)並將其作為下一個響應的一部分。要使用這兩個中間件,需要到app/Http/Kernel.php下的$routeMiddleware屬性中注冊它們:
protected $routeMiddleware = [ ... 'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken', 'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken', ];
JWT讓用戶認證變得簡單和安全,token會被保存到本地的storage/web或Cookie中,使用JWT,基於API的用戶認證將不再困難。
