Recently, I have been busy with the small program of the company and some H5 development of wechat. Of course, as the back-end, I have always just written simple logic. Because it has been mixed before the development, the heart is really tired ah. Therefore, I also want to improve the technical level of the group, and also to better division of labor, so I want to explore the mode of separating the front and back end.

Although I know that it takes great effort to learn something new. In order to avoid detours in the future, I took some time to learn fragments of API development when the project progress met expectations. This article draws on the examples of many peers, but also combined with their own analysis. I hope you can find something in it.

When talking about API development, I think the first problem to be solved is the state of the user. After all, HTTP is a stateless protocol. When we build API, we cannot store the user’s data in session and then perform login verification like common application and system development. For this reason, You God came up with a similar solution to JWT, which uses token to sense user authorization and status.

Before introducing JWT, let’s take a look at the traditional server-side way of using sessions to authorize multiple users. Of course, before the session, there are ways to store user authorization information on the client (such as the browser), but the pure cookie method is too insecure, so let’s say cookie with session.

Authorization is needed because HTTP is stateless.

That is to say, when a user sends a request, the request attached account name and password after login successfully, if this this user again to send a request, the server is not know this user is already logged in, this time the server also requires the user to provide authorization information again, also is the user name and password.

If the client can transmit its identity authorization information less over the network, the client can avoid its identity information being leaked to a greater extent.

Both sessions and tokens are designed to solve this problem.

Session Principle overview

The certification process

  1. When a user logs in using the username and password, the server generates a session file, which stores the authorization information of the user. This file can be stored in the hard disk, memory, or database.
  2. At the same time, a sessionid corresponding to the session file should be generated, and the session file can be found by the session ID.
  3. Then send the sessionID to the client, and the client saves the sessionID in various ways. At present, most cases save the sessionID through cookies.
  4. After saving, when the client sends a request to the server later, the request will carry the sessionID, so that the server will find the corresponding session file in the service area after receiving the SessionID. If the search succeeds, the user’s authorization information will be obtained. This completes an authorization.

The emergence of session solves part of the problems, but with the passage of time and the development of the Internet, some defects are also exposed, such as but not limited to the following points

  • As the number of users increases, each user needs to create a session file on the server, which puts pressure on the server
  • For server load diversion, if a user’s session is stored on a certain server, when the user accesses the server, the user can only complete authorization on this server, and other traffic diversion servers cannot distribute such requests
  • The same is also the problem of session storage, when we successfully log in on one server, if we want another server with a different domain name to also allow users to complete authorization without logging in, this time there will be a lot of trouble

To solve such problems, tokens came into being.

Overview of Token Principle

The certification process

  1. The client sends authentication information (typically a user name/password) and sends a request to the server
  2. The server authenticates the client’s authentication information. After successful authentication, the server returns an encrypted token(usually a string) to the client.
  3. The client stores this token (cookie, session, app) and carries it with it every time it sends a request to the server
  4. The server verifies that the token is valid, and once verified, the server considers the request to be a valid request

JWT overview

Token is just a way of thinking, a way of thinking to solve the problem of user authorization. Based on this way of thinking, there can be many implementations for different scenarios. Among the many implementations, JWT(JSON Web Token) is the most popular.

The JWT standard provides a set of methods and specifications for creating specific tokens that make the process of creating tokens more rational and efficient.

For example, in traditional practices, the server saves the generated token and compares it with the server when the client sends the token, but JWT does not need to store any token on the server. Instead, it uses a set of encryption/decryption algorithm and a key to decrypt the token sent by the user. Once decrypted successfully, the user’s information can be retrieved.

This method also increases the scalability of multiple servers. In traditional token authentication, once a user sends a token, it is necessary to find the server where the token is stored and verify the user’s identity by that server. With the existence of JWT, as long as each server knows the decryption key, each server can have the ability to verify user identity.

In this way, the server no longer stores any user authorization information, thus solving the session problem.



With a brief introduction to JWT, let’s take a quick look at how JWT is used in a real world scenario.


Laravel

1. Install the software using Composer

Version 1.0 or later is recommended
composer require tymon/jwt-auth 1.*@rc
Copy the code

2. Perform some configurations

Here refers to note is that some documents will be said to add Tymon \ JWTAuth \ will \ LaravelServiceProvider: : class, this only in Laravel version 5.4 and below are necessary, updated Laravel version do not need to add.

There is also documentation about adding Tymon\JWTAuth\Providers\JWTAuthServiceProvider, which is a version of JWT from a long time ago (around 0.5.3).

2.1 Publishing the Configuration File

# this command will add a config file to jwt.php under config
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
Copy the code

2.2 Generating an Encryption Key

This command generates an encryption key in the.env file, such as JWT_SECRET=foobar
php artisan jwt:secret
Copy the code

2.3 Update your model

If you use the default User table to generate tokens, you need to add a piece of code under the model

<? php namespace App; use Tymon\JWTAuth\Contracts\JWTSubject; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable implements JWTSubject# Don't forget to add here
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored inThe subject claim of the JWT. * Retrieve the identifier token to be stored in 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. This contains any custom declarations to add to the JWTreturn array
     */
    public function getJWTCustomClaims()
    {
        return[]; }}Copy the code

The above two methods actually implement what the JWTSubject excuse must implement. This can also be configured according to the official document.

2.4 Register two facades

These two facades are not required, but using them will make your code a little easier.

config/app.php

'aliases'=> [... // Add the following two lines'JWTAuth'= >'Tymon\JWTAuth\Facades\JWTAuth'.'JWTFactory'= >'Tymon\JWTAuth\Facades\JWTFactory',].Copy the code

If you do not use these two facades, you can use the auxiliary function auth() instead.

Auth () is a helper function that returns a guard, which can temporarily be seen as an Auth Facade.

About Auth Facade. I suggest you refer to this article. I also read it several times before I could understand it when I was learning JWT.

Laravel auxiliary function Auth and JWT extension details


2.5 modify the auth. PHP

config/auth.php

'guards'= > ['web'= > ['driver'= >'session'.'provider'= >'users',].'api'= > ['driver'= >'jwt'// Change the token to JWT'provider'= >'users',]],Copy the code

2.6 Registering Some Routes

Note: Under Laravel, routes in route/api.php have prefix apis by default.

Route::group([

    'prefix'= >'auth'].function ($router) {

    Route::post('login'.'AuthController@login');
    Route::post('logout'.'AuthController@logout');
    Route::post('refresh'.'AuthController@refresh');
    Route::post('me'.'AuthController@me');

});
Copy the code

2.7 Creating a Token Controller

php artisan make:controller AuthController
Copy the code

AuthController

Note that Laravel uses auth(‘ API ‘)

<? php namespace App\Http\Controllers; use Illuminate\Support\Facades\Auth; use App\Http\Controllers\Controller; Class AuthController extends Controller {/** * Get a JWT via given credentials. * This method is used to generate tokens * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email'.'password']);

        if (! $token = auth('api')->attempt($credentials)) {
            return response()->json(['error'= >'Unauthorized'], 401);
        }

        return $this->respondWithToken($token); } /** * Get the authenticated User. * this method uses token to obtain the corresponding User information * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth('api')->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth('api') - >logout(a);return response()->json(['message'= >'Successfully logged out']); } /** * Refresh a token. * Refresh a token. If the blacklist is enabled, the previous token becomes invalid. * It is important to note that obtaining a Token again using the getToken above does not count as a refresh. Both tokens are obtained in parallel, i.e. both are available. * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token* This method returns output in the specified format * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token'= >$token.'token_type'= >'bearer'.'expires_in' => auth('api')->factory()->getTTL() * 60 ]); }}Copy the code

JWT Token,

1. Obtain, use, delete, and refresh the token

  • Here’s a postman demo,
  • Routes written to api.php in Laravel have prefix apis by default

1.1 access token



1.2 use the token

There are two ways to use it:

  • Add to url:? Token = your token
  • Add to header. This is recommended because it is more secure in HTTPS:Authorization: Bearer your token



1.3 delete the token



After the token is deleted, the token becomes invalid and cannot be used to obtain data.


1.4 the refresh token




At this point we have successfully demonstrated the JWT development example, of course I will continue to show some common methods, as well as some special articles on JWT data structures.

JWT implementation principle

2. Token creation

The previous two lines in authController.php show how to create a token by trying the account and password given by the User. If the password is correct, a token is returned with the corresponding User information.

However, there is more than one way to create a token. Here are three ways to create a token:

  • Based on account secret parameters
  • An instance returned based on the Users model
  • Based on the user primary key ID in the Users model

A) Based on account secret parameters

That’s what I just said. Post the code.

// Use the helper function$credentials = request(['email'.'password']); 
$token = auth('api')->attempt($credentials// Use a Facade$credentials = $request->only('email'.'password');
$token = JWTAuth::attempt($credentials);
Copy the code

B) Instances returned based on the Users model

// Use the helper function$user = User::first();
$token = auth('api')->login($user); / / use the Facade$user = User::first();
$token = JWTAuth::fromUser($credentials);
Copy the code

C) Based on the primary key ID in the Users model

// Use the helper function$token = auth('api')->tokenById(1);
Copy the code

2.1 Token Resolution

A) Parse token to object

Only Facade needs that.

// Parse the request sent directly to the object JWTAuth::parseToken();Copy the code

B) Obtain the user information in the token

// Auxiliary functions$user = auth()->user();

// Facade
$user = JWTAuth::parseToken()->authenticate();
Copy the code

C) access token

Returns if the token is set, otherwise attempts to resolve the token from the request using methods, and returns false if the token is not set or cannot be resolved.

// Auxiliary functions$token = auth()->getToken();

// Facade
$token = JWTAuth::parseToken()->getToken();
Copy the code

D) If front-end

The required information can be obtained by directly base64 decoding the first two segments of the token.


3. Load setting and acquisition

A) Load setting

Payload information is obtained when tokens are decoded, and larger arrays generate longer tokens, so it is not recommended to put too much data. At the same time, because the payload is encoded with Base64Url, it is equivalent to plaintext, so it must not put passwords and other sensitive information.

$customClaims = ['foo'= >'bar'.'baz'= >'bob']; // Auxiliary functions$token = auth()->claims($customClaims)->attempt($credentials);

// Facade - 1
$token = JWTAuth::claims($customClaims)->attempt($credentials);
Copy the code

B) Load analysis

Parse out the payload from the request. You can go to the extension source code, there are many methods.

// Auxiliary functions$exp = auth()->payload()->get('exp');
$json = auth()->payload()->toJson();
$array = auth()->payload()->jsonSerialize();
$sub = $array['sub'];

// Facade - 1
$payload = JWTAuth::parseToken()->getPayload();
$payload->get('sub'); / / = 123$payload['jti']; / / ='asfe4fq434asdf'
$payload('exp') / / = 123456$payload->toArray(); / / = ['sub'= > 123,'exp'= > 123456,'jti'= >'asfe4fq434asdf'] etc

// Facade - 2
$exp = JWTAuth::parseToken()->getClaim('exp');
Copy the code

4. Token’s three times

A token typically has three time attributes, all of which are configured in config/jwt.php.

Valid time

The validity period refers to how long you can use the token to access content after you have acquired it.

// Unit: minutes'ttl' => env('JWT_TTL', 60)
Copy the code

The refresh time

Refresh time refers to the time during which the old token can be exchanged for a new token. For example, if the validity period of the token is 60 minutes and the refresh period is 20160 minutes, you can obtain a new token using this token within 60 minutes but cannot obtain a new token after 60 minutes. Then, you can obtain a new token repeatedly until the total time exceeds 20160 minutes.

// Unit: minutes'refresh_ttl' => env('JWT_REFRESH_TTL', 20160).Copy the code

Grace time

The grace period is used to solve the problem of concurrent requests. If the grace period is 0s, concurrent requests will fail during the transfer of the old and new tokens. Therefore, you need to set a grace period

// During the grace period, you need to enable the blacklist function (this function is enabled by default). After the blacklist function expires, the token cannot be used any more. Therefore, enable the blacklist function'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED'.true) // Set the grace period in seconds'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 60)
Copy the code

5. Token refresh problem?

A) Why should tokens be refreshed?

First of all, Basic Auth is the simplest authentication method. However, as each request carries a user name and password, frequent transmission is not secure, so cookies and sessions are used. If the token is not refreshed, then the token is equivalent to the above user name + password. As long as the token is obtained, it can always be stolen. Therefore, it is necessary to set the validity period and refresh the token.

B) How long is the validity period of token and how long is the refresh frequency appropriate?

The longer the validity period, the higher the risk, the shorter the validity, and the higher the refresh frequency, the refresh cost will exist, so this needs to be considered comprehensively. My personal general consideration range is: 15min ~ 120min.

C) Is it necessary to refresh tokens every time?

The solution available at JWT is to refresh the token once after each request, but this creates a new problem: concurrent requests. A concurrent request is a token, and the first completed request will cause all subsequent requests to fail. The available solution is to set a grace period, where a token is refreshed and the old token is still available for a short time. Unfortunately, this is not a perfect solution to replay attacks, but increases the cost of rogue attacks. This problem is not well addressed in JWT.

Here are the middleware available. The first two functions are the same, but the second does not throw an error, and the third and fourth functions are the same.

tymon\jwt-auth\src\Providers\AbstractServiceProvider.php

protected $middlewareAliases = [
    'jwt.auth' => Authenticate::class,
    'jwt.check' => Check::class,
    'jwt.refresh' => RefreshToken::class,
    'jwt.renew' => AuthenticateAndRenew::class,
];
Copy the code

5.1 Summary of Token Refreshing

Since replay attacks cannot be completely resolved, other security authentication measures are recommended in areas where replay attacks may cause significant security problems and losses. The recommended Settings for daily Api use are as follows:

Validity period: 15 to 120 minutes Refresh time: 7 to 30 days Grace period: 60 secondsCopy the code


Other common methods appendix

1. Two JWT facades

1.1 JWTAuth

JWTAuth::parseToken()-> method () can generally be replaced with auth()-> method ().

Token is generated

attempt

Create a token based on the user account secret.

$credentials = $request->only('email'.'password');
$token = JWTAuth::attempt($credentials);Copy the code

fromUser or fromSubject

Generates a token based on the User object. The latter is an alias for the former

$user = User::find(1);
$token = JWTAuth::fromUser($user);
Copy the code

Token control

refresh

Update the token.

$newToken = JWTAuth::parseToken()->refresh();
Copy the code

invalidate

Make a token invalid.

JWTAuth::parseToken()->invalidate();
Copy the code

check

Verify the validity of the token.

if(JWTAuth::parseToken()->check()) {
    dd("Token is valid");
}
Copy the code

Token parsing

authenticate or toUser or user

These three effects are the same, toUser is the alias of authenticate, and user has one less verification of user ID than the first two, but it has no effect.

$user = JWTAuth::parseToken()->toUser();
Copy the code

parseToken

Parse the token from the request into the object for the next step.

JWTAuth::parseToken();
Copy the code

getToken

Obtain the token from request.

$token= JWTAuth::getToken(); // This does not use parseToken, because the method is automatically executed once internallyCopy the code

Load control

customClaims or claims

Sets the customClaims portion of the payload. The latter is an alias for the former.

$customClaims = ['sid'= >$sid.'code'= >$code];
$credentials = $request->only('email'.'password');
$token = JWTAuth::customClaims($customClaims)->attempt($credentials);
Copy the code

getCustomClaims

Gets the customClaims part of the payload, returning an array.

$customClaims = JWTAuth::parseToken()->getCustomClaims()
Copy the code

getPayload or payload

Get all payloads, all three are the same, and the last one is generally used to verify the validity of the token

$payload = JWTAuth::parseToken()->payload();

// then you can access the claims directly e.g.
$payload->get('sub'); / / = 123$payload['jti']; / / ='asfe4fq434asdf'
$payload('exp') / / = 123456$payload->toArray(); / / = ['sub'= > 123,'exp'= > 123456,'jti'= >'asfe4fq434asdf'] etc
Copy the code

getClaim

Gets an element specified in the payload

$sub = JWTAuth::parseToken()->getClaim('sub');
Copy the code

1.2 JWTGuard

This Facade manages the payload, returning a payload object to which a token can then be generated through JWTAuth

// The height of the load is customized$payload = JWTFactory::sub(123)->aud('foo')->foo(['bar'= >'baz'])->make();
$token = JWTAuth::encode($payload);
Copy the code

$customClaims = ['foo'= >'bar'.'baz'= >'bob'];
$payload = JWTFactory::make($customClaims);
$token = JWTAuth::encode($payload);
Copy the code

1.3 Other uses

We’ll use auth here, because Laravel has multiple guards, and guard is not an API by default, so we need to write auth(‘ API ‘) otherwise, auth() will do.

Set the load

$token = auth('api')->claims(['foo'= >'bar'])->attempt($credentials);
Copy the code

Display setting token

$user = auth('api') - >setToken('eyJhb... ')->user();
Copy the code

Display setup request

$user = auth('api') - >setRequest($request)->user();
Copy the code

Rewrite valid time

$token = auth('api') - >setTTL(7200)->attempt($credentials);
Copy the code

Verify that the account encryption is correct

$boolean = auth('api')->validate($credentials);
Copy the code


Finally, if you want to set the rules in a uniform way, it is recommended that you use middleware, so that you can protect the routes and methods that you need to validate with your middleware. Article posted is written by other big god middleware, how to say? Each way friends go groping and groping well by themselves!

The middleware code is as follows:

<? php namespace App\Http\Middleware; use Auth; use Closure; use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Http\Middleware\BaseMiddleware; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; / / note that BaseMiddleware {/** * Handle an incoming request. ** @param \Illuminate\Http\Request$request
     * @param  \Closure $next
     *
     * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
     *
     * @return mixed
     */
    public function handle($request, Closure $next) {// Check if there is a token in this request and throw an exception if there is no token.$this->checkForToken($request); // Use the try package to catch the TokenExpiredException thrown by token expiration try {// Check the user login status, if normal passif ($this->auth->parseToken()->authenticate()) {
                return $next($request);
            }
            throw new UnauthorizedHttpException('jwt-auth'.'Not logged in');
        } catch (TokenExpiredException $exception) {// Here we catch a TokenExpiredException thrown by the token expiration. What we need to do here is refresh the user's token and add it to the response header try {// Refresh the user's token$token = $this->auth->refresh(); Auth::guard('api')->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()['sub']);
            } catch (JWTException $exception) {// If this exception is caught, refresh is also expired and the user cannot refresh the token and needs to log in again. throw new UnauthorizedHttpException('jwt-auth'.$exception->getMessage()); }} // Return the new token in the response headerreturn $this->setAuthenticationHeader($next($request), $token); }}Copy the code

Update handlers for exception handling

Because of our building is API service, so we need to update my app/Exceptions/Handler render in PHP

Method, custom handle some exceptions.

Handler.php

<? php namespace App\Exceptions; use Exception; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; class Handler extends ExceptionHandler { ... /** * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request$request
     * @param  \Exception $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception) {// Parameter validation error exception, we need to return 400 HTTP code and an error messageif ($exception instanceof ValidationException) {
            return response(['error' => array_first(array_collapse($exception->errors()))], 400); } // User authentication exception, we need to return 401 HTTP code and error messageif ($exception instanceof UnauthorizedHttpException) {
            return response($exception->getMessage(), 401);
        }

        return parent::render($request.$exception); }}Copy the code

At this point, laravel’s related combination and learning has come to an end.