[컴][php] lumen 에서 jwt 사용하기

custom dirver 만들기



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 를 사용하고 싶다면, 이 상황에서는 아래처럼 fromUser 를 사용하면 된다.
$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'));

저렇게 만들어진 token 을 가지고 jwt.auth 를 해도, token 을 가지고 user 를 판단하는 부분을 다르게 타줘야 하는데 그렇지 않다. 그래서 "Method once UsingId does not exist." 란 exception 을 확인하게 된다. 현재로서는 guard 를 변경하는 것이 최선인 듯 하다.
     


<?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 로 넘겨주면 된다.


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 동안 살려둔다.


Custom claims

cutom claim 은 User Model 의 getJWTCustomClaims() 을 이용해서 추가해 주면 된다.(참고: Quick start - jwt-auth)
  1. How can I create a token with custom claims from a user · Issue #1381 · tymondesigns/jwt-auth · GitHub
  2. Get Started with JSON Web Tokens - Auth0 > Payload 부분
  3. 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


References

  1. Installation · tymondesigns/jwt-auth Wiki · GitHub
  2. tymon/jwt-auth with Lumen 5.2 | iWader | Wade Urry
  3. GitHub - iWader/jwt-auth-demo: Code demoing how to use tymon/jwt-auth ^1.0@dev
  4. JSON web token authentication for Lumen 5 using tymon/jwt-auth · Akaita development
  5. 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)

댓글 없음:

댓글 쓰기