takeaway

First in the public: JAVA thief ship, original is not easy, like readers can pay attention to oh! A share Java learning resources, practical experience and technical articles of the public number!

preface

Elegant, which means elegant and elegant, in ape-speak means the code is comfortable to look at and comfortable to use. There are many login authentication methods, including cookie, session, and token authentication. This article focuses on the elegant handling of token authentication based on JWT custom annotations. The elegance here is only the author’s personal taste.

First of all, we need to understand what custom annotations are. Of course, this is just a brief explanation, but it is not the focus of this article.

The basic elements of annotations

Declare the element to be used for an annotation

  • Modifiers Access modifiers must be public, not written and default to pubic;

  • Keyword The keyword is @interface.

  • Annotation Name The annotation name is the name of the user-defined annotation.

  • Annotation type element An annotation type element is the content of an annotation, which can be understood as the implementation part of a custom interface.

public @interface LoginUser {
   //String name() default "hello";
}
Copy the code

Meta-annotations modify annotations

There are some meta annotations in the JDK, including @target, @Retention, @document, and @Inherited.

@Target

Table where the annotation is used, such as method, field, class. It has the following partial types:

type describe
ElementType.TYPE Applies to classes, interfaces (including annotation types), enumerations
ElementType.FIELD Applied to attributes (including constants in enumerations)
ElementType.METHOD Applied to methods
ElementType.PARAMETER Parameter applied to a method
ElementType.CONSTRUCTOR Apply to the constructor
ElementType.LOCAL_VARIABLE Apply to local variables
ElementType.ANNOTATION_TYPE Applies to annotation types
ElementType.PACKAGE Applied to the package

@Retention

Indicates the life cycle of the annotation

type describe
RetentionPolicy.SOURCE Discarded at compile time and not included in the class file
RetentionPolicy.CLASS The JVM is discarded when loaded and is included in the class file, default
RetentionPolicy.RUNTIME Loaded by the JVM, contained in a class file, available at run time

@Document

The element that indicates the annotation tag can be documented by Javadoc or a similar tool

@Inherited

An annotation that indicates that the @Inherited annotation is used, and that child classes of the labeled class also have this annotation


Now that the knowledge is in place, it’s time to implement custom annotations to solve login authentication

Introduction of depend on

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
  <dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId>  </dependency>   <dependency>  <groupId>org.projectlombok</groupId>  <artifactId>lombok</artifactId>  <version>1.18.8</version>  </dependency>  <! --jwt--> <dependency>  <groupId>com.auth0</groupId>  <artifactId>java-jwt</artifactId>  <version>3.4.1</version>  </dependency>   <dependency>  <groupId>org.apache.commons</groupId>  <artifactId>commons-lang3</artifactId>  <version>3.8.1</version>  </dependency>   </dependencies> Copy the code

Custom annotations

package com.ao.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;   // Define annotations to be used on parameters @Target(ElementType.PARAMETER) // Define annotations to take effect at runtime @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser {  }  Copy the code

Implements a parser for method parameters

Here explain HandlerMethodArgumentResolver method is used for processing parameters of the parser, consists of the following two methods:

  • SupportsParameter (if certain requirements are met, return true to enter resolveArgument)
  • resolveArgument
package com.ao.demo.annotation.support;

import com.ao.demo.annotation.LoginUser;
import com.ao.demo.utils.UserTokenManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer;  @Slf4j public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {  public static final String LOGIN_TOKEN_KEY = "X-My-Token";   / * ** Determines whether the parameter type to be converted is supported* /  @Override  public boolean supportsParameter(MethodParameter parameter) {  log.info("Come in supportsParameter, I want to determine if I support the type of argument I want to convert.");  ResolveArgument = resolveArgument (); resolveArgument = resolveArgument ()  return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);  }   @Override  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,NativeWebRequest request, WebDataBinderFactory factory) throws Exception {  / ** Each request checks for the presence of the HTTP header field 'x-my-token'.If so, the internal query is converted to LoginUser and then used as a request parameter.If not, as a NULL request parameter.* /  String token = request.getHeader(LOGIN_TOKEN_KEY);  log.info("Enter the resolveArgument and get the token" + token);  Integer userId = JwtHelper.verifyTokenAndGetUserId(token);  log.info("The login user ID is:"+ userId);  if (userId == null) { return null;  }  return userId;  } }  Copy the code

Custom interceptors

package com.ao.demo.config;

import com.ao.demo.annotation.support.LoginUserHandlerMethodArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;  import java.util.List;  @Configuration public class WxWebMvcConfiguration implements WebMvcConfigurer {   @Override  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {  argumentResolvers.add(new LoginUserHandlerMethodArgumentResolver());  }  }  Copy the code

JwtHelper

package com.ao.demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.apache.commons.lang3.time.DateUtils;  import java.util.*;  public class JwtHelper {  / / the secret key  static final String SECRET = "X-My-Token";  // The signature is generated by who  static final String ISSUSER = "me";  // Signature subject  static final String SUBJECT = "this is my token";  // The audience signed  static final String AUDIENCE = "MY-USER";    public String createToken(Integer userId){  try {  Algorithm algorithm = Algorithm.HMAC256(SECRET);  Map<String, Object> map = new HashMap<String, Object>();  map.put("alg"."HS256");  map.put("typ"."JWT");  String token = JWT.create()  // Set the Header information  .withHeader(map)  // Set the Payload  .withClaim("userId", userId)  .withIssuer(ISSUSER)  .withSubject(SUBJECT)  .withAudience(AUDIENCE)  // The time to generate the signature  .withIssuedAt(new Date())  // When the signature expires  .withExpiresAt(DateUtils.addHours(new Date(), 1))  / / sign Signature  .sign(algorithm);  return token;  } catch (JWTCreationException exception){  exception.printStackTrace();  }  return null;  }   public Integer verifyTokenAndGetUserId(String token) {  try {  Algorithm algorithm = Algorithm.HMAC256(SECRET);  JWTVerifier verifier = JWT.require(algorithm)  .withIssuer(ISSUSER)  .build();  DecodedJWT jwt = verifier.verify(token);  Map<String, Claim> claims = jwt.getClaims();  Claim claim = claims.get("userId");  return claim.asInt();  } catch (JWTVerificationException exception){  return null;  }  }  }  Copy the code

use

Add the @loginUser annotation to the interface that requires authenticated login

package com.ao.demo.web;

import com.ao.demo.annotation.LoginUser;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
 @RestController public class TestController {   @GetMapping("/test")  public String tt(@LoginUser Integer userId){  if (userId == null) { return "Please log in first.";  }  return "Login successful";  } }  Copy the code

test

First, use the main method to generate the token with user ID 1. The value is:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aGlzIGlzIG15IHRva2VuIiwiYXVkIjoiTVktVVNFUiIsImlzcyI6Im1lIiwiZXhwIjoxNTk 0MTc1Nzg4LCJ1c2VySWQiOjEsImlhdCI6MTU5NDE3MjE4OH0.eBsFzFPHjtjoL3yF2LvHFkFfNH2–XkJhbXBOz5hKBo

  • Login failure case


  • Login Success Cases


So far, this is a more elegant way to write, just add a custom annotation to the interface that requires authentication and make the judgment. Isn’t it a bit tedious for every authenticated interface to check if the userId is null? So what’s the solution? In fact, we can use global exceptions to handle, so that each authentication interface does not have to determine. I originally thought of writing a separate article on elegant processing returns, but thought it was too short, so I merged it with this article.


Graceful processing returns results

Define an exception enumeration class

Records user exception information

@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum UserExceptionEnum {
    UNLOGIN(500."Please log in first!!")
 / /... Defining exception Information  ;  private int code;  private String msg;  }  Copy the code

Custom exception

@Getter
public class UserException extends RuntimeException {

   private UserExceptionEnum userExceptionEnum;

 public UserException(UserExceptionEnum userExceptionEnum) {  this.userExceptionEnum = userExceptionEnum;  }  } Copy the code

Encapsulate return result

@Data
public class ExceptionResult {

   private int status;

 private String message;   private long timestamp;   public ExceptionResult(ExceptionEnum em) {  this.status = em.getCode();  this.message = em.getMsg();  this.timestamp = System.currentTimeMillis();  } } Copy the code

Global exception handling

@ControllerAdvice

It is more commonly used scenarios are as follows, here is not a say, you can go to understand.

  • Global exception handling
  • Global data binding
  • Global data preprocessing
ResponseEntity

ResponseEntity Identifies the entire HTTP response: status code, header information, and corresponding body content.

@ControllerAdvice
public class CommonExceptionHandler {
    @ExceptionHandler(UserException.class)
    public ResponseEntity<ExceptionResult> handleException(UserException e){
        return ResponseEntity.status(e.getUserExceptionEnum().getCode()).body(new ExceptionResult(e.getUserExceptionEnum()));
 }   /* Here you can define more than one to handle different businesses, such as user-related exceptions, merchandise order exceptions */ } Copy the code

This return result is not elegant, each business defines an exception class and exception enumeration class, and then handed over to the global exception handling, so that the code is more intuitive, business clearer point.

Login Authentication Optimization

If you don’t want to give each need login authentication interface to write a judgment, then you can to global exception handling, need only in LoginUserHandlerMethodArgumentResolver is modified, as follows:

  
 @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
     / ** Each request checks for the presence of the HTTP header field 'x-my-token'.If so, the internal query is converted to LoginUser and then used as a request parameter.If not, as a NULL request parameter.* /  String token = request.getHeader(LOGIN_TOKEN_KEY);  log.info("Enter the resolveArgument and get the token" + token);  Integer userId = JwtHelper.verifyTokenAndGetUserId(token);  log.info("The login user ID is:"+ userId);  if (userId == null) { throw new UserException(UserExceptionEnum.UNLOGIN);  }  return userId;  } Copy the code

If the userId is null, then the custom exception will be thrown

Test a wave


So the interface that requires login authentication does not have to check whether the userId is null.

Original is not easy, you can pay attention to oh! A share Java learning resources, technical articles, practical experience of the public account ~

This article is formatted using MDNICE