微服务的用户认证与授权杂谈(下)

[TOC]


AOP实现登录状态检查

微服务的用户认证与授权杂谈(上)一文中简单介绍了微服务下常见的几种认证授权方案,并且使用JWT编写了一个极简demo来模拟Token的颁发及校验。而本文的目的主要是延续上文来补充几个要点,例如Token如何在多个微服务间进行传递,以及如何利用AOP实现登录态和权限的统一校验。

为了让登录态的检查逻辑能够通用,我们一般会选择使用过滤器、拦截器以及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();
    }
}

然后编写两个接口用于模拟受保护的资源和获取token。代码如下:

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

最后我们来进行一个简单的测试,看看访问受保护的资源时是否会先执行切面方法来检查登录态。首先启动项目获取token:

在访问受保护的资源时在header中带上token:

访问成功,此时控制台输出如下:

Tips:

这里之所以没有使用过滤器或拦截器来实现登录态的校验,而是采用了AOP,这是因为使用AOP写出来的代码比较干净并且可以利用自定义注解实现可插拔的效果,例如访问某个资源不用进行登录态检查了,那么只需要把@CheckLogin注解给去掉即可。另外就是AOP属于比较重要的基础知识,也是在面试中经常被问到的知识点,通过这个实际的应用例子,可以让我们对AOP的使用技巧有一定的了解。

当然也可以选择过滤器或拦截器来实现,没有说哪种方式就是最好的,毕竟这三种方式都有各自的特性和优缺点,需要根据具体的业务场景来选择。


Feign实现Token传递

在微服务架构中通常会使用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作为示例。

这里也不做太复杂的校验逻辑,主要是判断用户是否是某个角色即可。所以首先定义一个注解,该注解有一个value,用于标识受保护的资源需要用户为哪个角色才允许访问。代码如下:

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

原文地址:https://blog.51cto.com/zero01/2436642

时间: 2024-10-10 00:13:57

微服务的用户认证与授权杂谈(下)的相关文章

微服务统一登陆认证怎么做?JWT ?

无状态登录原理 1.1.什么是有状态? 有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session. 例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session.然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息. 缺点是什么? 服务端保存大量数据,增加服务端压力 服务端保存用户状态,无法进行水平扩展 客户端

nginx服务做用户认证和基于域名的虚拟主机

实验一.用nginx怎么实现用户访问时的认证 一.目标        通过调整Nginx服务端配置,实现以下目标: 访问Web页面需要进行用户认证 用户名为:tom,密码为:123456 二.方案         通过Nginx实现Web页面的认证,需要修改Nginx配置文件,在配置文件中添加auth语句实现用户认证.    最后使用htpasswd命令创建用户及密码即可,服务端:192.168.4.102,客户端:192.168.4.101 三.实施步骤(nginx服务安装见我的"搭建ngin

Spring cloud微服务安全实战-5-6实现授权码认证流程(2)

授权服务器,返回给我一个授权码,这里我只需要把授权传回去就可以了.来证明我是这个服务器. URI的地址传和第一次的地址一样的,认证服务器会比,第一次跳转的请求和第二次申请令牌的请求redirect_uri这个参数是不是一致,如果不一致 就会报错.这样发出请求后 ,就会拿到一个令牌. 前端的服务器拿到令牌后还是要回到前端页面 跳转到根目录 调回来就回有个问题.autenticated现在的值是false,没有一个地方把autenticated设置为true.那么页面一刷新autenticated就

Spring Cloud Alibaba微服务从入门到进阶 完整版

第1章 课程介绍课程的总体介绍,课程需要的环境搭建和一些常用的快捷键介绍. 第2章 Spring Boot基础前期先带着学习Spring Boot基础,创建Spring Boot项目,讲解Spring Boot的配置,是学习Spring Cloud Alibaba的必知必会. 第3章 微服务的拆分与编写这一章讲解的微服务的概念,使用场景,建模,架构通览,讲师带着拆分微服务并且一步步分析,编写一些基础的微服务功能 第4章 Spring Cloud Alibaba介绍学习Spring Cloud A

Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务

在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka 服务治理下的应用,同时也从 Eureka 服务中获取所有其他微服务的实例信息.搭建 OAuth2 认证授权服务,并不是给每个微服务调用,而是通过 API 网关进行统一调用来对网关后的微服务做前置过滤,所有的请求都必须先通过 API 网关,API 网关在进行路由转发之前对该请求进行前置校验,实现对微服

微服务架构的登陆认证问题

从过去单体应用架构到分布式应用架构再到现在的微服务架构,应用的安全访问在不断的经受考验.为了适应架构的变化.需求的变化,身份认证与鉴权方案也在不断的更新变革.面对数十个甚至上百个微服务之间的调用,如何保证高效安全的身份认证?面对外部的服务访问,该如何提供细粒度的鉴权方案?本文将会为大家阐述微服务架构下的安全认证与鉴权方案. 传统的单体应用场景下,应用是一个整体,一般针对所有的请求都会进行权限校验.请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问

微服务架构中的安全认证与鉴权

转载:http://www.bootdo.com/blog/open/post/125 从单体应用架构到分布式应用架构再到微服务架构,应用的安全访问在不断的经受考验.为了适应架构的变化.需求的变化,身份认证与鉴权方案也在不断的变革.面对数十个甚至上百个微服务之间的调用,如何保证高效安全的身份认证?面对外部的服务访问,该如何提供细粒度的鉴权方案?本文将会为大家阐述微服务架构下的安全认证与鉴权方案. 单体应用 VS 微服务 随着微服务架构的兴起,传统的单体应用场景下的身份认证和鉴权面临的挑战越来越大

微服务架构下的身份认证

从单体应用架构到分布式应用架构再到微服务架构,应用的安全访问在不断的经受考验.为了适应架构的变化.需求的变化,身份认证与鉴权方案也在不断的变革.面对数十个甚至上百个微服务之间的调用,如何保证高效安全的身份认证?面对外部的服务访问,该如何提供细粒度的鉴权方案?本文将会为大家阐述微服务架构下的安全认证与鉴权方案. 单体应用 VS 微服务 随着微服务架构的兴起,传统的单体应用场景下的身份认证和鉴权面临的挑战越来越大.单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验.请求一般会通过一个

微服务系统中的认证策略

软件安全本身就是个很复杂的问题,由于微服务系统中的每个服务都要处理安全问题,所以在微服务场景下会更复杂.David Borsos在最近的伦敦微服务大会上作了相关内容的演讲,并评估了四种面向微服务系统的身份验证方案. 在传统的单体架构中,单个服务保存所有的用户数据,可以校验用户,并在认证成功后创建HTTP会话.在微服务架构中,用户是在和服务集合交互,每个服务都有可能需要知道请求的用户是谁.一种朴素的解决方案是在微服务系统中应用与单体系统中相同的模式,但是问题就在于如何让所有的服务访问用户的数据.解