📄 @Inner 注解使用及原理 👍
内部资料,请扫码登录
# @Inner 注解原理和使用
# ① 服务交互三种情况
- 外部从 Gateway 访问,需要鉴权(eg.CURD 操作)。这种是最常使用的,用户登录后正常访问接口,不需要添加 @Inner
- 如上图 ④⑤ ,外部从 Gateway 访问不需要鉴权(eg.获取短信验证码接口)。 只需要再目标接口增加 @Inner(false)
- 如上图 ①②③,外部从 Gateway 访问的请求没有携带 token(eg.登录接口), auth 需要调用 upms 的接口获取用户信息
# ② Inner 的原理
# 统一的 ignore-url 处理
首先我们来看看这个注解的代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
/**
* 是否AOP统一处理(可以理解为是否仅允许Feign之间调用)
*
* @return false, true
*/
boolean value() default true;
/**
* 需要特殊判空的字段(预留)
*
* @return {}
*/
String[] field() default {};
}
首先,在我们项目加载阶段,我们获取有 Inner 注解的类和方法,然后获取我们配置的 uri,经过正则替换后面的可变参数为*,然后将此 uri 加入到 ignore-url 中。此时我们就能达到所有 Inner 配置的方法/类上的接口地址,都统一在项目加载阶段自动帮我们加到 ignore-url 中,不需要我们手动配置,免去了很多开发工作,同时也能避免我们忘记配置,而浪费开发时间。核心代码如下:
@Slf4j
@Configuration
@ConditionalOnExpression("!'${security.oauth2.client.ignore-urls}'.isEmpty()")
@ConfigurationProperties(prefix = "security.oauth2.client")
public class PermitAllUrlProperties implements InitializingBean {
private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
@Autowired
private WebApplicationContext applicationContext;
@Getter
@Setter
private List<String> ignoreUrls = new ArrayList<>();
@Override
public void afterPropertiesSet() {
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
map.keySet().forEach(info -> {
HandlerMethod handlerMethod = map.get(info);
// 获取方法上边的注解 替代path variable 为 *
Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
Optional.ofNullable(method)
.ifPresent(inner -> info.getPatternsCondition().getPatterns()
.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, StringPool.ASTERISK))));
// 获取类上边的注解, 替代path variable 为 *
Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
Optional.ofNullable(controller)
.ifPresent(inner -> info.getPatternsCondition().getPatterns()
.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, StringPool.ASTERISK))));
});
}
}
# 统一的安全性处理
那上面讲到的,如果我们不希望这个 url 可以直接被外网调用,仅能在 Feign 服务中调用,改如何统一处理呢?
我们使用一个 Spring-AOP,在对所有 Inner 注解的方法做一个环绕增强的切点,进行统一的处理。在上面我们提到的 Inner 的 value 参数,当该参数为 true 时,我们对方法的入参进行判断,仅当符合我们定制的入参规则时(Pigx 这里是用的@RequestHeader(SecurityConstants.FROM)
与SecurityConstants.FROM_IN
做比较),我们对它进行放行,不符合时,抛出异常;当 value 为 false 时,咱不做任何处理,此时 Inner 仅起到了一个 ignore-url 的作用。
@Slf4j
@Aspect
@Component
@AllArgsConstructor
public class PigxSecurityInnerAspect {
private final HttpServletRequest request;
@SneakyThrows
@Around("@annotation(inner)")
public Object around(ProceedingJoinPoint point, Inner inner) {
String header = request.getHeader(SecurityConstants.FROM);
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
throw new AccessDeniedException("Access is denied");
}
return point.proceed();
}
}
通过这两步呢,我们首先是在加载时通过找到 Inner 注解,将相应的 uri 加入到 ignore-url 中,达到自动化配置的目的;之后我们又使用切面对 Inner 的方法进行环绕处理,达到安全控制。对比之前的处理方式,现在我们使用一个@Inner
注解,就能很快的满足上面说的两种场景,大大节省了我们的开发时间。
# ③ 合理的使用@Inner 注解
如下接口通过 Inner 接口不鉴权,Restful pathVariable 风格的请求
@Inner
@GetMapping("/info/{username}")
根据上文原理说明 ,会给 spring security 添加一条 /info/*
的 忽略拦截规则,这样会导致只要是满足此规则的路径获取不到用户信息。 (这也就是为啥 SecurityUtils.getUser 为空的原因)