Spring Cloud GateWay实现token的校验和传输

Spring Cloud GateWay实现token的校验和传输

1.1 SpringCloud Gateway 简介

Spring Cloud GateWay基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty.

Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

提前声明:Spring Cloud Gateway 底层使用了高性能的通信框架Netty

1.2 SpringCloud Gateway 特征

SpringCloud官方,对SpringCloud Gateway 特征介绍如下:

  • 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0

  • 集成 Hystrix 断路器

  • 集成 Spring Cloud DiscoveryClient

  • Predicates 和 Filters 作用于特定路由,易于编写的 Predicates 和 Filters

  • 具备一些网关的高级功能:动态路由、限流、路径重写

从以上的特征来说,和Zuul的特征差别不大。SpringCloud Gateway和Zuul主要的区别,还是在底层的通信框架上。

简单说明一下上文中的三个术语:

**(**1) Filter(过滤器)

和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。

(2)Route(路由)

网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。

**(**3)Predicate(断言)

这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。

1.3 SpringCloud Zuul的IO模型/GateWay的IO模型

Zuul

Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。

大家知道,servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。

弊端:servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。

所以Springcloud Zuul 是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet),并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端。虽然Zuul 2.0开始,使用了Netty,并且已经有了大规模Zuul 2.0集群部署的成熟案例,但是,Springcloud官方已经没有集成改版本的计划了。

GateWay

Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。

Webflux虽然可以兼容多个底层的通信框架,但是一般情况下,底层使用的还是Netty,毕竟,Netty是目前业界认可的最高性能的通信框架。而Webflux的Loop线程,正好就是著名的Reactor 模式IO处理模型的Reactor线程,如果使用的是高性能的通信框架Netty,这就是Netty的EventLoop线程。

1.4 token传递以及存储

由于在网关层面拦截token之后,需要检验并且解析,并且将解析后的token(包含用户信息)传递给其他微服务,实现微服务之间的调用

AuthLoginGlobalFilter全局认证过滤器,实现GlobalFilter接口,进行请求的过滤.

@Slf4j
@PropertySource(value = "classpath:loginfilter.properties")
@Component
public class AuthLoginGlobalFilter  implements GlobalFilter, Ordered {

    @Value("#{'${jwt.ignoreUrls}'.split(',')}")
    List<String> ignoreUrls;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * @Description  全局校验过滤器
     * @author Liu_gx
     * @date 2021/1/8 17:29
     * @param exchange
     * @param chain
     * @return
     */
    /*@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request=exchange.getRequest();
        if(ignoreUrls!=null&&ignoreUrls.contains(request.getURI().getPath())){
            return chain.filter(exchange);
        }
        //获取token
        String authoriztion=request.getHeaders().getFirst(Constants.LOGIN_TOKEN);
        //获取服务名称
        String serverName = getServerNameByPath(request.getURI().getPath());
        //获取具体的请求服务实例
        AbstractRequestUrlResolver serverInstance = AbstractRequestUrlResolver.getServerInstance(serverName);
        if(null == serverInstance){
            return onError(exchange,new ResponseEntity("fail", HttpStatus.BAD_REQUEST.value(), "暂不支持该登录类型", Collections.EMPTY_MAP));
        }
        serverInstance.setRedisTemplate(redisTemplate);
        //token如果为null,app端需要校验JwtUser,放行
        if(StringUtils.isEmpty(authoriztion)){
            //是否需要强制校验token
            if(serverInstance.tokenNullIsCheckToken()){
                return onError(exchange,serverInstance.setResponseEntity("fail", HttpStatus.UNAUTHORIZED.value(), "尚未登录", Collections.EMPTY_MAP));
            }else {
                ServerHttpRequest shr=request.mutate().header(Constants.JWT_USERINFO, JSON.toJSONString( new JwtUserInfo())).build();
                return chain.filter(exchange.mutate().request(shr).build());
            }
        }
        //校验token
        ResponseEntity responseEntity = serverInstance.checkAuthToken(authoriztion);
        if(null != responseEntity && responseEntity.getResult().equals("fail")){
            return onError(exchange,responseEntity);
        }

        ServerHttpRequest shr=request.mutate().header(Constants.JWT_USERINFO, JSON.toJSONString(responseEntity.getData())).build();
        return chain.filter(exchange.mutate().request(shr).build());
    }*/

    private static final String X_CLIENT_TOKEN_USER = "x-client-token-user";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request=exchange.getRequest();
        if(ignoreUrls!=null&&ignoreUrls.contains(request.getURI().getPath())){
            return chain.filter(exchange);
        }
        //获取token
        String authoriztion=request.getHeaders().getFirst(Constants.LOGIN_TOKEN);
        //获取服务名称
        String serverName = getServerNameByPath(request.getURI().getPath());
        //获取具体的请求服务实例
        AbstractRequestUrlResolver serverInstance = AbstractRequestUrlResolver.getServerInstance(serverName);
        if(null == serverInstance){
            return onError(exchange,new ResponseEntity("fail", HttpStatus.BAD_REQUEST.value(), "暂不支持该登录类型", Collections.EMPTY_MAP));
        }
        serverInstance.setRedisTemplate(redisTemplate);
        //token如果为null,app端需要校验JwtUser,放行
        if(StringUtils.isEmpty(authoriztion)){
            //是否需要强制校验token
            if(serverInstance.tokenNullIsCheckToken()){
                return onError(exchange,serverInstance.setResponseEntity("fail", HttpStatus.UNAUTHORIZED.value(), "尚未登录", Collections.EMPTY_MAP));
            }else {
                ServerHttpRequest shr=request.mutate().header(X_CLIENT_TOKEN_USER, JSON.toJSONString( new JwtUserInfo())).build();
                return chain.filter(exchange.mutate().request(shr).build());
            }
        }
        //校验token
        ResponseEntity responseEntity = serverInstance.checkAuthToken(authoriztion);
        if(null != responseEntity && responseEntity.getResult().equals("fail")){
            return onError(exchange,responseEntity);
        }
        JSONObject jsonObject = (JSONObject) JSON.toJSON(responseEntity.getData());
        ServerHttpRequest shr=request.mutate().header(X_CLIENT_TOKEN_USER, JSON.toJSONString(responseEntity.getData())).build();
        return chain.filter(exchange.mutate().request(shr).build());
    }

    /**
     * @Description  获取请求的服务名称
     * @author Liu_gx
     * @date 2021/1/8 17:28
     * @param path  请求路径
     * @return
     */
    private String getServerNameByPath(String path){
        return   getServerNameByPath(path,
                    (pathUrl)->{
                        return pathUrl.replace("/api/", "");
                    },(pathUrl)->{
                        return pathUrl.substring(0, pathUrl.indexOf("/"));
                    });
    }

    private  String getServerNameByPath(String str, Function<String,String> fun1,Function<String,String> fun2){
        return fun1.andThen (fun2).apply (str);
    }

    /**
     * @Description  异常返回信息
     * @author Liu_gx
     * @date 2021/1/8 17:29
     * @param exchange
     * @param rd
     * @return
     */
    private Mono<Void> onError(ServerWebExchange exchange,ResponseEntity rd){
        ServerHttpResponse response=exchange.getResponse();
        response.setStatusCode(HttpStatus.valueOf(rd.getRescode()));
        response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        ObjectMapper objectMapper=new ObjectMapper();
        String rs="";
        try {
            rs=objectMapper.writeValueAsString(rd);
        } catch (JsonProcessingException e) {
            log.error("occur Exception:"+e);
        }
        DataBuffer buffer= response.bufferFactory().wrap(rs.getBytes());
        return response.writeWith(Flux.just(buffer));
    }

    @Override
    public int getOrder() {
        return 0;
    }

}

方式一

HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:

  • supportsParameter(满足某种要求,返回true,方可进入resolveArgument做参数处理)
  • resolveArgument 解析操作

知识储备已到位,接下来着手实现,主要分为三步走:

  1. 自定义参数注解@LoginUser,添加至controller的方法参数userId之上;
  2. 自定义方法参数解析器LoginUserHandlerResolver,取出request中的userInfo,并赋值给添加了@LoginUser注解的参数userId。
  3. 将自定义参数解析器LoginUserHandlerResolver注册到spring容器中.

在Common公用模块自定义参数解析器LoginUserHandlerResolver实现HandlerMethodArgumentResolver(方法参数解析器)

@Configuration
public class LoginUserHandlerResolver implements HandlerMethodArgumentResolver
{
    private static final String CURRENT_ID = "jwtUserInfo";

    @Override
    public boolean supportsParameter(MethodParameter parameter)
    {
        return parameter.hasParameterAnnotation(LoginUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest nativeWebRequest, WebDataBinderFactory factory) throws Exception
    {
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        // 获取用户ID
        String userid = String.valueOf(request.getHeader(CURRENT_ID));
        if (userid == null){
            return null;
        }
        return userid;
    }
}

定义@LoginUser注解,绑定用户信息

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser
{}

将自定义解析器注册到spring容器中

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
 
    @Value("${file.userfiles-path}")
    private String filePath;
 
    /**
     * 登录校验拦截器
     *
     * @return
     */
    @Bean
    public AuthenticationInterceptor loginRequiredInterceptor() {
        return new AuthenticationInterceptor();
    }
 
    /**
     * CurrentUser 注解参数解析器
     *
     * @return
     */
    @Bean
    public CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver() {
        return new CurrentUserMethodArgumentResolver();
    }
 
    /**
     * 参数解析器
     *
     * @param argumentResolvers
     */
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(currentUserMethodArgumentResolver());
        super.addArgumentResolvers(argumentResolvers);
    }
 
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginRequiredInterceptor())
                .addPathPatterns(Constants.BASE_API_PATH + "/**")
                .excludePathPatterns(Constants.BASE_API_PATH + "/login");
        super.addInterceptors(registry);
    }
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/META-INF/resources/")
                .addResourceLocations("classpath:/static/page/")
                .addResourceLocations("classpath:/static/templates/")
                .addResourceLocations("file:" + filePath);
    }
 
    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.QuoteFieldNames,
                SerializerFeature.WriteEnumUsingToString,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteDateUseDateFormat,
                SerializerFeature.DisableCircularReferenceDetect);
        fastJsonConfig.setSerializeFilters((ValueFilter) (o, s, source) -> {
            if (null != source && (source instanceof Long || source instanceof BigInteger) && source.toString().length() > 15) {
                return source.toString();
            } else {
                return null == source ? EMPTY : source;
            }
        });
 
        //处理中文乱码问题
        List<MediaType> fastMediaTypes = new ArrayList<>();
        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        fastConverter.setSupportedMediaTypes(fastMediaTypes);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        converters.add(fastConverter);
    }
}

获取用户id

ResponseEntity<CentreListResponse<EfficiencyNeedhandleVo>> queryListByDayTwo(@LoginUser String userId, @RequestBody @Valid EfficiencyNeedhandleQueryForm from) throws UserCenterException {
     System.out.println(userId);
}

方式二

主要分为三步走:

  1. 自定义用户上下文UserContextHolder,并且使用ThreadLocal进行存储;
  2. 用户请求拦截器UserInterceptor实现HandlerInterceptor,取出request中的userInfo,并添加到用户上下文中;
  3. 将自定义拦截器UserInterceptor注册到spring容器中。

定义用户上下文

public class UserContextHolder {

    private ThreadLocal<Map<String, String>> threadLocal;

    private UserContextHolder() {
        this.threadLocal = new ThreadLocal<>();
    }

    /**
     * 创建实例
     *
     * @return
     */
    public static UserContextHolder getInstance() {
        return SingletonHolder.sInstance;
    }

    /**
     * 静态内部类单例模式
     * 单例初使化
     */
    private static class SingletonHolder {
        private static final UserContextHolder sInstance = new UserContextHolder();
    }

    /**
     * 用户上下文中放入信息
     *
     * @param map
     */
    public void setContext(Map<String, String> map) {
        threadLocal.set(map);
    }

    /**
     * 获取上下文中的信息
     *
     * @return
     */
    public Map<String, String> getContext() {
        return threadLocal.get();
    }

    /**
     * 获取上下文中的用户名
     *
     * @return
     */
    public String getUsername() {
        return Optional.ofNullable(threadLocal.get()).orElse(Maps.newHashMap()).get("id");
    }

    /**
     * 清空上下文
     */
    public void clear() {
        threadLocal.remove();
    }

}

定义用户请求拦截器UserInterceptor实现HandlerInterceptor,并且将拿到的用户信息存储到上下文UserContextHolder中

@Slf4j
public class UserInterceptor implements HandlerInterceptor {
    /**
     * 服务间调用token用户信息,格式为json
     * {
     * "user_name":"必须有"
     * "自定义key:"value"
     * }
     */
    public static final String X_CLIENT_TOKEN_USER = "x-client-token-user";
    /**
     * 服务间调用的认证token
     */
    public static final String X_CLIENT_TOKEN = "x-client-token";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从网关获取并校验,通过校验就可信任x-client-token-user中的信息
        checkToken(request.getHeader(X_CLIENT_TOKEN));
        String userInfoString = StringUtils.defaultIfBlank(request.getHeader(X_CLIENT_TOKEN_USER), "{}");
        UserContextHolder.getInstance().setContext(new ObjectMapper().readValue(userInfoString, Map.class));
        return true;
    }

    private void checkToken(String token) {
        //TODO 从网关获取并校验,通过校验就可信任x-client-token-user中的信息
        log.debug("//TODO 校验token:{}", token);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        UserContextHolder.getInstance().clear();
    }
}

将自定义拦截器注册到spring容器中

@Configuration
public class WebServerMvcConfigurerAdapter implements WebMvcConfigurer {

    @Bean
    public HandlerInterceptor userInterceptor() {
        return new UserInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor());
    }
}

获取用户信息

ResponseEntity<CentreListResponse<EfficiencyNeedhandleVo>> queryListByDayTwo(@RequestBody @Valid EfficiencyNeedhandleQueryForm from) throws UserCenterException {
        String username = UserContextHolder.getInstance().getUsername();
        System.out.println(username);
}
  • 10
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Spring Cloud Gateway是一个轻量级的网关框架,它可以作为微服务架构中的入口,实现代理和路由功能。在进行身份验证和授权时,可以使用Token进行安全验证。 Token是一种令牌,通常由后端服务颁发给客户端,用于验证客户端的身份和权限。在Spring Cloud Gateway中使用Token可以通过以下方式实现: 1. 首先,客户端在请求头中携带Token,可以使用常见的认证方案,如Bearer Token。在路由的配置文件中,可以通过添加过滤器来获取请求头中的Token,并进行验证。 2. 在网关层,可以编写自定义的GlobalFilter过滤器,在请求到达后执行Token的验证逻辑。可以通过解析Token的信息,比如用户名、角色等,来进行用户身份验证和授权操作。 3. 在网关层,还可以使用Spring Security进行身份验证和授权。Spring Security提供了丰富的特性,如基于角色的访问控制、加密解密等。可以通过配置Spring Security的过滤器链来实现Token验证的功能。 使用Token进行安全验证的好处是,可以将验证逻辑移到网关层,使后端服务更加专注于业务逻辑的实现Token的使用可以提供更高的灵活性和安全性,保护后端服务免受未经授权的访问。 总之,Spring Cloud Gateway可以通过添加过滤器、编写GlobalFilter过滤器或使用Spring Security来实现Token的安全验证。这样可以保护后端服务,确保只有经过身份验证和授权的客户端能够访问服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值