# JWT 外加 登入功能

### 用於用戶登入的認證，JWT用的是這套[Tymon\JWTAuth](https://github.com/tymondesigns/jwt-auth)和[Firebase\php-jwt](https://github.com/firebase/php-jwt)

對於JWT相關的認識，請先花10分鐘去瀏覽我在PHP筆記撰寫得一篇[文章](https://judechih.gitbooks.io/php/content/jwtjson-web-token.html)。

## 安裝

在專案資料夾開啟命令提示字元，輸入以下字串：

```
composer require firebase/php-jwt
```

## 產生套件設定

在命令提示字元執行以下字串，就可以將套件的config/jwt.php設定檔複製到我專案底下的設定檔目錄

```
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
```

## 產生JWT Secret Key

每個專案都應該有屬於自己的密鑰，在命令提示字元輸入以下字串便可以得到專屬密鑰

```
php artisan jwt:generate
```

產生的密鑰會設定在 `config/jwt.php` 中的 `secret` 中

```php
// config/jwt.php
[
    'secret' => env('JWT_SECRET', 'your_personal_jwt_secret_key')
]
```

## 範例

這是JWT的controller，放置於App\Http\Controller\Auth資料夾

```php
<?php

namespace App\Http\Controllers\Auth;

use JWTAuth;
use App\Services\AuthService;

define('secret', config('jwt.secret'));
define('algo', config('jwt.algo'));

trait JWTController {

    /**
     * 產生「JWT Token」
     * @param type $userData 使用者登入資料
     * @return type 若為「Null」為產生失敗
     */
    public function generateJWTToken($userData) {
        try {
            $datetimeNow = \Carbon\Carbon::now();
            $tokenId = base64_encode(mcrypt_create_iv(32));
            $issuedAt = $datetimeNow->timestamp;
            $notBefore = $datetimeNow->timestamp;
            $expire = $datetimeNow->addMinutes(10)->timestamp; // 10分鐘
            // $expire = $datetimeNow->addDay()->timestamp; // TEST
            $serverName = 'http://192.168.30.212:8080/'; /// set your domain name
            $data = [
                'iss' => $serverName, //iss: jwt簽發者
                //sub: jwt所面向的用戶
                //aud: 接收jwt的一方
                'exp' => $expire, //exp: jwt的過期時間，這個過期時間必須要大於簽發時間
                'nbf' => $notBefore, //nbf: 定義在什麼時間之前，該jwt都是不可用的.
                'iat' => $issuedAt, //iat: jwt的簽發時間。格式〔timestamp〕
                'jti' => $tokenId, //jti: jwt的唯一身份標識，主要用來作為一次性token,從而迴避重放攻擊。
                //使用者登入資料
                'data' => $userData
            ];
            $secretKey = base64_decode(secret);
            $jwt = JWT::encode($data, $secretKey, algo);
            return $jwt;
        } catch (\Exception $ex) {
            return null;
        }
    }

    /**
     * 檢查Token簽證
     * @return boolean TRUE：驗證通過、FALSE：驗證失敗
     */
    public function checkSignature($jwtToken) {
        try {
            //解碼 驗證Token
            $decodeJWT = JWT::decode($jwtToken, base64_decode(secret), array(algo));
            //檢查是否有「使用者登入資料」
            if (!isset($decodeJWT->data) || !isset($decodeJWT->data->ud_guid) || !isset($decodeJWT->data->ud_name)) {
                return false;
            }
            return true;
        } catch (\Exception $ex) {
            // echo 'catch';
            // echo '<br>';
            return false;
        }
    }

    /**
     * 取得「Token」中的使用者資料
     * @param type $jwtToken
     * @return type
     */
    public function getTokenUserData($jwtToken) {
        try {
            if (!isset($jwtToken)) {
                return null;
            }
            //解碼 驗證Token
            $decodeJWT = JWT::decode($jwttoken, base64_decode(secret), array(algo));
            //檢查是否有「使用者登入資料」
            if (!isset($decodeJWT->data) || !isset($decodeJWT->data->ud_guid) || !isset($decodeJWT->data->ud_name)) {
                return null;
            }
            return $decodeJWT->data;
        } catch (\Exception $ex) {
            return null;
        }
    }
}
```

登入頁面login的controller，放置於App\Http\Controllers\Auth資料夾

```php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use App\Http\Controllers\Auth\JWTController;
use App\Services\AuthService;
use \Firebase\JWT\JWT;

class LoginController extends Controller
{

    use AuthenticatesUsers;
    use JWTController;

    protected $redirectTo = '/index';
    protected $redirectToLogOut = '/login';

    public function __construct()
    {
        $this->middleware('guest', ['except' => 'logout']);
    }

    /**
     * 執行登入
     * @param \Illuminate\Http\Request $request
     * @return boolean
     */
    function login(\Illuminate\Http\Request$request) {
        try {
            //檢查帳號密碼是否有填寫
            if (!isset($request->ud_loginname) || !isset($request->ud_loginpwd)) {
                AuthService::clearToken();
                return redirect()->back()->withInput()->withErrors(['error' => '帳號或密碼錯誤！！']);
            }
            //檢查是否有這個使用者資料
            $userdata = $this->checkUserPassword($request->ud_loginname, $request->ud_loginpwd);
            if (!isset($userdata)) {
                AuthService::clearToken();
                return redirect()->back()->withInput()->withErrors(['error' => '帳號或密碼錯誤！！userdata']);
            }

            //建立「JWT Token」
            $jwttoken = $this->generateJWTToken($userdata);
            if (!isset($jwttoken)) {
                AuthService::clearToken();
                return redirect()->back()->withInput()->withErrors(['error' => '帳號或密碼錯誤！！jwttoken']);
            }
            //儲存「Token」
            AuthService::saveToken($jwttoken, $userdata);

            return redirect('/index');
        } catch (Exception $ex) {
            AuthService::clearToken();
            return redirect()->back()->withInput()->withErrors(['error' => '帳號或密碼錯誤！！']);
        }
    }

    /**
     * 執行登出
     * @param \Illuminate\Http\Request $request
     * @return type
     */
    function logOut(\Illuminate\Http\Request$request) {
        //清除「Token」
        AuthService::clearToken();
        return redirect('/login');
    }

    /**
     * 檢查使用者帳號密碼，並取得使用者資料
     * @param type $userName 使用者帳號
     * @param type $userPassword 使用者密碼
     * @return type 使用者資料 [ ud_id ,ud_name ,auth ]
     */
    private function checkUserPassword($userName, $userPassword) {
        $Ams_userdata = new \App\Repositories\Ams_userdataRepository;
        $userdata = $Ams_userdata->getDataByNickPass($userName, $userPassword);
        if (count($userdata) > 0) {
            // 做成Json格式回傳
            return json_decode(json_encode(['ud_guid' => $userdata[0]->ud_guid, 'ud_name' => $userdata[0]->ud_name]));
        } else {
            return null;
        }
    }
}
```

這是登入時會用到function集中放於這個controller，放置於App\Service資料夾

```php
<?php
namespace App\Services;

use Session;
use App\Http\Controllers\Auth\JWTController;

class AuthService {

    use JWTController;

    private static $sessionToken = 'jwttoken';
    private static $sessionUserData = 'userdata';
    private static $sessionUserName = 'ud_name';
    private static $sessionUserID = 'us_guid';

    /**
     * 儲存「JWT Token」
     * @param type $token
     */
    public static function saveToken($token, $userdata) {

        if (!isset($token) || !isset($userdata)) {
            return \Illuminate\Support\Facades\Redirect::route('logout');
        }

        Session::put(AuthService::$sessionToken, $token);

        Session::put(AuthService::$sessionUserData, $userdata);
        Session::put(AuthService::$sessionUserName, $userdata->ud_name);
        Session::put(AuthService::$sessionUserID, $userdata->ud_guid);

        $userlink = \App\Models\Ams_userauthority::join('ams_function_d','ams_userauthority.fd_id','ams_function_d.fd_id')->where('ams_userauthority.ud_guid',$userdata->ud_guid)->where('ams_userauthority.uda_browse',1)->get();

        Session::put('userlink',$userlink);

        $fm = \App\Models\Ams_function_m::where('fm_type',2)->get();

        Session::put('fm',$fm);
    }

    /**
     * 清除「JWT Token」
     */
    public static function clearToken() {
        Session::flush();
    }

    /**
     * 取得「JWT Token」
     * @return type
     */
    public static function token() {
        return Session::get(AuthService::$sessionToken);
    }

    /**
     * 使用者資料
     * @return type
     */
    public static function userData() {
        return Session::get(AuthService::$sessionUserData);
    }

    /**
     * 使用者名稱
     * @return type
     */
    public static function userName() {
        return Session::get(AuthService::$sessionUserName);
    }

    /**
     * 使用者代碼
     * @return type
     */
    public static function userID() {
        return Session::get(AuthService::$sessionUserID);
    }

    /**
     * 權限「Level」
     * @return type
     */
    public static function authLevel() {
        return Session::get(AuthService::$sessionUserAuth);
    }

    /**
     * 檢查使用者密碼
     * @param type $password
     * @return type
     */
    public static function checkPassword($password) {
        $repository = new \App\Repositories\Ams_userdataRepository;
        return $repository->checkUserPassword(AuthService::userID(), $password);
    }

}
```

JWT會用到的要素，其中頭部跟密鑰我放在config\jwt.php裡面，這樣比較方便修改。如下

```php
<?php

return [

    'secret' => env('JWT_SECRET', 'QowozZ2u98YvnvXawWxTp4C5BpzpHnNT'),

    'algo' => 'HS512',

    'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti']

]
```

以下是登入的流程圖

![](https://1779731781-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LxP_foAutqKlFXcyDah%2F-LxP_hUwaIUyQpJhh5en%2F-LxPa0HSoFsRO__KP8KL%2Flogin.png?generation=1577772936111632\&alt=media)

到這邊我想各位應該還會有其他的疑問：

1. 如果使用者直接在網址後面輸入各頁面的名稱照理是直接導到那個頁面，但是這就違反登入的規則了，而且使用者也有分等級，有些頁面是不能進去的。
2. 再來就是特定頁面指有特定使用者可以進入，這點要防範。

所以這邊順帶提一下，這邊我會用laravel裡面附帶的Middleware(中介層)，透過它來做判斷，如果沒登入就直接輸入網址，還是會被導到登入頁面，如果沒權限就無法進入特定的頁面。

下面是Middleware我自行創建的一個檔案，放置於\App\Http\Middleware資料夾

```php
<?php
namespace App\Http\Middleware;

use Closure;
use Session;
use App\Http\Controllers\Auth\JWTController;
use \App\Services\AuthService;

class AuthUserData {

    use JWTController;

    public function handle($request, Closure $next, $role) {
        $token = \App\Services\AuthService::token();
        if (!isset($token)) {
            return redirect('/logout');
        }
        //檢查「Token」
        if (!$this->checkSignature($token)) {
            return redirect('/logout');
        }

        //展延「Token」
        //產生新的「Token」
        $newToken = $this->generateJWTToken(\App\Services\AuthService::userData());
        //存入Session
        AuthService::saveToken($newToken, \App\Services\AuthService::userData());

        //根據權限進入各個網頁
        //如果沒權限就導到首頁
        $a = 0;
        foreach(Session::get('userlink') as $qqq){
            $aa = \App\Models\ams_function_d::where('fd_id',$qqq->fd_id)->select('fd_name')->get();
            if($aa[0]['fd_name'] == $role){
                $a = 1;
            }
        }
        if($role == 'index' || $role == 'all'){
            $a = 1;
        }

        if($a == 0){
            return redirect('/index');
        }else if($a == 1){
            return $next($request);
        }
    }

}
```

然後在\App\Http\Kernel.php裡面要宣告這個資料夾的名稱，如下

```php
protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,

        // 下面這個就是我們要宣告的名稱
        'userdata' => \App\Http\Middleware\AuthUserData::class,
    ];
```

然後我們要在使用者進入每個頁面之前要先做判斷，所以我們要從各頁面的route下手，在每個route後面加上`->middleware()`

```php
////////////////
///使用者資料///分 一般使用者 跟 管理者
/////////////////////////////////////////////////////////////
Route::get('/UserData','Ams_userdataController@UserData')->middleware('userdata:UserData');
Route::post('/UserDataDetail','Ams_userdataController@UserData')->middleware('userdata:UserData');
/////////////////////////////////////////////////////////////
```

裡面塞要執行哪個檔案，然後要傳什麼值進去，就可以判斷使用者能進去的頁面之中有沒有這個頁面。
