在微服务架构下,我们通常会面临以下几种鉴权场景
- 对内对外都开放接口
- 对内对外都需要鉴权
- 只对内开放,外部请求不允许访问
场景1和场景2完全可以通过安全框架来处理,我们需要处理的是场景3,为了更好的探讨处理方案,我需要去介绍一下我现有的项目鉴权规则
介绍现有项目鉴权规则
- 通过自定义忽略鉴权注解@IgnoreToken,实现通过注解方式忽略鉴权
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreToken {
/**
* 是否需要aop拦截
*/
boolean value() default true;
}
- 自定义配置类在初始化bean的时候,通过RequestMappingHandlerMapping获取到所有方法以及类信息,将带有@IgnoreToken的方法路径以及类路径添加到ignoreUrls集合中
@Slf4j
@Configuration
public class PermitAllPorperties implements InitializingBean {
private static final Pattern PATTERN = Pattern.compile("\{.*}");
@Resource
private ApplicationContext applicationContext;
//需要忽略鉴权的路径
@Getter
@Setter
private List<String> ignoreUrls = new ArrayList<>();
/**
* 收集所有携带@IgnoreToken的方法以及类
* 1、将带有@IgnoreToken注解的方法加入到ignoreUrls集合中,需要将@Pathvariable替换成*
* 2、将带有@IgnoreToken注解的类下所有方法加入到ignoreUrls集合中,需要将@Pathvariable参数替换成*
*
* @param
*/
@Override
public void afterPropertiesSet() throws Exception {
RequestMappingHandlerMapping handlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
//获取所有方法以及类信息
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((info, method) -> {
//收集所有带有IgnoreToken注解的方法路径
IgnoreToken ignoreToken = AnnotationUtil.getAnnotation(method.getMethod(), IgnoreToken.class);
Optional.ofNullable(ignoreToken).ifPresent(ignoreToken1 -> info.getPatternsCondition().getPatterns().forEach(url -> {
ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"));
}));
//收集所有带有IgnoreToken注解的类路径
IgnoreToken controller = AnnotationUtil.getAnnotation(method.getBeanType(), IgnoreToken.class);
Optional.ofNullable(controller).ifPresent(ignoreToken1 -> info.getPatternsCondition().getPatterns().forEach(url -> {
ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"));
}));
});
}
}
- 获取permitAllPorperties中IgnoreUrls集合,将集合中所有路径都忽略鉴权
/**
* 配置拦截规则
* 1、客户端作用域必须是server
* 2、关闭csrf
* 3、将session设置成无状态模式
*
* @param httpSecurity
*/
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers(permitAllPorperties.getIgnoreUrls().stream().toArray(String[]::new)).permitAll()
.antMatchers("/**").access("#oauth2.hasScope('server')")
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
场景3设计思路
- 接口上添加IgnoreToken注解,使其不会被security所鉴权
- feign内部调用的情况下添加内部请求头标识,外部请求通过网关清洗请求头中的内部请求标识,防止外部请求模拟内部请求
- 针对IgnoreToken编写AOP切面,验证内部请求标识,不通过则抛出AccessDeniedException
代码实现
- 针对@IgnoreToken编写AOP切面类,验证当前请求来源以及内部标识
@Slf4j
@Aspect
public class SecurityIgnoreTokenAspect {
@Resource
private HttpServletRequest request;
/**
* 判断当前注解是否需要aop处理
* 获取访问header中from标识
* from_feign标识为true则通过鉴权,否则抛去权限异常
*
* @param joinPoint
* @param ignoreToken
*/
@SneakyThrows
@Around(value = "@annotation(ignoreToken)")
public Object process(ProceedingJoinPoint joinPoint, IgnoreToken ignoreToken) {
String fromOpenFeign = request.getHeader(SecurityConstants.FROM_FEIGN);
if (ignoreToken.value() && !StrUtil.equalsIgnoreCase(fromOpenFeign, SecurityConstants.FROM_FEIGN_TRUE)) {
log.error("无权限访问{}", request.getRequestURL());
throw new AccessDeniedException("无权访问");
}
return joinPoint.proceed();
}
}
- 方法上添加@IgnoreToken注解,value默认是ture,则表示需要Aop拦截,则当前方法属于场景3,如果是场景1的话 将value设置成false,Aop则不会去验证请求来源
@IgnoreToken
@GetMapping(value = "/getUserInfo/{username}")
R<SysUser> getUserInfo(@PathVariable String username) {
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
return R.ok(sysUser);
}
- 场景3的feign接口上添加头部参数
/**
* 获取用户信息
*
* @param username
*/
@GetMapping("/getUserInfo/{username}")
R<SysUser> getUserInfo(@PathVariable("username") String username,@RequestHeader(SecurityConstants.FROM_FEIGN)String fromFeign);
- 通过feign服务远程调用场景3接口时,传入内部请求头标识值
R<SysUser> sysUserR = remoteUserService.getUserInfo(username, SecurityConstants.FROM_FEIGN_TRUE);
这就大功告成了,其实这是一种很简单的处理方案,却很实用