博客 / 詳情

返回

Spring Security 註冊過濾器注意事項

前兩天和小夥伴聊了 Spring Security+JWT 實現無狀態登錄,然後有小夥伴反饋了一個問題,感覺這是一個我們平時寫代碼容易忽略的問題,寫一篇文章和小夥伴們聊一聊。

一 問題復原

先來説問題吧,在 Spring Security+JWT 登錄中,整體上的思路就是用户登錄成功之後返回 JWT 字符串,然後以後用户每次請求都攜帶上 JWT 字符串,服務端進行校驗,校驗通過之後,請求繼續執行。

按照上面的思路,我們的項目中需要有一個 JwtFilter 用來從請求中提取請求傳來的 Jwt 字符串進行校驗,類似下面這樣:

@Component
public class JwtFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String requestURI = req.getRequestURI();
        if ("/login".equals(requestURI)) {
            //登錄請求,無需校驗令牌,請求繼續執行
            filterChain.doFilter(xxx,xxx);
            return;
        }
        //令牌校驗
    }
}

然後有一個小夥伴反饋,在項目中使用了 WebSecurityCustomizer 給 Swagger 相關的請求都放行了,結果這些被放行的請求都被 JwtFilter 攔截了,這是咋回事呢?

首先小夥伴們要知道,使用 WebSecurityCustomizer 放行的請求,都不再經過 SecurityFilter 了,所以按理不該再被 JwtFilter 攔截了,因為 JwtFilter 是隸屬於 SecurityFilter 這個過濾器鏈中的,並非原生的跟 Servlet 平級的那種 Filter。

但是為什麼又攔截了呢?

鬆哥看了下代碼,發現問題出在 @Component 這個註解上。

二 原理分析

在 Spring Boot 項目啓動的時候,有一個環節就是把 Spring 容器中所有類型為 Filter 的 Bean 找出來,並且自動添加到容器的過濾器鏈條中(注意不是添加到 Spring Security 過濾器鏈中)。

這段代碼的邏輯位於 ServletContextInitializerBeans#addAdaptableBeans 方法中,在該方法中,會調用 addAsRegistrationBean 方法完成以上事情:

private <T, B extends T> void addAsRegistrationBean(ListableBeanFactory beanFactory, Class<T> type,
        Class<B> beanType, RegistrationBeanAdapter<T> adapter) {
    List<Map.Entry<String, B>> entries = getOrderedBeansOfType(beanFactory, beanType, this.seen);
    for (Entry<String, B> entry : entries) {
        String beanName = entry.getKey();
        B bean = entry.getValue();
        if (this.seen.add(bean)) {
            // One that we haven't already seen
            RegistrationBean registration = adapter.createRegistrationBean(beanName, bean, entries.size());
            int order = getOrder(bean);
            registration.setOrder(order);
            this.initializers.add(type, registration);
        }
    }
}

可以看到,這裏傳入的參數 type 和 beanType 都是 Filter,從 Spring 容器中找到 Filter 類型的 Bean 存入到 initializers 集合中。不過注意,添加到集合中的實際上是封裝之後的 registration 對象,這個對象通過 adapter.createRegistrationBean 方法創建出來,在該方法中,由於我們沒有為當前過濾器設置攔截的請求地址,所以默認攔截所有請求,攔截規則是 /*

最後在 ServletWebServerApplicationContext#selfInitialize 方法中遍歷上一步找到的過濾器,並逐個進行配置,相關代碼如下:

DynamicRegistrationBean#register:

@Override
protected final void register(String description, ServletContext servletContext) {
    //註冊過濾器
    D registration = addRegistration(description, servletContext);
    //省略
}

AbstractFilterRegistrationBean#addRegistration:

@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
    Filter filter = getFilter();
    return servletContext.addFilter(getOrDeduceName(filter), filter);
}

可以看到,這最終就是大家熟知的添加過濾器的代碼了。

三 解決方案

找到問題的原因,那麼問題就好解決了。

問題的產生,主要是因為 Spring 自動查找容器中所有 Filter 類型的 Bean,並進行配置,那麼我們的解決方案就是不要把這個 Bean 註冊到 Spring 容器中,即不要添加 @Component 註解,而是直接自己 new 出來就行了,在配置過濾器鏈的時候,像下面這樣配置即可:

http.addFilterAfter(new JwtFilter(redisTemplate), SecurityContextHolderFilter.class);

經過上面這樣配置之後,JwtFilter 就不存在於原生過濾器鏈中了,只是單純的存在於 SecurityFilter 中。

理解了 Spring Security 原理,那麼日常開發中各種奇奇怪怪的情況,我們就都能輕車熟路的解決了。

如果小夥伴們想要徹底掌握 Spring Security+OAuth2,那麼可以看看鬆哥最近錄製的這套全新的視頻教程。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.