Inner 的诞生
结合 Security 就存在以下几种url访问应用场景:
1 外部从 Gateway 访问,需要鉴权(eg.CURD 操作)。这种是最常使用的,用户登录后正常访问接口,不需要我们做什么处理(可能有的接口需要加权限字段)。
2 外部从 Gateway 访问,不需要鉴权(eg.短信验证码)。需要我们将 uri 加入到 security.oauth2.client.ignore-urls 配置中,可以不需要鉴权访问
3 内部服务间用 Feign 访问,不需要鉴权(eg.Auth 查询用户信息)。也是需要我们将 uri 加入security.oauth2.client.ignore-urls 配置中,那与第二种的区别就是这种情况下大多数都是服务可以请求另一个服务的所有数据,不受约束,那我们如果仅仅只配置 ignore-url 的话,外部所有人都可以通过 url 请求到我们内部的链接,安全达不到保障。
鉴于上述第三种情况,我们配置了 ignore-url 和 Feign,此时该接口不需要鉴权,服务内部通过 Feign 访问,服务外部通过 url 也可以访问,所以 项目中,加入了一种@RequestHeader(SecurityConstants.FROM)的处理方式。即在接口方法中,对头部进行判断,只有请求带上相应的 Header 参数时,才允许通过访问,否则抛出异常。那这时候其实我们在外网通过 Gateway 访问的时候,也可以手动带上这个 Header 参数,来达到这个目的。所以我们便在 Gateway 中设置了一个 GlobalFilter 过滤器:
这个过滤器在处理 HttpRequest 的时候,会删除从外部请求头里的 SecurityConstants.FROM 这个参数。此时的效果就是,这个 URL 从外部访问不需要鉴权,但由于 Gateway 的过滤,最终到达我们接口方法时,由于缺少头部信息,被拒绝访问;而服务间通过 Feign 访问,不经过 Gateway,则可以正常访问。
那原始的处理方法和处理逻辑就是这样:首先将 uri 加入 ingore-url,然后在接口的方法和 Feign 的接口参数中写上 RequestHeader 参数,最后在 Feign-Client 中带上这个 SecurityConstants.FROM 参数。使用 AOP 将此步骤抽离出来,成为了 Inner。
Inner 的处理流程
统一的 ignore-url 处理
首先我们来看看这个注解的代码
package com.hzjt.gczjt.common.security.annotation;
import java.lang.annotation.*;
/**
@author gczjt
@date 2019/4/13
服务调用鉴权注解
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {/**
}
首先,在我们项目加载阶段,我们获取有 Inner 注解的类和方法,然后获取我们配置的 uri,经过正则替换后面的可变参数为*,然后将此 uri 加入到 ignore-url 中。此时我们就能达到所有 Inner 配置的方法/类上的接口地址,都统一在项目加载阶段自动帮我们加到 ignore-url 中,不需要我们手动配置,免去了很多开发工作,同时也能避免我们忘记配置,而浪费开发时间。核心代码如下:
package com.hzjt.gczjt.common.security.component;
import cn.hutool.core.util.ReUtil;
import com.hzjt.gczjt.common.security.annotation.Inner;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
/**
@author gczjt
@date 2019/4/13
资源服务器对外直接暴露URL,如果设置contex-path 要特殊处理
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnExpression(“!’${security.oauth2.client.ignore-urls}’.isEmpty()”)
@ConfigurationProperties(prefix = “security.oauth2.client”)
public class PermitAllUrlProperties implements InitializingBean {private static final Pattern PATTERN = Pattern.compile(“\{(.*?)\}”);
private final WebApplicationContext applicationContext;
@Getter
@Setter
private ListignoreUrls = 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, "*")))); // 获取类上边的注解, 替代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, "*")))); });
}
}
统一的安全性处理
我们使用一个 Spring-AOP,在对所有 Inner 注解的方法做一个环绕增强的切点,进行统一的处理。在上面我们提到的 Inner 的 value 参数,当该参数为 true 时,我们对方法的入参进行判断,仅当符合我们定制的入参规则时(项目是用的@RequestHeader(SecurityConstants.FROM) 与SecurityConstants.FROM_IN做比较),我们对它进行放行,不符合时,抛出异常;当 value 为 false 时,咱不做任何处理,此时 Inner 仅起到了一个 ignore-url 的作用。
/**
@author gczjt
@date 2018/11/26
服务间接口不鉴权处理逻辑
*/
@Slf4j
@Aspect
@AllArgsConstructor
public class GczjtSecurityInnerAspect {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 注解
,下面结合 Feign 做一个简单的示例,示例场景就是我们的用户密码登录中的一环:
1在接口上使用@Inner注解,使得 url 无需鉴权
/**
* 获取指定用户全部信息
* @return 用户信息
*/
@Inner
@GetMapping("/info/{username}")
public R info(@PathVariable String username) {
SysUser user = userService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername, username));
if (user == null) {
return R.failed(null, String.format("用户信息为空 %s", username));
}
return R.ok(userService.findUserInfo(user));
}
2 编写feign接口
@FeignClient(contextId = “remoteUserService”, value = ServiceNameConstants.UPMS_SERVICE)
public interface RemoteUserService {
/**
* 通过用户名查询用户、角色信息
* @param username 用户名
* @param from 调用标志
* @return R
*/
@GetMapping("/user/info/{username}")
R<UserInfo> info(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM) String from);
3 Feign-Client 中调用接口,带上SecurityConstants.FROM_IN参数为内部识别
/**
* 用户密码登录
* @param username 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String username) {
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
if (cache != null && cache.get(username) != null) {
return (GczjtUser) cache.get(username).get();
}
R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
UserDetails userDetails = getUserDetails(result);
cache.put(username, userDetails);
return userDetails;
}
现在”/info/{username}” 这个 uri 从网关外部我们访问是报错的(一般来说服务都是走网关暴露接口),而 Feign 内部带上参数是可以正常访问的。大功告成喝杯啤酒。