Learn from me about the design and implementation of spring boot development backend management system 8: matrxi web permission

Keywords: Spring Druid github JSON

The last article describes the idea of permission control implemented by matrix web as a whole. Let's review:

  • First of all, the user needs to log in and fill in the user name and password. The backend receives the login request and verifies the user and password. After the verification is successful, the Token is generated according to the user name and returned to the browser.

  • After the browser receives the Token, it will be stored in the local store.

  • The Token is carried by subsequent browsers when they initiate a request. After the request reaches the back end, it will be judged in the Filter. The first choice is to determine whether it is a white list url (such as login interface url). If it is, it will be released; otherwise, it will enter the Token verification. If there is a Token and the resolution is successful, it will be released; otherwise, it will return no access.

  • After Filter judgment, the request reaches the specific Controller layer. If the annotation of permission judgment is added to the Controller layer, a proxy class is generated. The proxy class will judge the permission according to the Token before executing the specific method.

    • Take the Token of the user and parse it to get the userId of the request. According to the userId, get the user's permission point from the storage layer. Authority control is implemented by RBAC.
  • After obtaining the user permission point, obtain the permission information of the annotation judged by the permission to see whether the user permission point contains the permission information of the permission annotation. If it does, the permission verification passes; otherwise, the request returns no permission.

[the external link image transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-7hlz5kp4-1590667445450)( https://ws3.sinaimg.cn/large/813fb3c3gy1g73tw6y311j20fu0cemxg.jpg ]

This article focuses on how to implement it in matrix web, and mainly explains some code details.

User login successful, generate Token

The user login interface is not controlled by permission, and can be accessed by anyone. The request needs to carry the user name and password. After the back-end service verifies that the user name and password are correct, a Token is generated. The login interface is as follows:

    @PostMapping("/login")
    public RespDTO login(@RequestParam String username, @RequestParam String password) {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", username);
        SysUser user = sysUserService.getOne(queryWrapper);
        if (user == null) {
            //Asynchronous storage log
            saveSysLoginLog(username, null, false);
            throw new AriesException(USER_NOT_EXIST);
        }
        if (!user.getPassword().equals(MD5Utils.encrypt(password))) {
            saveSysLoginLog(username, null, false);
            throw new AriesException(PWD_ERROR);
        }
        //Login successful
        String jwt;
        Map<String, String> result = new HashMap<>(1);
        try {
            jwt = JWTUtils.createJWT(user.getId() + "", user.getUserId(), 599999999L);
            result.put("token", jwt);
            log.info("login success,{}", jwt);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //Asynchronous storage log
        saveSysLoginLog(username, user.getRealname(), true);
        return RespDTO.onSuc(result);
    }

Jwt is generated in matrix web by using open source jjwt. The following dependencies are introduced into the pom file of the project:

<dependency>
  <groupId>io.jsonwebtoken</groupId>      <artifactId>jjwt</artifactId>
 <version>${jjwt.version}</version>
</dependency>

JWTUtils is encapsulated in the matrix web project to generate and parse JWT. Please check the code comments of each step for specific generation steps and parsing steps, which will not be repeated here.


public class JWTUtils {

//Generate Token
public static String createJWT(String id, String subject, long ttlMillis) throws Exception {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //Specify the signature algorithm used when signing, that is, the header part. jjwt has already encapsulated this part.
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        Map<String,Object> claims = new HashMap<String,Object>();//Create the private declaration of payload (add it according to the specific business needs. If you want to verify this, you usually need to communicate the verification method with the jwt receiver in advance)
//        claims.put("uid", "DSSFAWDWADAS...");
//        claims.put("user_name", "admin");
//        claims.put("nick_name","DASDA121");
        SecretKey key = generalKey();//The secret key used in signature generation. This method is encapsulated locally and can be read from the local configuration file. Remember that the secret key cannot be exposed. It is the private key of your server, which should not be revealed in any scenario. Once the client knows the secret, it means that the client can sign and issue jwt itself.
        //Here are the standard and private declarations for payload
        JwtBuilder builder = Jwts.builder() //This is actually new, a JwtBuilder. Set the body of jwt
                .setClaims(claims)          //If there is a private declaration, you must first set the private declaration created by yourself. This is to assign a value to the builder's claim. Once it is written in the standard declaration assignment, it will overwrite those standard declarations
                .setId(id)                  //Set jti(JWT ID): it is the unique identification of JWT. According to business needs, this can be set to a non repeated value, which is mainly used as a one-time token to avoid replay attacks.
                .setIssuedAt(now)           //IAT: issuing time of JWT
                .setSubject(subject)        //Subject: represents the main body of the JWT, that is, its owner. This is a string in json format, which can store userid, roldid, etc. as the unique flag of what user.
                .signWith(signatureAlgorithm, key);//Set the signature algorithm and secret key used for signature
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);     //Set expiration time
        }
        return builder.compact();
 }

//Parsing Token
public static Claims parseJWT(String jwt) throws Exception{
    SecretKey key = generalKey();  //The signature secret key as like as two peas of the generated signature.
    Claims claims = Jwts.parser()  //Get DefaultJwtParser
            .setSigningKey(key)         //Set the signature key
            .parseClaimsJws(jwt).getBody();//Set the jwt to be resolved
    return claims;
}

When the user logs in, the server judges the user name and password. If the user name and password are correct, a Token will be generated and returned to the browser, which will be stored in the JS cookie.

Token verification of request

All subsequent requests get Token from JS cookie and set Token in Http request header. The front-end of matrix Web uses axios network request framework, which can set the Token before the request is sent. The front-end code is as follows:


// request interceptor
service.interceptors.request.use(
  config => {
    var token = getToken()
    if (token) {
      config.headers['requestId'] = guid()
      config.headers['Authorization'] = token // Let each request carry a custom token. Please modify it according to the actual situation
    }
    // config.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
    return config
  },
  error => {
    // Do something with request error
    console.log('error',error) // for debug
    Promise.reject(error)
  }
)

Initial verification Token of backend HandlerInterceptor

When the current request reaches the matrix web back-end server, our Spring MVC HandlerInterceptor initially verifies whether the Token exists. The implementation class SecurityInterceptor implements the HandlerInterceptor interface, and obtains the Token in the preHandle sending method. If the Token does not exist, it returns no permission access. The specific code is as follows:


@Component
public class SecurityInterceptor implements HandlerInterceptor {

    LogUtils LOG = new LogUtils(SecurityInterceptor.class);

    private static final String ERROR_MSG = "{\"code\":\"1\",\"msg\":\"you have no permission to access\"}";


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        //Reject user request if user is not logged in

        String method = request.getMethod();
        if (ApiConstants.HTTP_METHOD_OPTIONS.equals(method)) {
            return true;
        }
        String token = UserUtils.getCurrentToken();
        LOG.info("requst uri:" + request.getRequestURI() + ",request token:" + token);
        if (StringUtils.isEmpty(token)) {
            writeNoPermission(response);
        }
        return true;
    }



    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {

    }


    private void writeNoPermission(ServletResponse servletResponse) {
        try {
            servletResponse.getWriter().write(ERROR_MSG);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

The above SecurityInterceptor needs to be registered in the WebMvcConfigurerAdapter of Spring MVC. The scope of SecurityInterceptor needs to remove the interfaces related to login, registration, druid monitoring and swagger. The specific implementation is as follows:


@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
    /**
     * Define exclude intercept path
     */
    public static String[] EXCLUDE_PATH_PATTERN = {
            //File upload and download
            "/file/**",
            //h5 It is recommended to split the api used by front-end h5 and back-end h5 into two services in production,
              //druid Monitoring request
            "/druid/**",

            //User registration and login
            "/user/register", "/user/login",
            //Error resource
            "/error",
            //swagger Online api documentation resources
            "/swagger-resources","/v2/api-docs","/swagger-ui.html","/webjars/**"
    };

    /**
     * Register the custom interceptor, add intercepting path and exclude intercepting path
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/**").excludePathPatterns(EXCLUDE_PATH_PATTERN);

    }
 }

In this way, through Spring Mvc's HandlerInterceptor, we can preliminarily determine whether the request carries a Token or not, and which requests are white list requests, without verifying the Token.

Authority judgment

When the request passes through the HandlerInterceptor interface of Spring MVC, the request will enter the specific Controller layer. Matrix web imitates the authority judgment mode of Spring security. It uses annotation Aop to automatically generate the area type facet in the class containing the method of custom annotation @ HasPermission. It will judge the authority before executing the specific code logic.

Custom annotation HasPermission

Write a user-defined annotation, as the pointcut of aop, with the properties of hasRole and hasPermission. The code is as follows:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HasPermission {

    String value() default "";

    String hasRole() default "";

    String hasPermission() default "";

}

aop implementation

Write a tangent, which is the annotation @ HasPermission, Around type notification, and judge the permission before the method. The method to determine permissions is checkPermission(hasPermission). The code is as follows:


@Aspect
@Component
@Slf4j
public class PermissionAspect implements Ordered {


    @Pointcut("@annotation(io.github.forezp.permission.HasPermission)")
    public void permissionPointCut() {

    }

    @Around("permissionPointCut()")
    public Object before(ProceedingJoinPoint point) throws Throwable {

        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        Annotation[] methodAnnotations = method.getDeclaredAnnotations();
        for (Annotation annotation : methodAnnotations) {
            if (annotation instanceof HasPermission) {
                HasPermission hasPermission = (HasPermission) annotation;
                if (!checkPermission(hasPermission)) {
                    throw new AriesException(NO_PERMISSION);
                }
            }
        }
   }

In the checkPermission method, the user id is first obtained according to the Token corresponding to the current request, and then the role set and permission set corresponding to the user are obtained according to the user id. Then compare and match with the attribute hasRole or hasPermission of annotation @ hasPermission. If the role set and permission set do not contain the hasRole or hasPermission above the annotation, the currently requested user has no permission to access, otherwise, he has permission. Please refer to the source code for the implementation logic of annotation code.

How do you use it?

Creating roles in matrix web management background requires ROLE_ADMIN role, annotate the interface where the role is created with @ HasPermission(hasRole = "ROLE_ADMIN”). The code is as follows:

@RestController
@RequestMapping("/user")
@Slf4j
public class SysUserController {

 @PostMapping("/roles")
    @HasPermission(hasRole = "ROLE_ADMIN")
    public RespDTO userSetRoles(@RequestParam String userId, @RequestParam String roleIds) {
        if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(roleIds)) {
            throw new AriesException(ERROR_ARGS);
        }

        sysUserService.setUserRoles(userId, roleIds);

        return RespDTO.onSuc(null);
    }
}

When the request calls this interface, the logic of aop will be executed first, and it will be judged whether the current user of the request has the hasRole or hasPermission permission permission of @ hasPermission annotation. If so, the normal logic will be executed, and if not, the operation without permission will be returned.

summary

This article and the previous article introduced the permission design and code implementation logic of matrix web in detail.

Source download

https://github.com/forezp/matrix-web

Posted by sparrrow on Thu, 28 May 2020 19:14:15 -0700