跨域 以及 shiro解决
同源策略:协议、域名、端口
作用:能帮助阻隔恶意文档,减少可能被攻击的媒介。
# 但也导致了跨域的问题的出现:前后端分离
# 前端访问后端,不满足同源策略,所以不能访问
1、反向代理解决
# 配置反向代理来实现跨域
# 前端访问代理
# 代理访问后端
# 代理将结果返回给前端
# 从而实现了跨域
2、CORS跨域
CORS(Cross-Origin Resource Sharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin
来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。
服务端允许CORS,服务端需要针对接口设置的一系列响应头 (Response Headers)
该字段必需。设置允许请求的域名,多个域名以逗号分隔,也可以设置成 * 即允许所有源访问
Access-Control-Allow-Origin: http://www.YOURDOMAIN.com
该字段必需。设置允许请求的方法,多个方法以逗号分隔
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
该字段可选。设置允许请求自定义的请求头字段,多个字段以逗号分隔
Access-Control-Allow-Headers: Authorization
该字段可选。设置是否允许发送 Cookies
Access-Control-Allow-Credentials: true
2.1请求分类(坑)
客户端向服务器发送请求,分为两种请求:简单请求、复杂请求
简单请求:只能请求一次
复杂请求:会先发送一次预检测请求,服务器返回204(表示预检测请求通过),才会真正发出请求,然后返回200
重点(坑):复杂请求会发送两次请求到服务器
预检测请求的作用
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是
GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的POST
请求),浏览器必须首先使用OPTIONS
方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和 HTTP 认证相关数据)。
简单请求
目前大多数情况都采用这种方式。简单请求只需要设置Access-Control-Allow-Origin
即可。满足以下两个条件,就属于简单请求。
- 请求方法:GET、POST、HEAD
- 除了以下的请求头字段之外,没有自定义的请求头
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type的值只有以下三种
(Content-Type一般是指在post请求中,get请求中设置没有实际意义)
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器
(未验证)
- XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
- 请求中没有使用 ReadableStream 对象
(未验证)
复杂请求
非简单请求即为复杂请求。复杂请求我们也可以称之为在实际进行请求之前,需要发起预检请求的请求。
非简单请求需要根据不同情况配置不同的响应头,一系列响应头配置项见上方
2.2简单请求
与复杂请求
的跨域设置
针对简单请求,在进行CORS设置的时候,我们只需要设置
Access-Control-Allow-Origin:*
// 如果只是针对某一个请求源进行设置的话,可以设置为具体的值
Access-Control-Allow-Origin: 'http://www.yourwebsite.com'
针对复杂请求,我们需要设置不同的响应头。因为在预检请求的时候会携带相应的请求头信息
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-CUSTOMER-HEADER, Content-Type
相应的响应头信息为:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
// 设置max age,浏览器端会进行缓存。没有过期之前真对同一个请求只会发送一次预检请求
Access-Control-Max-Age: 86400
如果发送的预检请求被进行了重定向,那大多数的浏览器都不支持对预检请求的重定向。我们可以通过先发送一个简单请求的方式,获取到重定向的url XHR.responseURL,然后再去请求这个url。
附带身份凭证的请求
一般而言,对于跨域 XMLHttpRequest
或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。
如果在发送请求的时候,给xhr 设置了withCredentials为true,从而向服务器发送 Cookies,如果服务端需要想客户端也发送cookie的情况,需要服务器端也返回Access-Control-Allow-Credentials: true
响应头信息。
对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin
的值为“*
”。
这是因为请求的首部中携带了Cookie
信息,如果 Access-Control-Allow-Origin
的值为“*
”,请求将会失败。而将 Access-Control-Allow-Origin
的值设置为 http://foo.example
(请求源),则请求将成功执行。
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
总结
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。
如果想实现当前页cookie的写入,用方法一:反向代理来解决
3、引入shiro权限认证导致的跨域问题
shiro使用cookie + session来进行权限认证,cookie的加入,就会使得简单请求变为复杂请求,从而导致跨域失败
解决步骤:
1、给预检请求(OPTION请求)返回204,别把它给拒绝了,设置可以可以跨域
package com.realguo.warehouse.config.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.AuthenticationFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Configuration
public class CROSUserFilter extends UserFilter {
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
setHeader(httpRequest, httpResponse);
return true;
}
return super.preHandle(request, response);
}
private void setHeader(HttpServletRequest request, HttpServletResponse response){
//跨域的header设置
response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", request.getMethod());
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
//防止乱码,适用于传输JSON数据
response.setHeader("Content-Type","application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
}
}
2、将上面这个对象放到到ShiroFilterFactoryBean
中:这样OPTION请求就不会失败
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(defaultWebSecurityManager);
// 在这里放进去
bean.getFilters().put("authc", new CROSUserFilter());
Map<String, String> filters = new LinkedHashMap<>();
filters.put("/user/login", "anon");
filters.put("/user/add", "anon");
filters.put("/index.html", "anon");
// 放行swagger
filters.put("/swagger-ui/*", "anon");
filters.put("/v2/api-docs", "anon");
filters.put("/swagger-resources/**", "anon");
filters.put("/webjars/**", "anon");
// 拦截
filters.put("/**", "authc");
bean.setFilterChainDefinitionMap(filters);
bean.setLoginUrl("/index.html");
return bean;
}
3、正常请求的跨域设置
package com.realguo.warehouse.config.shiro;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
//跨域设置,谁来都放行,与设置成*效果相同,但是这里设置成*行不通,因此用该方法代替
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
//不能设置成*,否则跨域请求会失败
response.setHeader("Access-Control-Allow-Methods", "POST,PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
//我这里需要放行这三个header头部字段
response.setHeader("Access-Control-Allow-Headers", "content-type,x-requested-with,token");
try {
filterChain.doFilter(request, response);
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
}
public void destroy() {
}
}
4、补充另外小问题
spring boot 接口接收application/x-www-form-urlencoded 的数据为null
解决方法1:
# qs.stringify()将对象 序列化成URL的形式,以&进行拼接
# qs.parse()将URL解析成对象的形式
解决方法2:
# 使用form-data格式 传输数据