Relearn spring cloud series 8 - microservice gateway security authentication - JWT

Gateway JWT authentication process

1, Authentication process of gateway authentication document

At present, the mainstream process of developing user authentication and service access authentication combined with microservice gateway and JWT token is as follows:

  • User authentication process: the user sends a login authentication request to the gateway, and the gateway forwards the request to the authentication service. After the authentication service verifies the user's login information (user password, SMS and picture verification code), if the verification is successful, a token token will be issued to the user (this token can be a JWT token)
  • Gateway level access authentication: when users access other business service interfaces in the system, they need to carry the JWT token issued during login authentication. The gateway verifies the legitimacy of the JWT token. If the token is illegal, the access authority of the return interface is insufficient. If the token is legal, the request will be forwarded to the corresponding service according to the routing rules of the gateway.
  • Service level access authentication: gateway level access authentication only authenticates the legitimacy of JWT token and preliminarily determines that you are a user of the system, but being a user of the system does not mean that you can access all service interfaces. Generally, there is a more strict division of access rights based on the role classification of users.

The issuance and verification of tokens need to be based on the same key, that is, the signature and de signing of JWT tokens must have the same key. The answer to the riddle must be "pagoda town river demon". If the key cannot be matched, the verification of the token will fail.

Therefore, in addition to forwarding the request, the gateway needs to do two things: one is to verify the legitimacy of the JWT token, the other is to parse the user identity from the JWT token and carry the user identity information when forwarding the request. In this way, when other business services in the system receive the forwarding request, they determine which interfaces the user can access according to the user's identity information.

2, Process optimization scheme

From the above process, we can see

  • The token is issued by the authentication service
  • The verification of the token is completed by the gateway

In other words, the basic configuration related to the JWT key must be configured on both "authentication service" and "gateway service". Such a decentralized configuration is not conducive to maintenance and key management. So let's optimize the process: develop the login authentication function on the service of gateway service gateway. The optimized process is as follows:

3, Basic knowledge required for learning this chapter

From the above process, it can be seen that the implementation of JWT authentication process is not very complex, but it involves a lot of basic knowledge to really do a good job in the authentication process of service interface.

3.1. Implement login authentication on the gateway

  • Because the basic framework of gateway gateway is Spring WebFlux, not Spring MVC. So you need to have some knowledge of WebFlux development.
  • The current support of Spring WebFlux for responsive programming of relational databases is very limited. The author has tested mybatis many times. At present, it must not be used. JPA compatibility is relatively good. So you should have knowledge of JPA. (WebFlux does not support the responsive programming of MySQL database access, which does not mean that it does not support mysql, but it can still use MySQL database)

3.2. Spring Security Foundation

When receiving the forwarding request, other business services in the system determine which interfaces the user can access according to the user's identity information. How to realize it? You should have the basic knowledge of Spring Security and RBAC permission management model

Appendix – sequence diagram code above

Online sequence diagram editing tool: https://www.websequencediagrams.com/

user->+gateway: Login request
 gateway-->-user: return token


user->+gateway: carry token Access business
 gateway->gateway: check token Legitimacy of
 gateway->+Other services: Forwarding request with user identity information
 Other services-->-gateway: return data
 gateway-->-user: return data

Login authentication JWT token issuance

The requirements to be realized in this section are: the user initiates a login authentication request, authenticates the user on the gateway service (user name and password), and returns the JWT token to the user client after successful authentication.

The project structure after implementation is as follows:

1, maven core dependency

On the basis of the code in the previous chapter, the following maven dependencies are added

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-crypto</artifactId>
</dependency>

<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>
  • jjwt is the core class library that implements JWT tokens
  • Spring boot starter data JPA is a persistence layer framework, because we need to load user information from the database. The reason why mybatis is not used is that the compatibility of mybatis under webFlux is not good at present.
  • Spring security crypto is a common class library for encryption, decryption, signing and unmarshalling under the spring framework

2, Core Controller

2 core functions:

  • Authentication realizes login authentication. After successful authentication, the JWT token is returned
  • refreshtoken refreshes the token and exchanges the old token for a new one (because the JWT token has a valid period, and the token beyond the valid period is illegal)

Note that Mono in the following is the method of WebFlux result response data callback, not my customization.

/**
 * JWT Get token and refresh token interface
 */
@RestController
@ConditionalOnProperty(name = "zimug.gateway.jwt.useDefaultController", havingValue = "true")
public class JwtAuthController {

    @Resource
    private JwtProperties jwtProperties;
    @Resource
    private SysUserRepository sysUserRepository;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Resource
    private PasswordEncoder passwordEncoder;

    /**
     * Exchange user name and password for JWT token
     */
    @RequestMapping("/authentication")
    public Mono<AjaxResponse> authentication(@RequestBody Map<String,String> map){
        //Get user name and password from request body
        String username  = map.get(jwtProperties.getUserParamName());
        String password = map.get(jwtProperties.getPwdParamName());

        if(StringUtils.isEmpty(username)
                || StringUtils.isEmpty(password)){
            return buildErrorResponse("User name or password cannot be empty");
        }
        //Find the user in the database according to the user name (user Id)
        SysUser sysUser = sysUserRepository.findByUsername(username);
        if(sysUser != null){
            //match the encrypted password of the database with the user's plaintext password
            boolean isAuthenticated = passwordEncoder.matches(password,sysUser.getPassword());
            if(isAuthenticated){ //If the match is successful
                //Generate JWT token through jwtTokenUtil and return
                return buildSuccessResponse(jwtTokenUtil.generateToken(username,null));
            } else{ //If password matching fails
                return buildErrorResponse("Please make sure the user name or password you entered is correct!");
            }
        }else{
            return buildErrorResponse("Please make sure the user name or password you entered is correct!");
        }
    }

    /**
     * Refresh the JWT token and replace the old token with a new one
     */
    @RequestMapping("/refreshtoken")
    public  Mono<AjaxResponse> refreshtoken(@RequestHeader("${zimug.gateway.jwt.header}") String oldToken){
        if(!jwtTokenUtil.isTokenExpired(oldToken)){
            return buildSuccessResponse(jwtTokenUtil.refreshToken(oldToken));
        }
        return Mono.empty();
    }

    private Mono<AjaxResponse> buildErrorResponse(String message){
       return Mono.create(callback -> callback.success( //Callback of successful request results
            AjaxResponse.error( //The response information is Error and returns with exception information
                    new CustomException(CustomExceptionType.USER_INPUT_ERROR, message)
            )
       ));
    }

    private Mono<AjaxResponse> buildSuccessResponse(Object data){
        return Mono.create(callback -> callback.success( //Callback of successful request results
                AjaxResponse.success(data)  //Respond successfully and return with data
        ));
    }

}

Four core service code classes will be introduced later

  • JwtProperties, JWT configuration loading class, including JWT key configuration, expiration time and other parameter configurations
  • SysUserRepository, database sys_ JPA Repository corresponding to user table. Because this table is a user information table, which contains user names and passwords.
  • The jwttil class encapsulates the token operation. Core methods: generate JWT token according to user id, verify the legitimacy of token, refresh token and other tool classes
  • PasswordEncoder is the encryption and decryption tool class of Spring Security. The core method is encode, which is used for password encryption; Matches is used for password verification. (use encode to encrypt when the user registers, and use matches to verify the password when the user logs in and authenticates)

3, JwtProperties

The following configuration properties need to be configured in the gateway configuration file. If they are not configured, the default values will be used.

@Data
@ConfigurationProperties(prefix = "zimug.gateway.jwt")
@Component
public class JwtProperties {

    //Whether to enable JWT, that is, inject relevant class objects
    private Boolean enabled;
    //JWT key
    private String secret;
    //JWT effective time
    private Long expiration;
    //When transmitting JWT from the front end to the back end, the header name of HTTP shall be used, and the front and back ends shall be unified
    private String header;
    //User login - username parameter name
    private String userParamName = "username";
    //User login - password parameter name
    private String pwdParamName = "password";
    //Use the default JWTAuthController
    private Boolean useDefaultController = false;

}
zimug:
  gateway:
    jwt:
      enabled: true   #Whether to enable JWT login authentication function
      secret: fjkfaf;afa  # The JWT private key is used to verify the legitimacy of the JWT token
      expiration: 3600000 #The validity period of the JWT token, which is used to verify the legitimacy of the JWT token
      header: JWTHeaderName #The Header name of the HTTP request. The Header passes the JWT token as a parameter
      userParamName: username  #User login authentication user name parameter name
      pwdParamName: password  #User authentication password name
      useDefaultController: true # Use the default JwtAuthController

These configurations will affect the component loading and running logic of the program in the code. For example, when ConditionalOnProperty - zimug gateway. jwt. The Bean of JwtAuthController class is initialized only when usedefaultcontroller = true. The purpose of this is that the gateway I plan to support not only JWT but also OAuth in the future, in order to avoid conflict or redundancy. We add switches to affect the initialization behavior of beans.

4, SysUserRepository

SysUser entity class corresponds to the sys of the database_ User table, defined according to JPA rules

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name="sys_user")
public class SysUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private String username;

    @Column
    private String password;

    @Column
    private Integer orgId;

    @Column
    private Boolean enabled;

    @Column
    private String phone;

    @Column
    private String email;

    @Column
    private Date createTime;
}

According to sys_ The user name field of the user table is used to query the SysUser user information.

public interface SysUserRepository extends JpaRepository<SysUser,Long> {

  //Pay attention to the name of this method. jPA will automatically generate SQL execution according to the method name without writing SQL yourself
  SysUser findByUsername(String username);
}

jpa and data source related configurations need to be added to the configuration file

spring:
  datasource:
    url: jdbc:mysql://ip:3306/linnadb?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: 
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: validate
    database: mysql
    show-sql: true

5, PasswordEncoder

We need to use PasswordEncoder to de sign and verify the password, so initialize a Bean: BCryptPasswordEncoder. It should be noted that we use BCryptPasswordEncoder The premise of matches de signing is that the password stored in the database during user registration also passes BCryptPasswordEncoder Encode encrypted.

6, JwtTokenUtil

Based on Io Code encapsulation of jsonwebtoken jjwt class library, tool class.

@Component
public class JwtTokenUtil {

    @Resource
    private JwtProperties jwtProperties;


    /**
     * Generate token token
     *
     * @param userId User Id or user name
     * @param payloads Additional information carried in the token
     * @return token Card
     */
    public String generateToken(String userId,
                                Map<String,String> payloads) {
        int payloadSizes = payloads == null? 0 : payloads.size();

        Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
        claims.put("sub", userId);
        claims.put("created", new Date());

        if(payloadSizes > 0){
            for(Map.Entry<String,String> entry:payloads.entrySet()){
                claims.put(entry.getKey(),entry.getValue());
            }
        }

        return generateToken(claims);
    }

    /**
     * Get user name from token
     *
     * @param token token
     * @return user name
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * Determine whether the token has expired
     *
     * @param token token
     * @return Expired
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            //Failure to verify JWT signature is equivalent to token expiration
            return true;
        }
    }

    /**
     * refresh token 
     *
     * @param token Original token
     * @return New token
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * Authentication token
     *
     * @param token       token
     * @param userId  User Id user name
     * @return Is it valid
     */
    public Boolean validateToken(String token, String userId) {

        String username = getUsernameFromToken(token);
        return (username.equals(userId) && !isTokenExpired(token));
    }


    /**
     * Generate a token from claims. If you don't understand it, it depends on who calls it
     *
     * @param claims Data declaration
     * @return token
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration());
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
                .compact();
    }

    /**
     * Obtain the data declaration from the token and verify the JWT signature
     *
     * @param token token
     * @return Data declaration
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }
}

7, Access test

The local machine starts the gateway for http://127.0.0.1:8777/authentication Login authentication and return the following results, which shows that our implementation is ok.

Refresh of test token

JWT authentication with global filter


In the previous section, we have implemented user login authentication. If the authentication is successful, the user will return a token to the user client, that is, JWT. We need to analyze the legitimacy of the JWT service from the gateway and forward the information of other users from the JWT service in this section.

1, JWT authentication with global filter

For all requests of the Gateway, the legitimacy of the JWT must be verified (except "/ authentication"), so it is most appropriate to use the Gateway global filter GlobalFilter. Add a global filter based on the code in the previous section

@Configuration
public class JWTAuthCheckFilter {
  @Resource
  private JwtProperties jwtProperties;
  @Resource
  private JwtTokenUtil jwtTokenUtil;

  @Bean
  @Order(-101)
  public GlobalFilter jwtAuthGlobalFilter()
  {
    return (exchange, chain) -> {
      ServerHttpRequest serverHttpRequest = exchange.getRequest();
      ServerHttpResponse serverHttpResponse = exchange.getResponse();
      String requestUrl = serverHttpRequest.getURI().getPath();


      if(!requestUrl.equals("/authentication")){
        //Get JWT token from HTTP request header
        String jwtToken = serverHttpRequest
                .getHeaders()
                .getFirst(jwtProperties.getHeader());
         //De sign the Token and verify whether the Token has expired
        boolean isJwtValid = jwtTokenUtil.isTokenExpired(jwtToken);
        if(isJwtNotValid){ //If the JWT token is illegal
          return writeUnAuthorizedMessageAsJson(serverHttpResponse,"Please log in first and then access the service!");
        }
        //Resolve the identity (userId) of the current user from JWT, continue to execute the filter chain and forward the request
        ServerHttpRequest mutableReq = serverHttpRequest
                .mutate()
                .header("userId", jwtTokenUtil.getUsernameFromToken(jwtToken))
                .build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
        return chain.filter(mutableExchange);
      }else{ //If it is a login authentication request, it is directly executed without JWT permission verification
        return chain.filter(exchange);
      }
    };
  }

  //Respond the message of JWT authentication failure to the client
  private Mono<Void> writeUnAuthorizedMessageAsJson(ServerHttpResponse serverHttpResponse,String message) {
    serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
    serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
    AjaxResponse ajaxResponse = AjaxResponse.error(CustomExceptionType.USER_INPUT_ERROR,message);
    DataBuffer dataBuffer = serverHttpResponse.bufferFactory()
            .wrap(JSON.toJSONStringWithDateFormat(ajaxResponse,JSON.DEFFAULT_DATE_FORMAT)
                    .getBytes(StandardCharsets.UTF_8));
    return serverHttpResponse.writeWith(Flux.just(dataBuffer));
  }

}

The filter core code does two things

  • Verify the legitimacy of JWT. Illegal requests will be returned directly and will not be forwarded.
  • If the JWT is legal, the userId (user identity information) is parsed from the JWT and put into the HTTP header. (the services behind the gateway will be used in the next section)


Please understand the implementation of global JWT authentication in combination with the above comments. If it is difficult to understand, understand the above code in combination with the following test process.

2, Testing

  • Access without JWT token http://127.0.0.1:8777/sysuser/pwd/reset .
  • Go login http://127.0.0.1:8777/authentication , get JWT token
  • Add JWT token to http://127.0.0.1:8777/sysuser/pwd/reset In the Header of the access request, initiate the request again

    give the result as follows

    Let's modify the JWT token string and visit again http://127.0.0.1:8777/sysuser/pwd/reset , the results are as follows:

Internal permission management of microservices

1, Look at the process again


According to the above process, we have finished

  • The function of login authentication is developed on the gateway. After login authentication, the user returns the JWT token to the client
  • A global filter is created on the gateway. When a request is sent to the gateway, the filter verifies the legitimacy of the JWT token. Only legitimate token requests will be forwarded to specific business services. In the filter, we resolve the userId (user identity information) in the JWT token and pass it to the service behind the gateway.


Other services are divided into two types:

  • One service is open to all users in the system, that is, after the user passes the JWT authentication of the gateway, the service itself will no longer restrict the user's permission, and all interfaces can be accessed.
  • Another service has permission requirements, such as judging whether you have permission to access some interfaces according to your role. For example, as a system administrator user, you can access "system log", "system management" and other functional interfaces; As a system operator, you can only access some business operation interfaces.

3, Permission management within microservices

Known: we can get userId (user identity information), and we don't know anything else. We can use RBAC permission model to manage user permissions.

  • User information can be obtained by querying userId
  • Role information can be queried according to user information (a user has multiple roles)
  • According to the role information, you can find the interface permission information (a role has multiple permissions)

The final service obtains the list X of interface permissions that the user can access through userId (user identity information). The interface that the user is accessing is in the X list, which means that the user can access the interface, otherwise he has no permission.

database model

We can use the database design model in the figure below to describe such a relationship.

  • A user has one or more roles
  • A role contains multiple users
  • A role has multiple permissions
  • One permission belongs to multiple roles
  • sys_user is the user information table, which is used to store the basic information of the user, such as user name and password
  • sys_role is a role information table, which is used to store all roles in the system
  • sys_menu is the menu information table of the system, which is used to store all menus in the system. Maintain a menu tree structure with the field relationship between id and parent id.
  • sys_user_role is a user role many to many relationship table. A relationship record between userid and roleid indicates that the user has the role, and the role contains the user.
  • sys_role_menu is the role menu (permission) relationship table. A relationship record between roleid and menuid indicates that the role is authorized by a menu, and the menu permission can be accessed by a role.
    You can implement this process in combination with Spring Security, shiro, or you can judge the implementation without any framework. The knowledge of permission management inside microservices has gone beyond the scope of Spring Cloud. I won't explain the implementation one by one with you.

Keywords: Spring Cloud Microservices security

Added by marinedalek on Tue, 08 Feb 2022 10:08:18 +0200