User authentication and authorization topics for micro services (under)

[TOC]


AOP implement the login status check

In the user authentication and authorization topics for micro services (on) a paper simply introduces several common authentication and authorization scheme under micro-services, and the use of JWT prepared a minimalist demo Token issued to simulate and verify. The purpose of this paper is a continuation of several points to supplement the above, such as how Token passed between a plurality of micro-services, and how to use AOP checksum unified login state and permissions.

In order to check the logic state can log on GM, we generally will choose to use a filter, AOP interceptors and other means to achieve this function. And this section is to introduce the use of AOP implementation logged in check because the use of AOP intercept resource access request can also be protected, do some necessary checks in front of resource access.

First, we need to add depend on AOP in the project:

<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Definition of an annotation, a method for identifying which needs to be checked login state before being accessed. Specific code as follows:

package com.zj.node.usercenter.auth;

/**
 * 被该注解标记的方法都需要检查登录状态
 *
 * @author 01
 * @date 2019-09-08
 **/
public @interface CheckLogin {
}

Write an aspect, to achieve the specified logical checking login state, as follows:

package com.zj.node.usercenter.auth;

import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 登录态检查切面类
 *
 * @author 01
 * @date 2019-09-08
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CheckLoginAspect {

    private static final String TOKEN_NAME = "X-Token";

    private final JwtOperator jwtOperator;

    /**
     * 在执行@CheckLogin注解标识的方法之前都会先执行此方法
     */
    @Around("@annotation(com.zj.node.usercenter.auth.CheckLogin)")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 从header中获取Token
        String token = request.getHeader(TOKEN_NAME);

        // 校验Token是否合法
        Boolean isValid = jwtOperator.validateToken(token);
        if (BooleanUtils.isFalse(isValid)) {
            log.warn("登录态校验不通过,无效的Token:{}", token);
            // 抛出自定义异常
            throw new SecurityException("Token不合法!");
        }

        // 校验通过,可以设置用户信息到request里
        Claims claims = jwtOperator.getClaimsFromToken(token);
        log.info("登录态校验通过,用户名:{}", claims.get("userName"));
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("userName", claims.get("userName"));
        request.setAttribute("role", claims.get("role"));

        return joinPoint.proceed();
    }
}

Then write two connectors for analog protected resources and access token. code show as below:

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    private final JwtOperator jwtOperator;

    /**
     * 需要校验登录态后才能访问的资源
     */
    @CheckLogin
    @GetMapping("/{id}")
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }

    /**
     * 模拟生成token
     *
     * @return token
     */
    @GetMapping("gen-token")
    public String genToken() {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("id", 1);
        userInfo.put("userName", "小眀");
        userInfo.put("role", "user");

        return jwtOperator.generateToken(userInfo);
    }
}

Finally, we conduct a simple test to see if the method will first perform the cut for access to protected resources to check the login state. First start the project get token:
User authentication and authorization topics for micro services (under)

Bring token in the header at the time of access to protected resources:
User authentication and authorization topics for micro services (under)

Successful visit, this time the console output is as follows:
User authentication and authorization topics for micro services (under)

Tips:

There is no reason why the use of filters or interceptor achieved verify the login state, instead of using AOP, because the use of code written AOP relatively clean and can be implemented using a custom annotation effect pluggable access e.g. a resource state not to sign checks, then simply take @CheckLoginnotes to be removed. The other is the more important part of AOP basics, but also frequently asked in the interview knowledge through practical application examples of this, let us use the technique of AOP have a certain understanding.

Of course, you can also choose to filter or interceptor realized, did not say which way is the best, after all, these three methods have their own characteristics and advantages and disadvantages need to be selected according to specific business scenarios.


Feign realize Token passing

在微服务架构中通常会使用Feign来调用其他微服务所提供的接口,若该接口需要对登录态进行检查的话,那么就得传递当前客户端请求所携带的Token。而默认情况下Feign在请求其他服务的接口时,是不会携带任何额外信息的,所以此时我们就得考虑如何在微服务之间传递Token。

让Feign实现Token的传递还是比较简单的,主要有两种方式,第一种是使用Spring MVC的@RequestHeader注解。如下示例:

@FeignClient(name = "order-center")
public interface OrderCenterService {

    @GetMapping("/orders/{id}")
    OrderDTO findById(@PathVariable Integer id,
                      @RequestHeader("X-Token") String token);
}

Controller里的方法也需要使用这个注解来从header中获取Token,然后传递给Feign。如下:

@RestController
@RequiredArgsConstructor
public class TestController {

    private final OrderCenterService orderCenterService;

    @GetMapping("/{id}")
    public OrderDTO findById(@PathVariable("id") Integer id,
                            @RequestHeader("X-Token") String token) {
        return orderCenterService.findById(id, token);
    }
}

从上面这个例子可以看出,使用@RequestHeader注解的优点就是简单直观,而缺点也很明显。当只有一两个接口需要传递Token时,这种方式还是可行的,但如果有很多个远程接口需要传递Token的话,那么每个方法都得加上这个注解,显然会增加很多重复的工作。

所以第二种传递Token的方式更为通用,这种方式是通过实现一个Feign的请求拦截器,然后在拦截器中获取当前客户端请求所携带的Token并添加到Feign的请求header中,以此实现Token的传递。如下示例:

package com.zj.node.contentcenter.feignclient.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 请求拦截器,实现在服务间传递Token
 *
 * @author 01
 * @date 2019-09-08
 **/
public class TokenRelayRequestInterceptor implements RequestInterceptor {

    private static final String TOKEN_NAME = "X-Token";

    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 获取当前的request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 从header中获取Token
        String token = request.getHeader(TOKEN_NAME);

        // 传递token
        requestTemplate.header(TOKEN_NAME,token);
    }
}

然后需要在配置文件中,配置该请求拦截器的包名路径,不然不会生效。如下:

# 定义feign相关配置
feign:
  client:
    config:
      # default即表示为全局配置
      default:
        requestInterceptor:
          - com.zj.node.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor

RestTemplate实现Token传递

除了Feign以外,部分情况下有可能会使用RestTemplate来请求其他服务的接口,所以本小节也介绍一下,在使用RestTemplate的情况下如何实现Token的传递。

RestTemplate也有两种方式可以实现Token的传递,第一种方式是请求时使用exchange()方法,因为该方法可以接收header。如下示例:

@RestController
@RequiredArgsConstructor
public class TestController {

    private final RestTemplate restTemplate;

    @GetMapping("/{id}")
    public OrderDTO findById(@PathVariable("id") Integer id,
                            @RequestHeader("X-Token") String token) {
        // 传递token                    
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Token", token);

        return restTemplate.exchange(
                "http://order-center/orders/{id}",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                OrderDTO.class,
                id).getBody();
    }
}

另一种则是实现ClientHttpRequestInterceptor接口,该接口是RestTemplate的拦截器接口,与Feign的拦截器类似,都是用来实现通用逻辑的。具体代码如下:

public class TokenRelayRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final String TOKEN_NAME = "X-Token";

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        // 获取当前的request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest servletRequest = attributes.getRequest();
        // 从header中获取Token
        String token = servletRequest.getHeader(TOKEN_NAME);

        // 传递Token
        request.getHeaders().add(TOKEN_NAME,token);
        return execution.execute(request, body);
    }
}

Finally, you need to implement the interceptor is registered in RestTemplate allowed to take effect, the code is as follows:

@Configuration
public class BeanConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(
                new TokenRelayRequestInterceptor()
        ));

        return restTemplate;
    }
}

AOP achieve user permissions verification

In the first section we describe how to use AOP implementation login state inspection, in addition to some protected resources may require the user to have certain privileges can access, then we have to do before school permission to access the resource is experience. Similarly permission check function may also be used filters, interceptors or AOP achieved as before using AOP this section as an example.

Here nothing is too complex validation logic, mainly to determine whether the user is a role can be. So first define a comment, the comment has a value, that identifies the protected resources require users to which roles are allowed to access. code show as below:

package com.zj.node.usercenter.auth;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * 被该注解标记的方法都需要检查用户权限
 *
 * @author 01
 * @date 2019-09-08
 **/
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {

    /**
     * 允许访问的角色名称
     */
    String value();
}

Then define a section for realizing the specific rights validation logic. code show as below:

package com.zj.node.usercenter.auth;

import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 权限验证切面类
 *
 * @author 01
 * @date 2019-09-08
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuthAspect {

    private static final String TOKEN_NAME = "X-Token";

    private final JwtOperator jwtOperator;

    /**
     * 在执行@CheckAuthorization注解标识的方法之前都会先执行此方法
     */
    @Around("@annotation(com.zj.node.usercenter.auth.CheckAuthorization)")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 从header中获取Token
        String token = request.getHeader(TOKEN_NAME);

        // 校验Token是否合法
        Boolean isValid = jwtOperator.validateToken(token);
        if (BooleanUtils.isFalse(isValid)) {
            log.warn("登录态校验不通过,无效的Token:{}", token);
            // 抛出自定义异常
            throw new SecurityException("Token不合法!");
        }

        Claims claims = jwtOperator.getClaimsFromToken(token);
        String role = (String) claims.get("role");
        log.info("登录态校验通过,用户名:{}", claims.get("userName"));

        // 验证用户角色名称是否与受保护资源所定义的角色名称匹配
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckAuthorization annotation = signature.getMethod()
                .getAnnotation(CheckAuthorization.class);
        if (!annotation.value().equals(role)) {
            log.warn("权限校验不通过!当前用户角色:{} 允许访问的用户角色:{}",
                    role, annotation.value());
            // 抛出自定义异常
            throw new SecurityException("权限校验不通过,无权访问该资源!");
        }

        log.info("权限验证通过");
        // 设置用户信息到request里
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("userName", claims.get("userName"));
        request.setAttribute("role", claims.get("role"));

        return joinPoint.proceed();
    }
}

When used together with the notes and only need to set the role name to, the following examples:

/**
 * 需要校验登录态及权限后才能访问的资源
 */
@GetMapping("/{id}")
@CheckAuthorization("admin")
public User findById(@PathVariable Integer id) {
    log.info("get request. id is {}", id);
    return userService.findById(id);
}

Guess you like

Origin blog.51cto.com/zero01/2436642