解决跨域请求和接口安全问题

写在前面:最近一个月因为事杂且多,没有抽出时间来整理技术点。早就想好写这方面的内容了,这周末才勉强挤出时间整理下。

步入正题,既然是解决跨域请求,那么要知道什么是跨域请求,为什么会有跨域请求。

一、什么是跨域请求:

首先引入一个概念:同源策略。

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能。为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

正是有了这个同源策略,传统的单web请求一般都是在同一个域下,那么请求就是符合同源策略的,不会产生跨域请求。相对的如果web的请求不在同一个域下,那就会产生跨域请求。

二、为什么会有跨域请求:

既然符合同源策略的请求是浏览默认支持并且相对安全的。那为什么web还要跨域请求呢。同源策略只是浏览器的默认安全功能,也并非就是一定安全的。跨域也好,不跨域也好,安全都是要解决的问题。跨域并非一定意味不安全,况且跨域还有很多好处。我们今天生活在高度信息化产品的时代,而为了让我们享受到更快,更好的信息服务,那就必须把服务精准分布,使服务具有专业性,而不是像传统的大杂烩一样。提供专业的服务需要业务的分离部署,此外为了将前后端能更好的提供服务,一般的前后端都会分离部署,分离部署也是一种对后台服务的保护。服务是分布的,前后端是分离的,那用户页面请求服务就必然会产生跨域问题,什么样的请求是跨域的,是否允许通信,具体的如下表所示:

URL

说明

是否允许通信

http://www.test.com/a.js

http://www.test.com/b.js

http://www.test.com/c

同一域名,不同文件或路径

允许

http://www.test.com/a.js

http://www.test.com:8080/b.js

同一域名,不同端口

不允许

http://www.test.com/a.js

https://www.test.com/b.js

同一域名,不同协议

不允许

http://www.test.com/a.js

http://192.168.10.26:80/a.js

域名和域名对应ip

不允许

http://www.test.com/a.js

http://x.test.com/b.js

http://test.com/c.js

主域相同,子域不同

不允许

http://www.test.com/a.js

http://www.dev.com/a.js

不同域名

不允许

 三、如何解决跨域请求问题:

从表中可以看出,跨域请求的现象是服务端和浏览器端不允许通信。当然安全问题不是跨域独有,不跨域也同样有安全问题。无论跨域与否,安全问题是一样的,都要解决,下文在单独说明。

目前前后端分离模式、服务抽象分布日益流行的情况下,跨域问题有了不少的解决方案。本文暂且说明三种方法:

1、修改请求的方式:jsonp

2、CORS设置

3、使用nginx代理

下面将对这三种方法分别进行说明和比较。

1、修改请求的方式:jsonp

该方案极不推荐,jsonp能够跨域是因为javascript的script标签,通过服务器返回script标签的code,使得该代码绕过浏览器跨域的限制。并且在客户端页面按照格式定义了回调函数,使得script标签返回实现调用。ajax实现如下。

$.ajax({

Url:’http://www.an.com:80/login’,

Type:’get’,

dataType:’jsonp’, //请求方式为jsonp

jsoonpCallback:’Back’, //自定义回调函数名

data:{}

})

 如果采用jsonp方式。第一、在编码上jsonp会单独因为回调的关系,在传入传出还有定义回调函数上都会有编码的”不整洁”感。第二、jsonp只支持get请求。第二点是jsonp的死穴,对请求的限制一般会阻止很多人选择这种方法。

2、CORS设置

同第三种nginx代理都是目前主流的解决方法。通常跨域请求浏览器端会因为Access-Control-Allow-Origin不允许而导致无法接受后台的服务返回。跨域请求也分为简单跨域和非简单跨域。

简单跨域就是GET,HEAD和POST请求,但是POST请求的"Content-Type"只能是application/x-www-form-urlencoded, multipart/form-data 或 text/plain。反之,就是非简单跨域,此跨域有一个预检机制,说直白点,就是会发两次请求,一次OPTIONS请求,一次真正的请求。

通常我们使用的是非简单跨域请求,该种请求会有一个预检机制,请求方式是option,该预检机制可以知道服务是否正常,是否提供了此服务,也可以知道鉴权是否通过。一般的不合法的请求可以大大减少,也减少了后台服务的压力。

CORS方法简单来说就是要实现服务允许被接受以及支持更多的请求方式。设置一般来说可以有两种方式实现,单本质是一样的。

1)对网关中的CORS对象进行设置,实现如下:

@Bean
public CorsFilter corsFilter() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true); // 允许cookies跨域
    config.addAllowedOrigin("*");// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
    config.addAllowedHeader("*");// #允许访问的头信息,*表示全部
    config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
    config.addAllowedMethod("OPTIONS");// 允许提交请求的方法,*表示全部允许
    config.addAllowedMethod("HEAD");
    config.addAllowedMethod("GET");// 允许Get的请求方法
    config.addAllowedMethod("PUT");
    config.addAllowedMethod("POST");
    config.addAllowedMethod("DELETE");
    config.addAllowedMethod("PATCH");
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

2)通过拦截器对请求方式和返回限制的解除设置,实现如下:

 response.setHeader("Access-Control-Allow-Origin", "*");
 response.setHeader("Access-Control-Allow-Credentials", "true");
 response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,PUT,DELETE");
 response.setHeader("Access-Control-Allow-Headers", "Authentication,Origin, X-Requested-With, Content-Type, Accept,token");

 3、nginx代理

在使用nginx代理之前,先说一下nginx。nginx是一款自由的、开源的、高性能的HTTP服务器和反向代理服务器;同时也是一个IMAP、POP3、SMTP代理服务器;nginx可以作为一个HTTP服务器进行网站的发布处理,另外nginx可以作为反向代理进行负载均衡的实现。

1)nginx反向代理

这里的代理和我们生活中的代理有点相似。就是类似与一种角色或者一个渠道。

既然有反向代理,那会不会有正向代理?答案是有的。那就先说说正向代理。生活中我们买东西一般的都是自己去买,当然也有不自己去买的,那不自己去买的人一般会找一个人---代理或者中间人。由这个人去把要买的东西买好给我们。我们无需知道代理人或者中间人是怎么去买的,而卖方也不知道是我们要买的,卖方是和代理人打交道的。这样对于卖方,就隐去的我们的信息。像这种提供服务的一方不知道真正买方的方式,一般的叫正向代理。

而反向代理一个典型的例子就是我们生活中常用的淘宝。由于有大量的用户,单服务已经无法满足巨量的请求,因此网站就需要把不同的服务进行拆分并且分布式部署,而且同一种服务也进行多份分布式部署。这样才能满足人们的需求。用户一般的只需要知道网站的地址就行,当请求向指向该地址服务器时,服务器就是一个nginx代理,会将用户信息以及请求按照既定规则分发到不同的服务,然后再将服务返回的信息转发给用户。这样请求的服务是哪一台是不明确的,但是无论是哪一台服务接到请求,该服务都会知道用户的信息的。

2)负载均衡

在上述中的反向代理中,nginx扮演了反向代理服务器的角色,它是以依据什么样的规则进行请求分发的呢?不同的项目应用场景,分发的规则是否可以控制呢?

负载均衡使用的场景是超大负载量,单服务无法满足的情况下。负载量是客户端发送的、nginx反向代理服务器接收到的请求数量。请求数量按照一定的规则进行分发到不同的服务器处理的规则,就是一种均衡规则。因此将服务器接收到的请求按照规则分发的过程,称为负载均衡。

负载均衡在实际项目操作过程中,有硬件负载均衡和软件负载均衡两种,硬件负载均衡也称为硬负载,如F5负载均衡,相对造价昂贵成本较高,但是数据的稳定性安全性等等有非常好的保障;更多的公司考虑到成本原因,会选择使用软件负载均衡,软件负载均衡是利用现有的技术结合主机硬件实现的一种消息队列分发机制。

nginx支持的负载均衡调度算法方式如下:

  1. weight轮询(默认):接收到的请求按照顺序逐一分配到不同的后端服务器,即使在使用过程中,某一台后端服务器宕机,nginx会自动将该服务器剔除出队列,请求受理情况不会受到任何影响。 这种方式下,可以给不同的后端服务器设置一个权重值(weight),用于调整不同的服务器上请求的分配率;权重数据越大,被分配到请求的几率越大;该权重值,主要是针对实际工作环境中不同的后端服务器硬件配置进行调整的。
  2. ip_hash:每个请求按照发起客户端的ip的hash结果进行匹配,这样的算法下一个固定ip地址的客户端总会访问到同一个后端服务器,这也在一定程度上解决了集群部署环境下session共享的问题。
  3. fair:智能调整调度算法,动态的根据后端服务器的请求处理到响应的时间进行均衡分配,响应时间短处理效率高的服务器分配到请求的概率高,响应时间长处理效率低的服务器分配到的请求少;结合了前两者的优点的一种调度算法。但是需要注意的是nginx默认不支持fair算法,如果要使用这种调度算法,请安装upstream_fair模块。
  4. url_hash:按照访问的url的hash结果分配请求,每个请求的url会指向后端固定的某个服务器,可以在nginx作为静态服务器的情况下提高缓存效率。同样要注意nginx默认不支持这种调度算法,要使用的话需要安装nginx的hash软件包。

上面简单介绍了nginx。那么在跨域请求中,我们如何使用nginx呢。回想一下跨域请求产生的原因--请求的协议、域名、端口号不同,浏览器和服务端不允许通信。而nginx恰恰可以使用反向代理将前端页面和后台服务代理到同一个域下,那么前后端即便分离,服务即便分布部署,利用这种“瞒天过海”或者“暗渡陈仓”的手段,让浏览器认为和服务端在同一个域下,那跨域就不存在了,也就没有了跨域问题。nginx反向代理配置如下:

server{
    listen 80;
    server_name localhost;
    #access_log c:/access.log combined;
    #index index.html index.htm index.jsp index.php;
    location / {
        root   /home/page;#文件根目录
        index  index.html index.htm;#默认起始页      
    }
    location /api { #将localhost:80域    中的api拦截进行代理转发  
            proxy_pass   http://localhost:8080/api;
    }
}

nginx监听本机的80端口,访问80端口就会在代理下转发到真正的页面index。对于80端口下的所有/api的请求路径进行拦截并转发到8080端口。这样浏览中就是在80域中操作,而实际页面和请求分别到了对应的地方。

到此跨域问题基本解决了。

四、接口安全问题

无论跨域还是不跨域,接口都有一样的安全风险。接口安全涉及的范围较广,这里我仅对数据和权限的安全进行说明。

一般的解决安全的手段有很多,如:

    1、通信使用https。

    2、请求签名,防止参数被篡改。

    3、身份确认机制,每次请求都要验证是否合法。

    4、对所有请求和响应都进行加解密操作。

    5、APP中使用ssl pinning防止抓包操作。

    6、其他安全设计方案。

 很显然,方案有很多种,没有绝对的安全,但是当项目中做的越多,也意味着安全性也更高。但安全和效率是成反比的,所以我们一般都需要按照实际情况采取最合适的方案。

如果对数据的安全性要求高,需要以下实现:

1、当有POST请求的数据发出时,前端统一加密。

2、后台将返回的数据加密处理。

3、前段统一处理数据的响应,在渲染到页面之前进行解密操作。

前端加密可以一定层度提高接口的安全性,但是前端服务本质上是不够安全的,不可靠的。如果采用AES对称加密。那么前端必然会导致加密的key泄露。针对这种情况,只能借助后端提供一种动态的加密key的方式。利用RSA非对称加密和AES对称加密结合。AES加密效率高,RSA安全性高但运行速度慢,用RSA来加密传输AES的秘钥,用AES来加密数据,两者相互结合,优势互补。如下:

   1、客户端启动,发送请求到服务端,服务端用RSA算法生成一对公钥和私钥,我们简称为pubkey1,prikey1,将公钥pubkey1返回给客户端。

    2、客户端拿到服务端返回的公钥pubkey1后,自己用RSA算法生成一对公钥和私钥,我们简称为pubkey2,prikey2,并将公钥pubkey2通过公钥pubkey1加密,加密之后传输给服务端。

    3、此时服务端收到客户端传输的密文,用私钥prikey1进行解密,因为数据是用公钥pubkey1加密的,通过解密就可以得到客户端生成的公钥pubkey2。

    4、然后后端生成对称加密,也就是我们的AES,相对于我们配置中的那个16位长度的加密key,生成了这个key之后就用公钥pubkey2进行加密,返回给客户端,因为只有客户端有pubkey2对应的私钥prikey2,只有客户端才能解密,客户端得到数据之后,用prikey2进行解密操作,得到AES的加密key,最后就用加密key进行数据传输的加密,至此整个流程结束。

以上过程和https的方式有点类似。如果对接口权限要求高,那就需要对接口进行鉴权操作。目前主流的鉴权方式往往是采用JWT的方式进行加密。流程如下:

1、用户输入正确的账户密码,登录成功,后台利用JWT工具,可以结合用户的基本信息,时间戳等动态生成一串token口令。

2、用户拿到口令后,每次请求将该口令添加到请求头Header中

3、后台对其他非授权的接口进行token口令验证。验证通过,则放行,否则拦截。

对请求的拦截一般放到filter拦截器中进行验证。token口令一般的保障了用户的合法性,但是在对于用户权限控制要求高的场景是远远不满足的。往往需要在Interceptor拦截器中进行权限的校验。在springboot项目中,该种实现方式是对WebMvcConfigurerAdapter继承并重写注册Interceptor拦截器。如下:

@Configuration
@ConditionalOnWebApplication
public class WebApplicationAutoConfig extends WebMvcConfigurerAdapter{
@Autowired
private ThreadProfileInterceptor threadProfileInterceptor;
@Bean
public UnableRepeatRequestInterceptor unableRepeatRequestInterceptor(){
    UnableRepeatRequestInterceptor unableRepeatRequestInterceptor = new UnableRepeatRequestInterceptor();
    return unableRepeatRequestInterceptor;
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
    AuthenticationInterceptor authenticationInterceptor = new AuthenticationInterceptor(properties.isAuth(), properties.isToken());
    return authenticationInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(unableRepeatRequestInterceptor());
    /**
     * 服务的interceptor,用来做慢的服务调用统计
     */
    registry.addInterceptor(threadProfileInterceptor);

    /**
     * 限流拦截器的注册
     */
    if (rateLimiterProperties.isEnabled()) {
        RateLimitInterceptor interceptor = new RateLimitInterceptor(rateLimiterProperties.getGlobalRate());
        interceptor.setLimitMappings(rateLimiterProperties.findRuleList());
        registry.addInterceptor(interceptor);
    }
   
    /**
     * AuthenticationInterceptor 拦截器注册
     */
    InterceptorRegistration authenticationRegistration = registry.addInterceptor(authenticationInterceptor());
    Arrays.stream(properties.getInterceptorPattern()).forEach(authenticationRegistration::addPathPatterns);    Arrays.stream(properties.getExcludeInterceptorPattern()).forEach(authenticationRegistration::excludePathPatterns);
}
}

 

public class AuthenticationInterceptor extends HandlerInterceptorAdapter implements InitializingBean {
private static final String HTTP_METHOD_OPTIONS = "OPTIONS";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //跨域请求,2rows
    if (HTTP_METHOD_OPTIONS.equals(request.getMethod())) {
        return true;
    } else if (!(handler instanceof HandlerMethod)) {
        return true;
    }
    executeLoginAuth(request, response, (HandlerMethod) handler);//该方法自己定义实现,根据权限和业务编写
    return true;
}
}

 以上就是MvcConfigurer和Interceptor和实现和注册。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值