spring cloud oauth 内部鉴权处理方案

1,526 阅读2分钟

在微服务架构下,我们通常会面临以下几种鉴权场景

  1. 对内对外都开放接口
  2. 对内对外都需要鉴权
  3. 只对内开放,外部请求不允许访问

场景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设计思路

  1. 接口上添加IgnoreToken注解,使其不会被security所鉴权
  2. feign内部调用的情况下添加内部请求头标识,外部请求通过网关清洗请求头中的内部请求标识,防止外部请求模拟内部请求
  3. 针对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);

这就大功告成了,其实这是一种很简单的处理方案,却很实用