JSON Web Token (JWT) is one of the most popular cross-domain authentication solutions.

I. The origin of the story

Speaking of JWT, let’s talk about traditional Sess-based authentication schemes and bottlenecks.

The traditional session interaction process is shown as follows:

When the browser sends a login request to the server, the user information will be stored in Seesion after the authentication passes, and then the server will generate a sessionId and put it into the cookie, and then return it to the browser.

When the browser sends the request again, it puts the sessionId in the cookie in the request header and sends the request data to the server.

The server can retrieve user information from Seesion again, and the process is complete!

Usually, the seesion duration is set on the server. For example, if the seesion is inactive for 30 minutes, the stored user information is removed from the Seesion.

session.setMaxInactiveInterval(30 * 60); // If there is no activity for 30 minutes, it will be automatically removedCopy the code

At the same time, you can use seesion on the server to check whether the current user has logged in. If the value is empty, the login page is displayed. If it is not empty, you can obtain user information from the session to perform subsequent operations.

In a single application, this kind of interaction is fine.

However, if the application server requests become very large, and the number of requests a single server can support is limited, this time is prone to slow requests or OOM.

The solution is to add configurations for a single server or add new servers to meet service requirements through load balancing.

If the configuration of a single server is added, the number of requests continues to increase, but services cannot be processed.

Obviously, by adding new servers, you can achieve infinite horizontal scaling.

However, after A new server is added, the sessionId of different servers is different. It may be that the login has been successful on server A and the user information can be obtained from the session of the server, but the session information cannot be found on server B. At this time, it must be extremely awkward and we have to quit to continue the login. As A result, the session on server A expires due to timeout. After login, it is forced to log out and ask to log in again. It is embarrassing to think ~ ~

Faced with this situation, several big heads then discussed together and came up with a token scheme.

Each application program is connected to the memory database REDis, and the user information of successful login is encrypted by certain algorithms. The generated ID is called token, and the token and user information are stored in REDIS. When the user initiates a request again, the token and request data are sent to the server, and the server verifies whether the token exists in Redis. If so, the verification is successful. If not, the browser is told to jump to the login page, and the process ends.

Token scheme ensures stateless service and all information is stored in distributed cache. Based on distributed storage, this can scale horizontally to support high concurrency.

Of course, SpringBoot also provides a session sharing scheme. Similar to the Token scheme, the session is stored in Redis. After a login in the cluster environment, each server can obtain user information.

What is JWT

In the previous section, we talked about session and token schemes. In the cluster environment, they all rely on the third-party cache database Redis to realize data sharing.

Is there a solution, not cache database Redis to achieve user information sharing, in order to achieve a login, everywhere visible effect?

Rob: Well, the answer is definitely there, and that’s what we’re talking about today.

JWT is the full name of JSON Web Token. In a simple way, after a user logs in successfully, the user’s information is encrypted and a Token is generated and returned to the client, which is not much different from the traditional session interaction.

The interaction process is as follows:

The only difference is that the token stores the basic information of the user. More intuitively, the user data that was originally put into redis is put into the token.

In this way, both the client and the server can obtain the user’s basic information from the token. Since the client can obtain the user’s basic information from the token, it cannot store sensitive information, because the browser can directly obtain the user’s information from the token.

What exactly does a JWT look like?

JWT is made up of three pieces of information. Use these three pieces of information text. The links together make up the JWT string. Something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cB ab30RMHrHDcEfxjoYZgeFONFh7HgQCopy the code
  • The first part is called the header, which is used to store the token type and encryption protocol. It is usually fixed.

  • The second part: we call it a payload, where the user’s data is stored.

  • The third part: is the visa (signature), mainly used for server authentication;

1, the header

The header of the JWT carries two pieces of information:

  • Declare type, in this case JWT;

  • Declare the encryption algorithm, usually directly use HMAC SHA256;

The complete header looks like this JSON:

{  'typ': 'JWT',  'alg': 'HS256'}
Copy the code

Using Base64 encryption forms the first part.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Copy the code
2, playload

Payload is the place where valid information is stored, and this valid information consists of three parts:

  • A declaration of registration in the standard;

  • A public statement;

  • A private declaration;

The declaration of registration in the standard (recommended but not mandatory) includes the following parts:

  • Iss: JWT issuer;

  • Sub: the user JWT is targeting;

  • Aud: the side receiving JWT;

  • Exp: expiration time of JWT. This expiration time must be longer than the issue time.

  • NBF: defines the time before the JWT is unavailable;

  • Iat: issue time of JWT;

  • The unique identifier of JWT is mainly used as a one-time token to avoid replay attacks.

Public declaration part: Public declaration part can add any information, generally add user information or other necessary information required by the business, but it is not recommended to add sensitive information, because this part can be decrypted on the client.

Private declaration part: A private declaration is a declaration defined by both the provider and the consumer. It is generally not recommended to store sensitive information because Base64 is symmetrically decrypted, which means that this part of the information can be classified as plaintext information.

Define a payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
Copy the code

It is then base64 encrypted to get the second part of Jwt:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

Copy the code
3, signature

The third part of the JWT is a visa information, which consists of three parts:

  • Header (base64);

  • Payload (base64);

  • Secret;

This part is used by the base64-encrypted header and the Base64-encrypted payload. A string of concatenated strings, then salted secret with the encryption method declared in the header, which forms the third part of the JWT.

//javascript var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); Var signature = HMACSHA256(encodedString, 'key ');Copy the code

After encryption, the signature information is obtained.

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Copy the code

Concatenate the three parts into a complete string to form the final JWT:

// JWT final format eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cB ab30RMHrHDcEfxjoYZgeFONFh7HgQCopy the code

This is just a javascript demo, JWT signing and key saving are done on the server side.

Secret is used for JWT signing and JWT validation, so it should not be revealed in any scenario.

Third, in actual combat

So how do you do that? Blah, blah, blah, blah, blah, blah, blah, blah, blah, blah, blah!

  • To create aspringbootProject, addJWTDependent libraries
<! -- JWT support --> <dependency> <groupId>com.auth0</groupId> <artifactId> Java -jwt</artifactId> <version>3.4.0</version> </dependency>Copy the code
  • Then, create a user information class that will be stored encryptedtokenIn the
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class UserToken implements Serializable { private static final long serialVersionUID = 1L; /** * userId */ private String userId; /** * private String userNo; /** * private String userName; }Copy the code
  • Next, create oneJwtTokenUtilUtility class for creatingtoken, validation,token
Public class JwtTokenUtil {// Define the token return header public static final String AUTH_HEADER_KEY = "Authorization"; // Token prefix public static final String TOKEN_PREFIX = "Bearer "; Public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x"; Public static final Long EXPIRATION_TIME = 1000L*60*60*2; /** * createToken * @param content * @return */ public static String createToken(String content){return TOKEN + JWT.create() .withSubject(content) .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .sign(Algorithm.HMAC512(KEY)); } /** * verifyToken * @param token */ public static String verifyToken(String token) throws Exception {try {return JWT.require(Algorithm.HMAC512(KEY)) .build() .verify(token.replace(TOKEN_PREFIX, "")) .getSubject(); } catch (TokenExpiredException e){throw new Exception("token has expired, please login again ",e); } catch (JWTVerificationException e) {throw new Exception(" Token validation failed!" ,e); }}}Copy the code
  • Write configuration classes that allow cross-domain, and create a permission interceptor
@slf4j @Configuration Public class GlobalWebMvcConfig implements WebMvcConfigurer {/** ** implements WebMvcConfigurer Registry */ @override public void addCorsMappings(CorsRegistry Registry) {// Add a mapping path Registry.addmapping ("/**") // AllowedOrigins ("*") // Whether to send Cookie information. AllowCredentials (true) // Which primitive domains are allowed. AllowedMethods ("GET", "POST", "DELETE", "PUT", "OPTIONS", AllowedHeaders ("*") // What headers are exposed (because cross-domain access cannot get all headers by default). ExposedHeaders ("Server","Content-Length",  "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials"); } /** * Add interceptor * @param registry */ @override public void addInterceptors(InterceptorRegistry) registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**"); }}Copy the code
  • useAuthenticationInterceptorThe interceptor validates the interface parameters
@Slf4j public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object Handler) throws Exception {// Fetch token final String Token = from the HTTP request header request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY); // If not mapped to a method, use if(! (handler instanceof HandlerMethod)){ return true; } / / if it is a method to detect, directly through the if (HttpMethod. OPTIONS. Equals (request) getMethod ())) {response. SetStatus (HttpServletResponse. SC_OK); return true; } // If the method has JwtIgnore annotations, go directly to HandlerMethod HandlerMethod = (HandlerMethod) handler; Method method=handlerMethod.getMethod(); if (method.isAnnotationPresent(JwtIgnore.class)) { JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class); if(jwtIgnore.value()){ return true; }} LocalAssert. IsStringEmpty (token, the token is empty, the authentication failed! "" ); String userToken = jWTToKenUtil. verifyToken(token); String userToken = jWTToKenUtil. verifyToken(token); . / / the token into the local cache WebContextUtil setUserToken (userToken); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws the Exception {/ / method ends, remove the cache token WebContextUtil. RemoveUserToken (); }}Copy the code
  • Finally, in thecontrollerAfter the layer user logs in, create onetoken, stored in the head
@jwtignore @requestMapping (value = "/login", method = requestmethod. POST, produces = {"application/json; charset=UTF-8"}) public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){ //... Legitimacy validation / / parameters retrieved from the database User information User dbUser = userService. SelectByUserNo (userDto. GetUserNo); / /... User/password authentication // Create a token and put the token in the response header UserToken UserToken = new UserToken(); BeanUtils.copyProperties(dbUser,userToken); String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken)); response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token); UserVo result = new UserVo(); BeanUtils.copyProperties(dbUser,result); return result; }Copy the code

And that’s pretty much it!

Which used in AuthenticationInterceptor JwtIgnore is an annotation, for in the way that does not require authentication token, such as authentication code acquisition and so on.


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
 
 
    boolean value() default true;
}

Copy the code

WebContextUtil is a thread caching utility class that other interfaces use to get user information from tokens.

Public Class WebContextUtil {private static ThreadLocal<String> local = new ThreadLocal<>(); @param Content */ public static void setUserToken(String content){removeUserToken(); local.set(content); } @return public static UserToken getUserToken(){if(local.get()! = null){ UserToken userToken = JSONObject.parseObject(local.get() , UserToken.class); return userToken; } return null; } @return public static void removeUserToken(){if(local.get()! = null){ local.remove(); }}}Copy the code

Finally, to start the project, let’s test it with Postman and see what the header returns.

We extract the returned information and decrypt the first two parts using the browser’s Base64.

  • The first part, the header, results in the following:

  • The second part, playload, results in the following:

You can clearly see that header and payload information can be decrypted through Base64.

So don’t store sensitive information in tokens!

When we need to request other service interfaces, we only need to add the Authorization parameter to the request headers.

When the permission interceptor validates, the user information can be retrieved in the interface method only through the WebContextUtil utility class.

/ / get the user token information UserToken UserToken = WebContextUtil. GetUserToken ();Copy the code

Four,

Compared with the Session scheme, JWT can be supported across languages due to the generality of JSON. Many languages such as JAVA, JavaScript and PHP can be used by JWT, while the Session scheme is only for JAVA.

With the Payload part, JWT can store non-sensitive information that is necessary for other business logic.

At the same time, it is very important to protect the server side secret private key, because the private key can be used for data authentication, decryption!

Use HTTPS if you can!

Five, write to the end

I believe youyou already know how to use JWT to implement single sign-on, click follow, don’t get lost, follow programmers once, every day to share different Java basics, if you want to know more about Java knowledge and interview me here to organize a my ownGitHub, you can check it yourself if you need to