lumen 5.4 에서 jwt 사용하기
여기서 jwt 는 tymon/jwt-auth 를 사용한다. 버전은 "tymon/jwt-auth": "1.0.0-beta.3" 이다. 대부분의 사용법은 여기를 참고하였다.jwt-auth 설치
composer require tymon/jwt-auth:"1.0.0-beta.3"
$app->withFacades();
$app->withEloquent();
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);
$app->register(App\Providers\AuthServiceProvider::class);
$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);
config
config 폴더를 만들고 그 안에 auth.php 와 jwt.php 를 넣자. lumen 에서 config 폴더안에 설정파일을 만들어 넣으면 알아서 load하게 된다.jwt.php 는 tymon/jwt-auth의 config 를 이용하면 되고, auth.php 는 아래처럼 설정하자.
참고로 guards.api 라는 값으로 jwt 를 설정했다면, defaults.guard 의 값은 'api' 가 되어야 한다. 그렇지 않으면 $this->jwt->attempt() 를 호출 할 때 "Method once does not exists." 가 발생한다.
$this->jwt->attempt([...]);만약 다른 guard 를 사용하고 싶다면,
$user = User::where([ ['id', '=', $request->mb_id], ['password', '=', $request->password)], ])->first(); if(is_null($user)){ return response()->json(['user_not_found'], 404); } try { $token = JWTAuth::fromUser($user); } catch (TokenExpiredException $e) { return response()->json(['token_expired'], $e->getStatusCode()); } catch (TokenInvalidException $e) { return response()->json(['token_invalid'], $e->getStatusCode()); } catch (JWTException $e) { return response()->json(['token_absent' => $e->getMessage()], $e->getStatusCode()); } return response()->json(compact('token'));
<?php return [ 'defaults' => [ 'guard' => env('AUTH_GUARD', 'api'), ], 'guards' => [ // 'api' => ['driver' => 'api'], 'api' => [ 'driver' => 'jwt', 'provider' => 'users', ], ], 'providers' => [ // 'users' => [ 'driver' => 'eloquent', 'model' => App\MyMember::class, ], ], 'passwords' => [ // ], ];
driver
여기서는 eloquent 를 사용한다고 했지만, eloquent 는 table 의 password column 이름이 'password'로 고정되어 있다. 그리고 Hash 를 사용한 암호화가 되어 있어야 한다. 여하튼 그런 이유로 table 의 조건이 맞지 않는다면, 자신의 driver 를 만들어야 한다. custom driver 는 아래 글을 참고하자.User Model
자신이 사용하는 User Model 을 아래처럼 만들어준다. 당연히 auth.php 의 설정에도 이 model 이 사용돼야 한다.class MyMember extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject{
use Authenticatable, Authorizable;
...
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
Header Authorization 문제
header 에 token 을 실어서 날리면 될 것 같았는데 문제가 좀 있다. 먼저 Apache 설정 문제로 안날라오는 경우도 있는데 그것은 아래 링크를 참조하면 된다.
내 문제는 header 에 authorization 이 들어있지만, $this->jwt->parseToken() 에서 header 를 parsing 할 때 사용하는 $request 에는 header 정보가 보이지 않는 것이었다.
여하튼 그래서 결국 parseToken 을 사용하는 것을 포기하고, setToken 과 toUser 를 사용했다.
다시 말하면, 일단 token 은 GET request 의 parameter 로 넘기고, 이것을 auth middleware 를 사용하지 않고, 각각의 controller 의 함수내에서 auth 를 check 했다.
만약 jwt.auth middleware 를 사용하면, 그냥 token 만 GET request 의 parameter 로 넘겨주면 된다.
다시 말하면, 일단 token 은 GET request 의 parameter 로 넘기고, 이것을 auth middleware 를 사용하지 않고, 각각의 controller 의 함수내에서 auth 를 check 했다.
만약 jwt.auth middleware 를 사용하면, 그냥 token 만 GET request 의 parameter 로 넘겨주면 된다.
public function loginPost(Request $request) { $this->getAuthenticatedUser($request->token); $this->validate($request, [ 'mb_id' => 'required|email|max:255', 'password' => 'required', ]); ... } public function getAuthenticatedUser(string $token) { $this->jwt->setToken($token); try { if (! $user = $this->jwt->toUser()) { return response()->json(['user_not_found'], 404); } } catch (TokenExpiredException $e) { // 401 Unauthorized return response()->json(['token_expired'], 401); } catch (TokenInvalidException $e) { // 401 Unauthorized return response()->json(['token_invalid'], 401); } catch (JWTException $e) { // 401 Unauthorized return response()->json(['token_absent' => $e->getMessage()], 401); } // the token is valid and we have found the user via the sub claim return $user; }
AuthController
이 authcontroller 에서 postLogin 을 확인하면 된다. postLogin 에서는 $this->jwt->attempt() 를 이용해서 첫token 을 생성하게 된다.
Middleware
jwt-auth 에서 2개의 middleware 를 제공한다. 이 2개는 Authenticate 과 RefreshToken 이다. 아래처럼 app.php 에서 설정해 주면 사용할 수 있다.middleware 의 사용법은 아래 링크를 참고하자.
- Authenticate 은 일반적인 Authenticate 처럼 valid 한 token 을 가지고 있는지 여부를 검사해 주고,
- RefreshToken 은 valid 한 token 을 가졌는지 검사한 후
- valid 한 token 을 가진 request 가 왔을때 token 을 refresh 해서 response 를 날려준다.
- expired token 인 경우에는 refresh ttl 이내로 들어온 token 인 경우에 refresh 해서 response 를 날려준다.
// app.php $app->routeMiddleware([ 'auth' => App\Http\Middleware\Authenticate::class, 'jwt.auth' => Tymon\JWTAuth\Middleware\Authenticate::class, 'jwt.refresh' => Tymon\JWTAuth\Middleware\RefreshToken::class, ]);
jwt.refresh 의 race condition, concurrency
처음에 jwt.refresh 로 모든 api auth 를 설정하면 될 것이라고 생각했는데, 이 경우에 한 client에서 동시에 2개의 request 를 날리면, 한쪽은 token 이 expired 돼 버리는 문제가 있다.이 글이 비슷한 이야기를 하고 있다. How to Refresh JWT Token 글을 통해 내린 결론은 jwt.refresh 대신에 refresh 할 controller 를 따로 두고, refresh 를 token 의 validate 시간이 다돼서 expired 되면, client 에서 다시 요청을 하는 방식으로 구현을 하는 것이다.
관련해서 아래 자료를 참고하면 될듯 하다.
rc.1 버전부터 JWT_BLACKLIST_GRACE_PERIOD 가 지원된다고 한다. 이것을 이용해서 expired 된 token 을 JWT_BLACKLIST_GRACE_PERIOD 동안 살려둔다.
- Laravel 5.5 + Vue.js 2 + JWT Auth 1.0.0-rc.1 · Issue #1355 · tymondesigns/jwt-auth
- When I do refresh token? · Issue #872 · tymondesigns/jwt-auth
Custom claims
cutom claim 은 User Model 의 getJWTCustomClaims() 을 이용해서 추가해 주면 된다.(참고: Quick start - jwt-auth)
- How can I create a token with custom claims from a user · Issue #1381 · tymondesigns/jwt-auth · GitHub
- Get Started with JSON Web Tokens - Auth0 > Payload 부분
- 4.1. Registered Claim Names | RFC 7519 - JSON Web Token (JWT)
사용법
jwt.auth 로 모든 auth 관련 처리를 하면 된다. 그리고 client 쪽에서 jwt.auth 로 보냈는데 401 status code 가 return 될 때만 jwt.refresh 를 사용해서 token 을 업데이트한 후 다시 jwt.auth 로 처리하도록 하면 된다.
tymondesigns/jwt-auth 작성자의 comment 를 보면, 이 작성자의 design concept 을 확인할 수 있다.
설정에 보면 아래 2개의 ttl 이 있다. 대략적으로 설명하면 아래와 같다.
- JWT_TTL: 이 시간동안 token 이 유효하다.
- JWT_REFRESH_TTL: 이 시간동안 token 을 refresh 할 수 있다.
보통 JWT_REFRESH_TTL 이 길어야 한다. 그래야 JWT_TTL 동안에 token 을 사용하다가, 만료가 되면 다시 refresh 할 수 있기 때문이다.
Example
아래처럼 처음에 authentication 을 하고나면, token 을 얻게 된다. 그리고 이 token 을 전달해서 refresh token 이 되면, response header 에 새로운 token 을 던져준다.- request 1st:
- http://localhost:8082/auth/loginUser?mb_id=test@test.com&password=1234
- response:
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODIvYi9hdXRoL2xvZ2luIiwiaWF0IjoxNTA2MTM2Njg1LCJleHAiOjE1MDYxNDAyODUsIm5iZiI6MTUwNjEzNjY4NSwianRpIjoiQWNkNWx6QzRGelAxQ1ZQOCIsInN1YiI6MTQzfQ.VX18T7noaLiQ-bVqy0fjKjnUXRnSbgarO4JN5NwGpJ4"} - request 2nd:
- http://localhost:8082/auth/refreshTest?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODIvYi9hdXRoL2xvZ2luIiwiaWF0IjoxNTA2MTM2Njg1LCJleHAiOjE1MDYxNDAyODUsIm5iZiI6MTUwNjEzNjY4NSwianRpIjoiQWNkNWx6QzRGelAxQ1ZQOCIsInN1YiI6MTQzfQ.VX18T7noaLiQ-bVqy0fjKjnUXRnSbgarO4JN5NwGpJ4
- response header :
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODIvYi9hdXRoL2xvZ2luMiIsImlhdCI6MTUwNjEzNzI1OCwiZXhwIjoxNTA2MTQwODY4LCJuYmYiOjE1MDYxMzcyNjgsImp0aSI6IkQ1cHVYQVFJeDdVUGZqMm0iLCJzdWIiOjE0M30.GsBriJ305x69xhGG76a1FReGIyyZhi9v3XxQJu6lo-A
<? class AuthController extends Controller { /** * @var \Tymon\JWTAuth\JWTAuth */ protected $jwt; public function __construct(JWTAuth $jwt) { $this->jwt = $jwt; $this->middleware('jwt.refresh', ['only'=>[ 'refreshTest', ]]); } public function loginUser(Request $request) { //$this->getAuthenticatedUser($request->token); $this->validate($request, [ 'mb_id' => 'required|email|max:255', 'password' => 'required', ]); try { if (! $token = $this->jwt->attempt( ['mb_id' => $request->mb_id, 'mb_password' => $request->password,])) { return response()->json(['user_not_found'], 404); } } catch (TokenExpiredException $e) { return response()->json(['token_expired'], $e->getStatusCode()); } catch (TokenInvalidException $e) { return response()->json(['token_invalid'], $e->getStatusCode()); } catch (JWTException $e) { return response()->json(['token_absent' => $e->getMessage()], $e->getStatusCode()); } return response()->json(compact('token')); } public function refreshTest(Request $request) { return response()->json(['refresh-token test']); } }
See Also
- Lumen 의 auth.php 의 설정 : config/auth.php 에 대한 설명이다. Tymon\JWTAuth 의 설정과 관련된 초기 동작에 대한 글도 같이 있다.
- https://randomkeygen.com/ : JWT_KEY 생성할 때 사용하면 된다.
References
- Installation · tymondesigns/jwt-auth Wiki · GitHub
- tymon/jwt-auth with Lumen 5.2 | iWader | Wade Urry
- GitHub - iWader/jwt-auth-demo: Code demoing how to use tymon/jwt-auth ^1.0@dev
- JSON web token authentication for Lumen 5 using tymon/jwt-auth · Akaita development
- GitHub - akaita/json-web-token-authentication-for-lumen-5: Easy to follow example of JWT integration for Lumen/Laravel 5.x using the latest mechanisms (guards)
댓글 없음:
댓글 쓰기