踩坑:SpringBoot2.0+中的攔截器會攔截靜態資源的原因及解決方案

1. 此坑的背景

        說到這個坑,來得挺機緣巧合的。當時老師帶着我們做springboot項目時,因爲涉及到了登錄權限的問題,所以,就帶着我們寫了個登錄攔截器。這一寫,導致我整個springboot項目中的靜態資源全都無法訪問。當時,看着老師的項目是能夠正常訪問的,所以,當我出現這個問題時,我並沒有考慮到是攔截器的原因。趁此機會,我就適當地去了解下SpringBoot對靜態資源的處理。說實話,這個問題折騰了我一個下午,這確實令人有點抓狂,好在這個問題最終被解決了。在此,記錄一下。

2. SpringBoot1.5.x

        SpringBoot2.0+與SpringBoot1.5.x之間還是存在許多差別的。可以看看這篇博文------Spring Boot1.5X升級到2.0指南,說不定對你有幫助。

        在SpringBoot1.5.x中,放在resources/static目錄下的靜態資源可以被直接訪問的,並且訪問路徑不需要帶上“static”。如果,你還不太清楚springboot對靜態資源的處理,可以查看這篇博文 SpringBoot對靜態資源映射的處理

        即使給此版本的springboot項目添加攔截器,請求靜態資源時,也不會被攔截。因爲我比較懶,在此處就不給出相應的例子了。(下面有)

3. SpringBoot2.0+

添加攔截器:

        要想實現登錄攔截,可以使用SpringMVC中的攔截器。類似於Servlet中的過濾器

@Component
public class UserLoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        User user = (User)request.getSession().getAttribute("user");
        if (StringUtils.isEmpty(user)) {
            response.sendRedirect("/login.html");
            return false;
        }
        return true;
    }
}

        新建一個類UserLoginInterceptor並實現了HandlerInterceptor接口(裏面的三個方法均爲jdk8中的新特性----默認方法),重寫了preHandle()方法。如果用戶已登錄,則放行;否則,重定向到登錄頁面login.html

        preHandle(): 在請求時,進入到Controller中的方法之前進行調用preHandle()方法。如果返回true,繼續訪問Controller中的方法;否則,終止請求。
        具體的內容可以查看: 處理器攔截器(HandlerInterceptor)詳解

註冊攔截器:

        僅僅寫了個登錄攔截器並通過IOC注入到Spring容器中還不行,必須通過一個配置類進行配置此攔截器,才能生效。此配置類需要實現一些規範接口。它可以實現WebMvcConfigurer接口,或者繼承WebMvcConfigurationSupport類。WebMvcConfigurerAdapter類已經被廢棄了

配置類:

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Autowired
    private UserLoginInterceptor userLoginInterceptor;


    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        // 1.註冊攔截器
        InterceptorRegistration interceptor = registry.addInterceptor(userLoginInterceptor);
        // 2. 配置攔截規則
        interceptor
                .addPathPatterns("/cart/list");
    }
}

        新建一個配置類InterceptorConfig繼承了WebMvcConfigurationSupport 類,並重寫了addInterceptors()方法。在此方法中,配置了攔截規則:"/cart/list"。它只會攔截"/cart/list"的路徑,即:
當請求路徑爲“localhost:8080/cart/list”時,會判斷用戶是否登錄。

靜態資源不能訪問,原因何在?

        進行了如上配置後,你啓動項目,再次進行訪問靜態資源,其結果是不能訪問得 到的。

        在攔截器中,我並未設置攔截靜態資源的規則。爲什麼我添加了攔截器後,就不能訪問靜態資源呢?但你若不註冊此攔截器,發現又是能訪問靜態資源的。初步判斷就是攔截器搗地蛋。

查看源碼,真相大白?

        查看源碼,究其原因。因爲spring boot 2.x依賴的spring 5.x版本,相對於spring boot 1.5.x依賴的spring 4.3.x版本而言,針對資源的攔截器初始化時有區別,具體源碼在WebMvcConfigurationSupport中。

源碼spring 4.3.x:

@Bean
public HandlerMapping resourceHandlerMapping() {
    ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
				this.servletContext, mvcContentNegotiationManager());
    addResourceHandlers(registry);

    AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
    if (handlerMapping != null) {
        handlerMapping.setPathMatcher(mvcPathMatcher());
        handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
        
        // 此處固定添加了一個Interceptor
        handlerMapping.setInterceptors(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
        handlerMapping.setCorsConfigurations(getCorsConfigurations());
		}
    else {
        handlerMapping = new EmptyHandlerMapping();
    }
    return handlerMapping;
}

spring 5.x的源碼:

@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
		@Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
		@Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
		@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
		@Qualifier("mvcConversionService") FormattingConversionService conversionService,
		@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

	Assert.state(this.applicationContext != null, "No ApplicationContext set");
	Assert.state(this.servletContext != null, "No ServletContext set");

	ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
			this.servletContext, contentNegotiationManager, urlPathHelper);
	addResourceHandlers(registry);

	AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
	if (handlerMapping == null) {
		return null;
	}
	handlerMapping.setPathMatcher(pathMatcher);
	handlerMapping.setUrlPathHelper(urlPathHelper);
   
    // 此處是將所有的HandlerInterceptor都添加了(包含自定義的HandlerInterceptor)
	handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
	handlerMapping.setCorsConfigurations(getCorsConfigurations());
	return handlerMapping;
}

protected final Object[] getInterceptors(
		FormattingConversionService mvcConversionService,
		ResourceUrlProvider mvcResourceUrlProvider) {
	if (this.interceptors == null) {
	
		// 使用構造器初始化了InterceptorRegistry對象
		InterceptorRegistry registry = new InterceptorRegistry();
		addInterceptors(registry);
		registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
		registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
		
		this.interceptors = registry.getInterceptors();
	}
	return this.interceptors.toArray();
}
  1. 在配置類中InterceptorConfig調用了addInterceptors()方法,來註冊攔截器。看看下面一個代碼語句
// 1.註冊自定義的攔截器
InterceptorRegistration interceptor = registry.addInterceptor(userLoginInterceptor);

調用了InterceptorRegistry 類中的addInterceptor()方法。

  1. 查看addInterceptor()方法
public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) {

	// 初始化了InterceptorRegistration對象,並傳入了自定義的HandlerInterceptor 對象
	InterceptorRegistration registration = new InterceptorRegistration(interceptor);
	this.registrations.add(registration);
	return registration;
}
  1. 查看InterceptorRegistration構造器
public InterceptorRegistration(HandlerInterceptor interceptor) {
	Assert.notNull(interceptor, "Interceptor is required");
	this.interceptor = interceptor;
}

把自定義的HandlerInterceptor 對象賦值給了InterceptorRegistration類中的屬性interceptor

  1. InterceptorRegistration類是InterceptorRegistry類的一個屬性
public class InterceptorRegistry {

	private final List<InterceptorRegistration> registrations = new ArrayList<>();
	...
}
  1. 在getInterceptors()方法中調用了registry.getInterceptors()
this.interceptors =  registry.getInterceptors();
  1. 查看InterceptorRegistry類中的getInterceptors()方法
protected List<Object> getInterceptors() {
	return this.registrations.stream()
			.sorted(INTERCEPTOR_ORDER_COMPARATOR)
			.map(InterceptorRegistration::getInterceptor)
			.collect(Collectors.toList());
}

        就是對InterceptorRegistry類中屬性registrations(InterceptorRegistration類型的List集合)進行遍歷。而這個集合存儲了SpringBoot中默認的攔截器和用戶的自定義攔截器

  1. 所以,getInterceptors()是返回了所有的HandlerInterceptor(包含自定義的HandlerInterceptor)

        大多博客都是這樣寫的-----從源碼分析出:使用spring 5.x時,靜態資源也會執行自定義的攔截器。但令我不解的是,自定義攔截器並沒有配置攔截靜態資源的規則,那麼靜態資源爲何不顯示呢?其實,我也不清楚。我希望以後能有機會補上這個坑。。。

解決方案

雖然,我並沒有弄清楚上面那個問題的本質,但解決方案確實找到了。

修改配置類:

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Autowired
    private UserLoginInterceptor userLoginInterceptor;


    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        // 1.註冊攔截器
        InterceptorRegistration interceptor = registry.addInterceptor(userLoginInterceptor);
        // 2. 配置攔截、非攔截規則
        interceptor
                .addPathPatterns("/cart/list")
                .excludePathPatterns("/static/**");
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
                .addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}
  • addResourceHandlers()方法:把resources/static文件夾下的靜態資源映射爲"/static/"。每次請求時,需要添加“/static”
  • addInterceptors()方法:配置了不攔截的規則,這樣,訪問靜態資源時,就能訪問了

舉例:在頁面顯示resources/static/img/a.jpg圖片。

<img src="/static/img/a.jpg" />

【參考資料】:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章