Shiro过滤器导致的前端跨域

沐晨 2020-06-06 00:45:07
原文地址:https://segmentfault.com/a/1190000022422958

问题背景

公司项目是前后端分离的,最近要求在请求时都要在请求头加入自定义的 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 导致的跨域问题,如果内容对你有所帮助,可以分享给你的好友共同学习。

> 本文由博客一文多发平台 OpenWrite 发布!

声明:该文章系转载,转载该文章的目的在于更广泛的传递信息,并不代表本网站赞同其观点,文章内容仅供参考。

本站是一个个人学习和交流平台,网站上部分文章为网站管理员和网友从相关媒体转载而来,并不用于任何商业目的,内容为作者个人观点, 并不代表本网站赞同其观点和对其真实性负责。

我们已经尽可能的对作者和来源进行了通告,但是可能由于能力有限或疏忽,导致作者和来源有误,亦可能您并不期望您的作品在我们的网站上发布。我们为这些问题向您致歉,如果您在我站上发现此类问题,请及时联系我们,我们将根据您的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。