マイクロサービスに対するユーザー認証と承認のトピック(下)

[TOC]


AOPは、ログイン状態のチェックを実装します

ではマイクロサービスに対するユーザーの認証と認可のトピック(上)紙単にマイクロサービスの下で、いくつかの共通の認証および認可スキームを紹介し、JWTの使用は、トークンがシミュレートして検証するために発行したミニマルなデモを用意しました。この論文の目的は、このようなトークンは、マイクロ複数のサービス間で渡される方法、およびAOPサム統一ログイン状態とパーミッションを使用する方法として、上記を補足するためのいくつかの点の続きです。

GMにログインすることができます論理状態を確認するために、我々は一般的に、この機能を実現するために、フィルタ、AOPインタセプタやその他の手段を使用することを選択します。そして、このセクションでは、AOPインターセプトリソースへのアクセス要求の使用をも保護することができますので、チェックしてログインAOPの実装の使用を紹介するリソースへのアクセスの前にいくつかの必要なチェックを行うことです。

まず、プロジェクトにAOPに依存追加する必要があります。

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

注釈の定義は、必要同定するための方法は、アクセスされる前に、ログイン状態をチェックします。具体的なコードは次のよう:

package com.zj.node.usercenter.auth;

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

以下のように、指定された論理チェックログイン状態を達成するために、側面を書きます:

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();
    }
}

そして、アナログ保護されたリソースとアクセストークンのための2つのコネクタを作成します。コードは以下の通りであります:

@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);
    }
}

最後に、私たちは、この方法は、まずログイン状態をチェックするために保護されたリソースへのアクセスのためのカットを実行するかどうか確認するために、簡単なテストを行っています。最初のプロジェクトは、トークンを取得開始します。
マイクロサービスに対するユーザー認証と承認のトピック(下)

保護されたリソースへのアクセス時のヘッダーにトークンを持参:
マイクロサービスに対するユーザー認証と承認のトピック(下)

次のように成功した訪問は、この時間は、コンソール出力は次のようになります。
マイクロサービスに対するユーザー認証と承認のトピック(下)

ヒント:

コードの使用は比較的きれいAOPを書かれており、例えば、カスタム注釈効果プラグイン可能なアクセスを使用して実装することができるので、フィルタやインターセプタの使用が代わりにAOPを使用しての、ログイン状態を確認達成しない理由はありませんリソースの状態は、単に取り、小切手に署名しないように@CheckLogin削除するノートが。他には、AOPの基本のより重要な部分ですが、また頻繁にこれの実用的な応用例を通じてインタビューの知識に尋ねた、私たちは一定の理解を持ってAOPの技術を使用してみましょう。

もちろん、あなたはまた、フィルタやインターセプタ実現することを選択し、最適な方法は言いませんでしたが、結局、これらの3つの方法は、独自の特徴と利点を持っていると短所は、特定のビジネスシナリオに応じて選択する必要があります。


トークンパッシングを実現装います

在微服务架构中通常会使用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);
    }
}

最後に、あなたがRestTemplateに登録されているを有効にするには許可されインターセプタを実装する必要があり、コードは次のとおりです。

@Configuration
public class BeanConfig {

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

        return restTemplate;
    }
}

AOPは、ユーザー権限の検証を実現します

我々はAOPの実装のログイン状態検査を使用する方法について説明最初のセクションでは、いくつかの保護されたリソースに加えて、特定の権限を持っているユーザーが必要な場合がありますアクセスすることができ、その後、我々は、リソースにアクセスするには、学校の許可の前に行う必要があります経験。同様に許可チェック機能はまた、例として、AOPこのセクションを使用して、前のように達成フィルタ、インターセプタまたはAOPを使用することができます。

ここでは何も主に、ユーザは役割ができているかどうかを判断するために、あまりにも複雑な検証ロジックではありません。だから、最初のコメントを定義し、コメントが保護されたリソースが役割がアクセスを許可されていることをユーザーに要求識別する値を持っています。コードは以下の通りであります:

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();
}

次に、具体的な権利の検証ロジックを実現するためのセクションを定義します。コードは以下の通りであります:

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();
    }
}

ノートだけに役割名を設定する必要があり、以下の例と一緒に使用する場合:

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

おすすめ

転載: blog.51cto.com/zero01/2436642