Shiro過濾器導致的前端跨域


問題背景

公司項目是前后端分離的,最近要求在請求時都要在請求頭加入自定義的 token,在做接口調試時,前端總是請求不通,然而自己用 POSTMAN 等工具時都可以,這就出現了問題,也就是 復雜請求 的跨域問題。

問題分析

部分文段摘自 跨域資源共享 CORS 詳解

復雜請求

瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

只要同時滿足以下兩大條件,就屬於簡單請求。

  • 請求方法是以下三種方法之一:
HEAD
GET
POST
  • HTTP的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type: 只限於三個值 application/x-www-form-urlencoded multipart/form-data text/plain

這是為了兼容表單(form),因為歷史上表單一直可以發出跨域請求。AJAX 的跨域設計就是,只要表單可以發,AJAX 就可以直接發。

凡是不同時滿足上面兩個條件,就屬於非簡單請求。

預檢請求

非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。

瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。

過濾器

由於項目中的 shiro 使用了 UserFilter, 下面是其代碼:

public class UserFilter extends AccessControlFilter {
    public UserFilter() {
    }

    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (this.isLoginRequest(request, response)) {
            return true;
        } else {
            Subject subject = this.getSubject(request, response);
            return subject.getPrincipal() != null;
        }
    }

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.saveRequestAndRedirectToLogin(request, response);
        return false;
    }
}

可以看出過濾器在過濾時上面的判斷是用來判斷是否為登錄請求的,否則就去尋找登錄憑證。而在 OPTIONS 請求中,是沒有攜帶上 token 信息的,下面是當時情況下請求的 header:

=== MimeHeaders ===
host = 192.168.7.139:4000
connection = keep-alive
accept = */*
access-control-request-method = POST
access-control-request-headers = content-type,x-admin-token
origin = http://192.168.7.117:8080
sec-fetch-mode = cors
referer = http://192.168.7.117:8080/
user-agent = Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
accept-encoding = gzip, deflate
accept-language = zh-CN,zh;q=0.9

可以看到 token 是被帶在了 access-control-request-headers 中,這樣 shiro 是找不到登錄憑證的,請求自然就被拒絕了。

問題解決

解決辦法就是重寫 UserFilter(具體看項目用的是哪個過濾器) 的 isAccessAllowed 方法,代碼如下:

public class StatelessAuthcFilter extends UserFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
            httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
            httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
            httpResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

然后再去 shiro 配置中將 user 過濾器修改為自定義的過濾器:

/**
     * Shiro過濾器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // Shiro的核心安全接口,這個屬性是必須的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 身份認證失敗,則跳轉到登錄頁面的配置
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // 權限認證失敗,則跳轉到指定頁面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);

        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("user", new StatelessAuthcFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // Shiro連接約束配置,即過濾鏈的定義
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 對靜態資源設置匿名訪問
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/ajax/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        // 不需要攔截的訪問
        filterChainDefinitionMap.put("/auth/login", "anon");
        // 所有請求需要認證
        filterChainDefinitionMap.put("/**", "user");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

本以為這樣就好了,但實際上在頁面重定向后再請求接口還是有問題,這是因為重定向會會默認把請求頭清空,所以還需要將 onAccessDenied 方法重寫,完整的代碼如下:

public class StatelessAuthcFilter extends UserFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
            httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod());
            httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
            httpResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResp = WebUtils.toHttp(response);
        HttpServletRequest httpReq = WebUtils.toHttp(request);

        /*系統重定向會默認把請求頭清空,這里通過攔截器重新設置請求頭,解決跨域問題*/
        httpResp.addHeader("Access-Control-Allow-Origin", httpReq.getHeader("Origin"));
        httpResp.addHeader("Access-Control-Allow-Headers", "*");
        httpResp.addHeader("Access-Control-Allow-Methods", "*");
        httpResp.addHeader("Access-Control-Allow-Credentials", "true");

        this.saveRequestAndRedirectToLogin(request, response);
        return false;
    }

}

這樣就解決了 shiro 導致的跨域問題,如果內容對你有所幫助,可以分享給你的好友共同學習。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM