在这里插入图片描述

前言

更新到这里呢,终于满足了读者两年的好奇心。从上年年底探索Spring Cloud GateWay,众所周知,网关最大的用途在于限流、和路由,还有一个功能就是做鉴权,一直在我心中的疑惑,这个Spring Security OAuth2是和网关怎么关联的呢,最近终于找出了答案。上图:
在这里插入图片描述
首先,介绍下流程

  1. 客户端请求认证服务进行认证。
  2. 认证服务认证通过向浏览器cookie写入token(身份令牌),认证服务请求用户中心查询用户信息,认证服务请求Spring Security申请令牌,认证服务将token(身份令牌)和jwt令牌存储至redis中,认证服务向cookie写入 token(身份令牌)。
  3. 前端携带token请求认证服务获取jwt令牌,前端获取到jwt令牌并存储在sessionStorage,前端从jwt令牌中解析中用户信息并显示在页面。
  4. 前端携带cookie中的token身份令牌及jwt令牌访问资源服务,前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt令牌,前端请求资源服务前在http header上添加jwt请求资源。
  5. 网关校验token的合法性,用户请求必须携带token身份令牌和jwt令牌
    网关校验redis中token是否合法,已过期则要求用户重新登录
  6. 资源服务校验jwt的合法性并完成授权
    资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。

身份校验

需求分析

  1. 从cookie查询用户身份令牌是否存在,不存在则拒绝访问
  2. 从http header查询jwt令牌是否存在,不存在则拒绝访问
  3. 从Redis查询user_token令牌是否过期,过期则拒绝访问

分享整体代码结构

在这里插入图片描述
已经上传到github上面, https://github.com/fafeidou/fast-cloud-nacos.git,欢迎fork。

配置application.yml,没有配置redis,使用的默认配置

spring:
  application:
    name: security-api-gateway
  cloud:
    gateway:
      discovery:
        locator:
          lower-case-service-id: true  #gateway可以通过开启以下配置来打开根据服务的serviceId来匹配路由,默认是false大写
          enabled: true # 是否可以通过其他服务的serviceId来转发到具体的服务实例。默认为false
      routes:
      - id: security-auth
        uri: lb://security-auth # lb://serviceId
        predicates:
        - Path=/service-hi/auth/**
        filters:
        - StripPrefix=1
      - id: security-provider
        uri: lb://security-provider # lb://serviceId
        predicates:
        - Path=/security-provider/**
        filters:
        - StripPrefix=1
      - id: security-consumer
        uri: lb://security-consumer # lb://serviceId
        predicates:
        - Path=/security-consumer/**
        filters:
        - StripPrefix=1
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 使用nacos作为注册中心

创建全局处理器·TokenGlobalFilter实现GlobalFilter接口

public class TokenGlobalFilter implements GlobalFilter {
    @Autowired
    AuthService authService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String tokenFromCookie = authService.getTokenFromCookie(request);
        if (StringUtils.isEmpty(tokenFromCookie)) {
            ExceptionCast.cast(CommonCode.UNAUTHENTICATED);
        }
        //从header中取jwt
        String jwtFromHeader = authService.getJwtFromHeader(request);
        if (StringUtils.isEmpty(jwtFromHeader)) {
            //拒绝访问
            ExceptionCast.cast(CommonCode.UNAUTHENTICATED);
        }
        //从redis取出jwt的过期时间
        long expire = authService.getExpire(tokenFromCookie);
        if (expire < 0) {
            //拒绝访问
            ExceptionCast.cast(CommonCode.FORBIDDEN);
        }
        return chain.filter(exchange);
    }
}

添加gataway全局异常处理器

spring cloud gateway 是基于webflux的,用之前的controllerAdvice,已经不生效了,这里需要我们自己去实现,这里有两个类JsonExceptionHandlerErrorHandlerConfiguration,服务之间的调用controllerAdvice是生效的。这里只给出代码片段。具体代码请看github。

public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {

    public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
                                ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    /**
     * 获取异常属性
     */
    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        int code = 500;
        Throwable error = super.getError(request);
        if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
            code = 404;
        }
        if (error instanceof fast.cloud.nacos.common.model.exception.CustomException) {
            CustomException customException = (CustomException) error;
            return response(code, customException.getResultCode().message());
        }
        return response(code, this.buildMessage(request, error));
    }

    /**
     * 指定响应处理方法为JSON处理的方法
     *
     * @param errorAttributes
     */
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    /**
     * 根据code获取对应的HttpStatus
     *
     * @param errorAttributes
     */
    @Override
    protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
        int statusCode = (int) errorAttributes.get("code");
        return HttpStatus.valueOf(statusCode);
    }

    /**
     * 构建异常信息
     *
     * @param request
     * @param ex
     * @return
     */
    private String buildMessage(ServerRequest request, Throwable ex) {
        StringBuilder message = new StringBuilder("Failed to handle request [");
        message.append(request.methodName());
        message.append(" ");
        message.append(request.uri());
        message.append("]");
        if (ex != null) {
            message.append(": ");
            message.append(ex.getMessage());
        }
        return message.toString();
    }

    /**
     * 构建返回的JSON数据格式
     *
     * @param status       状态码
     * @param errorMessage 异常信息
     * @return
     */
    public static Map<String, Object> response(int status, String errorMessage) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", status);
        map.put("message", errorMessage);
        map.put("data", null);
        return map;
    }

}

测试

配置host

127.0.0.1 batman.com

启动nacos,api-gateway,auth,provider服务,使用postman测试

查看nacos已经注册这几个服务:

在这里插入图片描述

获取令牌。

POST 请求: http://batman.com:40400/auth/userlogin
username和password都是batman

在这里插入图片描述
这个时候身份令牌已经写入cookie,jwt令牌写入redis。

在这里插入图片描述
在这里插入图片描述

获取jwt令牌
GET http://batman.com:40400/auth/userjwt
在这里插入图片描述

调用provider接口
GET 请求 http://batman.com:18085/security-provider/security/hello
在这里插入图片描述

错误实例

token传到有误

在这里插入图片描述
由于篇幅问题,这篇就先写到这里,下次会分享下,微服务之间调用如何传递token的问题以及怎样实现授权。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐