[SpringSecurity5.6.2源码分析三]:SpringWebMvcImportSelector


theme: channing-cyan
highlight: atom-one-dark-reasonable

1、SpringWebMvcImportSelector

  • SpringSecurity支持在SpringMVC进行参数解析的时候填充参数,支持以下的对象
    • 通过@AuthenticationPrincipal,获取UserDetails
    • 通过@CurrentSecurityContext,获取SecurityContext
    • 通过参数类型为CsrfToken获取CsrfToken
  • 究其原因是因为SpringSecurity为这些参数类型注册了对应的参数解析器
  • SpringWebMvcImportSelector源码如下:
class SpringWebMvcImportSelector implements ImportSelector {

   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      boolean webmvcPresent = ClassUtils.isPresent(
            "org.springframework.web.servlet.DispatcherServlet",
            getClass().getClassLoader());
      return webmvcPresent
            ? new String[] {
                  "org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration" }
            : new String[] {};
   }
}
  • 分析可以看出当可以加载SpringMVC的DispatcherServlet的时候注册一个WebMvcSecurityConfiguration类

2、WebMvcSecurityConfiguration

  • 此类作用如下:
    • 注册四个参数解析器
      • AuthenticationPrincipalArgumentResolver:针对@AuthenticationPrincipal,注意这里是两个名称相同并且支持的注解名称也一模一样的
      • CurrentSecurityContextArgumentResolver:针对@CurrentSecurityContext
      • CsrfTokenArgumentResolver:针对CsrfToken
    • 注册CsrfRequestDataValueProcessor:当开启了Csrf的情况下,此类负责将Csrf添加到具有隐藏域的表单中
class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContextAware {

   private BeanResolver beanResolver;

   @Override
   @SuppressWarnings("deprecation")
   public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
      AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver();
      authenticationPrincipalResolver.setBeanResolver(this.beanResolver);
      argumentResolvers.add(authenticationPrincipalResolver);
      argumentResolvers
            .add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver());
      CurrentSecurityContextArgumentResolver currentSecurityContextArgumentResolver = new CurrentSecurityContextArgumentResolver();
      currentSecurityContextArgumentResolver.setBeanResolver(this.beanResolver);
      argumentResolvers.add(currentSecurityContextArgumentResolver);
      // 注册 CsrfToken 的参数解析器
      argumentResolvers.add(new CsrfTokenArgumentResolver());
   }

   @Bean
   RequestDataValueProcessor requestDataValueProcessor() {
      return new CsrfRequestDataValueProcessor();
   }

   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
      this.beanResolver = new BeanFactoryResolver(applicationContext.getAutowireCapableBeanFactory());
   }

}

2.1 AuthenticationPrincipalArgumentResolver

  • 这里仅介绍org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver,另外一个多了SpEL的解析方式
  • 可以看出当方法入参中有携带@AuthenticationPrincipal的时候,会从线程级别的安全上下文中获取认证对象
@Deprecated
public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

   @Override
   public boolean supportsParameter(MethodParameter parameter) {
      return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
   }

   @Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
      // 通过线程级别的安全上下文获得认证对象
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      if (authentication == null) {
         return null;
      }
      // 获得用户对象
      Object principal = authentication.getPrincipal();
      // 如果两者类型不匹配是否抛出异常
      if (principal != null && !parameter.getParameterType().isAssignableFrom(principal.getClass())) {
         AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
         if (authPrincipal.errorOnInvalidType()) {
            throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType());
         }
         return null;
      }
      return principal;
   }

   /**
    * 获得指定注解
    */
   private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
      T annotation = parameter.getParameterAnnotation(annotationClass);
      if (annotation != null) {
         return annotation;
      }
      Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
      for (Annotation toSearch : annotationsToSearch) {
         annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
         if (annotation != null) {
            return annotation;
         }
      }
      return null;
   }

}
  • 这里的认证对象指的是Authentication,部分实现如下:
    • UsernamePasswordAuthenticationToken:通过用户名和密码生成的认证对象
    • RememberMeAuthenticationToken:通过记住我令牌生成的认证对象

2.2 CurrentSecurityContextArgumentResolver

  • 支持解析标注了@CurrentSecurityContext注解的参数、
    • 支持Controller方法中的入参中有标注了@CurrentSecurityContext放在SecurityContext参数上
    • 支持 Spring SpEl表达式从SecurityContext中获取值
      • eg:@CurrentSecurityContext(expression=“authentication”) Authentication authentication
public final class CurrentSecurityContextArgumentResolver implements HandlerMethodArgumentResolver {

   private ExpressionParser parser = new SpelExpressionParser();

   private BeanResolver beanResolver;

   /**
    * 此参数解析器只能支持带有 {@code CurrentSecurityContext} 注解的参数
    * @param parameter
    * @return
    */
   @Override
   public boolean supportsParameter(MethodParameter parameter) {
      return findMethodAnnotation(CurrentSecurityContext.class, parameter) != null;
   }

   @Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
      // 从线程级别的策略中拿到安全上下文
      SecurityContext securityContext = SecurityContextHolder.getContext();
      if (securityContext == null) {
         return null;
      }
      Object securityContextResult = securityContext;
      // 从参数上拿到指定的 CurrentSecurityContext 注解信息
      CurrentSecurityContext annotation = findMethodAnnotation(CurrentSecurityContext.class, parameter);
      String expressionToParse = annotation.expression();
      // 是否以 SpEL 进行解析
      // SpEL 不懂
      if (StringUtils.hasLength(expressionToParse)) {
         StandardEvaluationContext context = new StandardEvaluationContext();
         context.setRootObject(securityContext);
         context.setVariable("this", securityContext);
         context.setBeanResolver(this.beanResolver);
         Expression expression = this.parser.parseExpression(expressionToParse);
         securityContextResult = expression.getValue(context);
      }
      // 如果有安全上下文,但是参数类型不对
      if (securityContextResult != null
            && !parameter.getParameterType().isAssignableFrom(securityContextResult.getClass())) {
         // 是否抛出异常,还是返回空
         if (annotation.errorOnInvalidType()) {
            throw new ClassCastException(
                  securityContextResult + " is not assignable to " + parameter.getParameterType());
         }
         return null;
      }
      return securityContextResult;
   }

   /**
    * Set the {@link BeanResolver} to be used on the expressions
    * @param beanResolver the {@link BeanResolver} to use
    */
   public void setBeanResolver(BeanResolver beanResolver) {
      Assert.notNull(beanResolver, "beanResolver cannot be null");
      this.beanResolver = beanResolver;
   }

   /**
    * 在指定的方法参数上,获得指定的注解
    * @param annotationClass the class of the {@link Annotation} to find on the
    * {@link MethodParameter}
    * @param parameter the {@link MethodParameter} to search for an {@link Annotation}
    * @return the {@link Annotation} that was found or null.
    */
   private <T extends Annotation> T findMethodAnnotation(Class<T> annotationClass, MethodParameter parameter) {
      // 拿到参数上的指定注解
      T annotation = parameter.getParameterAnnotation(annotationClass);
      if (annotation != null) {
         return annotation;
      }
      Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
      for (Annotation toSearch : annotationsToSearch) {
         annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
         if (annotation != null) {
            return annotation;
         }
      }
      return null;
   }

}

2.3 CsrfTokenArgumentResolver

  • 源码很简单就是直接从请求域中获得CsrfToken
public final class CsrfTokenArgumentResolver implements HandlerMethodArgumentResolver {

   /**
    * 此参数解析器仅支持 {@code CsrfToken}
    * @param parameter
    * @return
    */
   @Override
   public boolean supportsParameter(MethodParameter parameter) {
      return CsrfToken.class.equals(parameter.getParameterType());
   }

   @Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
      // 从请求域中获得CsrfToken, 此属性值是由CsrfFilter负责放入的
      CsrfToken token = (CsrfToken) webRequest.getAttribute(CsrfToken.class.getName(),
            RequestAttributes.SCOPE_REQUEST);
      return token;
   }

}
  • 至于为什么在请求域中有CsrfToken,下面的代码能看出是在SpringSecurity的CsrfFilter中负责将CsrfToken放到请求域中的
public final class CsrfFilter extends OncePerRequestFilter {
  ......
  @Override
  protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain)
              throws ServletException, IOException {
     .......
     request.setAttribute(CsrfToken.class.getName(), csrfToken);
     ......
  }
.......
}
  • 又衍生出一个问题,到底是setAttribute还是getAttribute先执行呢
  • 下图能够看出应用程序中一共有五个过滤器,前三个是SpringMVC提供的,第四个就是FilterChainProxy也就是SpringSecurity的过滤器链,CsrfFilter就是在这里面执行的,而参数解析器是在DispatcherServlet中负责执行的,而DispatcherServlet最终是这里的第五个过滤器中负责调用的
  • 所以说一定是先是setAttribute还再getAttribute
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值