feign调用走不走网关全局拦截_服务之间调用还需要鉴权?(ruoyi-cloud 为例)
一、背景
一般在微服务架构中我们都会使用spring security oauth2来进行权限控制,我们将资源服务全部放在内网环境中,将API网关暴露在公网上,公网如果想要访问我们的资源必须经过API网关进行鉴权,鉴权通过后再访问我们的资源服务。我们根据如下图片来分析一下问题。
现在我们有三个服务:分别是用户服务、订单服务和产品服务。用户如果购买产品,则需要调用产品服务生成订单,那么我们在这个调用过程中有必要鉴权吗?答案是否定的,因为这些资源服务放在内网环境中,完全不用考虑安全问题。
如果要想实现这个功能,我们则需要来区分这两种请求,来自网关的请求进行鉴权,而服务间的请求则直接调用。是否可以给接口增加一个参数来标记它是服务间调用的请求?这样虽然可以实现两种请求的区分,但是实际中不会这么做。在 Spring Cloud Alibaba系列(三)使用feign进行服务调用中曾提到了实现feign的两种方式,一般情况下服务间调用和网关请求的数据接口是同一个接口,如果写成两个接口来分别给两种请求调用,这样无疑增加了大量重复代码。也就是说我们一般不会通过改变请求参数的个数来实现这两种服务的区分。虽然不能增加请求的参数个数来区分,但是我们可以给请求的header中添加一个参数用来区分。这样完全可以避免上面提到的问题。
3.1 自定义注解
我们自定义一个InnerAuth的注解,然后利用aop对这个注解进行处理
package com.ruoyi.common.security.annotation; import java.lang.annotation.*; * 内部认证注解 * @author ruoyi @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface InnerAuth * 是否校验用户信息 boolean isUser() default false;
package com.ruoyi.common.security.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import com.ruoyi.common.core.constant.SecurityConstants; import com.ruoyi.common.core.exception.InnerAuthException; import com.ruoyi.common.core.utils.ServletUtils; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.security.annotation.InnerAuth; * 内部服务调用验证处理 * @author ruoyi @Aspect @Component public class InnerAuthAspect implements Ordered @Around("@annotation(innerAuth)") public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE); // 内部请求验证 if (!StringUtils.equals(SecurityConstants.INNER, source)) throw new InnerAuthException("没有内部访问权限,不允许访问"); String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID); String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME); // 用户信息验证 if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) throw new InnerAuthException("没有设置用户信息,不允许访问 "); return point.proceed(); * 确保在权限认证aop执行前执行 @Override public int getOrder() return Ordered.HIGHEST_PRECEDENCE + 1;
3.2 如何去请求
定义一个接口:
服务间进行调用,传请求头
package com.ruoyi.system.api; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import com.ruoyi.common.core.constant.SecurityConstants; import com.ruoyi.common.core.constant.ServiceNameConstants; import com.ruoyi.common.core.domain.R; import com.ruoyi.system.api.domain.SysUser; import com.ruoyi.system.api.factory.RemoteUserFallbackFactory; import com.ruoyi.system.api.model.LoginUser; * 用户服务 * @author ruoyi @FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class) public interface RemoteUserService * 通过用户名查询用户信息 * @param username 用户名 * @param source 请求来源 * @return 结果 @GetMapping("/user/info/{username}") public R<LoginUser> getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); * 注册用户信息 * @param sysUser 用户信息 * @param source 请求来源 * @return 结果 @PostMapping("/user/register") public R<Boolean> registerUserInfo(@RequestBody SysUser sysUser, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
4.1 安全性
上面虽然实现了服务间调用,但是我们将@InnerAuth的请求暴露出去了,也就是说不用鉴权既可以访问到,那么我们是不是可以模拟一个请求头,然后在其他地方通过网关来调用呢?答案是可以,那么,这时候我们就需要对网关中分发的请求进行处理,在网关中写一个全局拦截器,将请求头的form参数清洗。gateway 网关中认证。
package com.ruoyi.gateway.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import com.ruoyi.common.core.constant.CacheConstants; import com.ruoyi.common.core.constant.HttpStatus; import com.ruoyi.common.core.constant.SecurityConstants; import com.ruoyi.common.core.constant.TokenConstants; import com.ruoyi.common.core.utils.JwtUtils; import com.ruoyi.common.core.utils.ServletUtils; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.redis.service.RedisService; import com.ruoyi.gateway.config.properties.IgnoreWhiteProperties; import io.jsonwebtoken.Claims; import reactor.core.publisher.Mono; * 网关鉴权 * @author ruoyi @Component public class AuthFilter implements GlobalFilter, Ordered private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); // 排除过滤的 uri 地址,nacos自行添加 @Autowired private IgnoreWhiteProperties ignoreWhite; @Autowired private RedisService redisService; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); String url = request.getURI().getPath(); // 跳过不需要验证的路径 if (StringUtils.matches(url, ignoreWhite.getWhites())) return chain.filter(exchange); String token = getToken(request); if (StringUtils.isEmpty(token)) return unauthorizedResponse(exchange, "令牌不能为空"); Claims claims = JwtUtils.parseToken(token); if (claims == null) return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); String userkey = JwtUtils.getUserKey(claims); boolean islogin = redisService.hasKey(getTokenKey(userkey)); if (!islogin) return unauthorizedResponse(exchange, "登录状态已过期"); String userid = JwtUtils.getUserId(claims); String username = JwtUtils.getUserName(claims); if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) return unauthorizedResponse(exchange, "令牌验证失败"); // 设置用户信息到请求 addHeader(mutate, SecurityConstants.USER_KEY, userkey); addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); // 内部请求来源参数清除(为了安全性考虑外部调用时清除请求头中的from-source参数) removeHeader(mutate, SecurityConstants.FROM_SOURCE); return chain.filter(exchange.mutate().request(mutate.build()).build()); private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) if (value == null) return; String valueStr = value.toString(); String valueEncode = ServletUtils.urlEncode(valueStr); mutate.header(name, valueEncode); private void removeHeader(ServerHttpRequest.Builder mutate, String name) mutate.headers(httpHeaders -> httpHeaders.remove(name)).build(); private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath()); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED); * 获取缓存key private String getTokenKey(String token) return CacheConstants.LOGIN_TOKEN_KEY + token; * 获取请求token private String getToken(ServerHttpRequest request) String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION); // 如果前端设置了令牌前缀,则裁剪掉前缀 if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY); return token; @Override public int getOrder() return -200;
4.2 扩展性
我们自定义@Inner注解的时候,放了一个boolean类型的value(),默认为true。如果我们想让这个请求可以通过网关访问的话,将value赋值为false即可。
这样我们总共实现了以下几个功能:
服务间访问可以不鉴权,添加注解@Inner即可。
网关访问不需要鉴权的资源,添加注解@Inner(value=false)即可。当然,这样服务间不鉴权也可以访问。
为了安全性考虑,将网关中的请求头form参数清洗,以防有人模拟请求,来访问资源。
由于各个服务都是在内网环境中,只有网关会暴露公网,因此服务间调用是没必要鉴权的。
所有评论(0)