API服务安全性及其解决方案

一. 引言

  Api接口暴露在外面,须要增长其安全性。目前能够了解的安全问题以下:php

(1)如何验证调用者身份?css

(2)如何防止数据被篡改,保证数据一致性?html

(3)如何加密关键数据?java

二. 项目分析

(1)如何验证调用者身份?nginx

  HTTP是无状态协议,用户经过身份验证,下回再次访问,还得验证下。web

  session验证:在服务端为用户生成一个session,session存储在服务端而且返回给用户,用户请求带session访问服务端,服务端根据session验证。算法

  token认证:spring

  1)客户端使用用户名跟密码请求登陆
  2)服务端收到请求,去验证用户名与密码
  3)验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4)客户端收到 Token 之后能够把它存储起来
  5)客户端每次向服务端请求须要受权的资源,须要带着服务端签发的Token
  6)服务端收到请求,而后去验证客户端请求里面带着的 Token,若是验证成功,就向客户端返回请求的数据数据库

  基于约约出行项目分析token认证:json

  1)applicationContext.xml配置,以PassengerTokenInterceptor为例子: 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <context:property-placeholder location="classpath:resources.properties" />
    <context:component-scan base-package="com.summersoft.ts"/>

    <!-- dubbo 配置 -->
    <dubbo:application name="ts_consumer" />
    <dubbo:registry address="${zookeeper.host}" />
    <dubbo:protocol name="dubbo" port="-1"/>
    <dubbo:consumer check="false" timeout="60000" retries="0"/>
    <dubbo:annotation package="com.summersoft.ts" />
    <dubbo:annotation package="com.summersoft.initialize" />
    <dubbo:annotation package="com.summersoft.interceptor" />

    <mvc:annotation-driven/>
    <mvc:default-servlet-handler/>

    <mvc:resources mapping="/swagger/**" location="/swagger/"/>

    <!-- 配置拦截器 -->
    <mvc:interceptors>
       <!--服务器请求拦截器-->
        <mvc:interceptor>
            <mvc:mapping path="/api/**" />
            <bean class="com.summersoft.interceptor.ApiInterceptor" />
        </mvc:interceptor>
       <!--司机端token拦截器-->
        <mvc:interceptor>
            <mvc:mapping path="/api/v1/driver/token/**" />
            <mvc:mapping path="/api/v3/driver/**" />

            <!--排除司机token校验过滤-->
            <mvc:exclude-mapping path="/api/v3/driver/loginOverseas/login.yueyue"/>
            <bean class="com.summersoft.interceptor.DriverTokenInterceptor" />
        </mvc:interceptor>
        <!--乘客端token拦截器-->
        <mvc:interceptor>
            <mvc:mapping path="/api/v1/passenger/token/**" />
            <mvc:mapping path="/api/v2/passenger/**" />
            <mvc:mapping path="/api/v3/passenger/**" />
            <mvc:mapping path="/wx/token/**" />
            <!--排除token校验过滤-->
            <mvc:exclude-mapping path="/api/v3/passenger/loginOverseas/login"/>
            <mvc:exclude-mapping path="/api/v3/passenger/order/listCarLevelOverseas"/>
            <bean class="com.summersoft.interceptor.PassengerTokenInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>

    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.ResourceHttpMessageConverter">
            </bean>
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=UTF-8</value>
                        <value>application/json;charset=UTF-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="209715200"/>
        <property name="maxInMemorySize" value="40960"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/page/" />
        <property name="suffix" value=".jsp" />
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    </bean>
</beans>

2)用户登陆的时候,服务端生成一个随机数给token(固然网上说有一些巧妙的设计,能够作一些加密设计,直接解密token,验证合法性。不过,该项目只限于key-value验证方式)。

3)PassengerTokenInterceptor核心代码,拦截器,作一些判断和操做: 

  @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        PrintWriter out = response.getWriter();
        String token = request.getHeader(Constants.TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(Constants.TOKEN_KEY);
        }
        if (StringUtils.isBlank(token)) {
            outPrint(out,createJsonObject(Constants.ERR_MSG_LOGIN_VIOLATION, Constants.ERR_CODE_NO_TOKEN, false, null));
            return false;
        }
        Map<String, String> tokenResult = passengerService.getPassengerCachedByToken(token);
        if (StringUtil.isNull(tokenResult) ) {
            outPrint(out,createJsonObject(Constants.ERR_MSG_LOGIN_OTHER, Constants.ERR_CODE_OTHER_TOKEN, false, null));
            return false;
        }
        if (StringUtil.isNull(tokenResult.get("uuid")) ) {
            outPrint(out,createJsonObject(Constants.ERR_MSG_LOGIN_OTHER, Constants.ERR_CODE_OTHER_TOKEN, false, null));
            return false;
        }
        //封号拦截
        String status = tokenResult.get("status");
        if(StringUtil.isNotNull(status)){
            Integer intStatus = Integer.parseInt(status);
            if(intStatus==Constants.DRIVER_STATUS_SHORT_CLOSE||intStatus==Constants.DRIVER_STATUS_LONG_CLOSE){
                outPrint(out,createJsonObject(Constants.ERR_MSG_LOGIN_USER_CLOSE, Constants.ERR_CODE_PASSENGER_STATUS_INVALID, false, null));
                return false;
            }
        }
        String uuid = tokenResult.get("uuid");
        request.setAttribute("uuid", uuid);
        response.reset();
        return true;
    }

4)验证token(从缓存服务获取->从数据库获取->更新缓存;都无,则返回false)

@Override
public Map<String, String> getPassengerCachedByToken(String token) {
	return updateCached(token);
}

/**
 * 更新缓存
 */
public Map<String, String> updateCached(String token) {

	//获取乘客信息
	Map<String, String> map = new HashMap<String, String>(8);
	map.put("token", token);
	List<PassengerDto> passengers = passengerMapper.list(map);
	if (passengers.size() > 0) {
		PassengerDto passenger = passengers.get(0);
		map.clear();
		map.put("uuid", passenger.getUuid());
		map.put("nickName", passenger.getNickname());
		map.put("sex", StringUtil.isNull(passenger.getSex())?"" :passenger.getSex().toString());
		map.put("mobile", passenger.getMobile());
		map.put("face", passenger.getFace());
		map.put("status",String.valueOf(passenger.getStatus()));
		//添加缓存
		cacheService.set(token, map);//将乘客基础信息放入map集合中,再放入缓存中。
		cacheService.set(passenger.getUuid(),token);
		return map;
	}
	return null;
}

  (2)如何防止数据被篡改,保证数据一致性?

  使用MD5签名与验签来校验数据的完整性。MD5全名Message-Digest Algorithm 5(信息-摘要算法)是一种不可逆的加密算法。MD5方式流程:

1)客户端根据MD5签名加密密钥(与服务端一致),拼接数据生成签名;

2)服务端接受到客户端的请求,自身根据MD5签名加密密钥,拼接数据生成签名,两个签名校验,一致则数据没被篡改,不一致则数据已被篡改。

  基于约约项目服务端分析:

1)在applicationContext.xml配置了ApiInterceptor,ApiInterceptor核心代码:

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        PrintWriter out = response.getWriter();
        if(null != request.getContentType() && -1 != request.getContentType().indexOf("json")){//请求是JSON格式数据
            String sign = request.getHeader(Constants.SIGN_KEY);
            if(StringUtils.isBlank(sign)){
                sign = request.getParameter(Constants.SIGN_KEY);
            }
            if(StringUtils.isBlank(sign)){
                outPrint(out,createJsonObject(Constants.ERR_MSG_ACCESS_VIOLATION, Constants.ERR_CODE_NO_SIGN, false, null));
                return false;
            }
            String appid = request.getHeader(Constants.APPID_KEY);
            if(StringUtils.isBlank(appid)){
                appid = request.getParameter(Constants.APPID_KEY);
            }
            if (StringUtils.isBlank(appid)) {
                outPrint(out,createJsonObject(Constants.ERR_MSG_ACCESS_VIOLATION, Constants.ERR_CODE_NO_APPID, false, null));
                return false;
            }
            String noncestr = request.getHeader(Constants.NONCESTR_KEY);
            if(StringUtils.isBlank(noncestr)){
                noncestr = request.getParameter(Constants.NONCESTR_KEY);
            }
            if (StringUtils.isBlank(noncestr)) {
                outPrint(out,createJsonObject(Constants.ERR_MSG_ACCESS_VIOLATION, Constants.ERR_CODE_NO_NONCESTR, false, null));
                return false;
            }
            Map<String, String[]> paramMap = request.getParameterMap();
            if(!paramMap.isEmpty()){
                String dd = checkAuthentication(paramMap);
                if (dd != "") {
                    outPrint(out,createJsonObject(dd, Constants.ERR_CODE_NO_ILLEGAL, false, null));
                    return false;
                }
            }else {
                String bodyStr = getBodyString(request.getReader());
                String result = checkSignByJson(sign,appid,noncestr,bodyStr);
 if (!"success".equals(result)) { outPrint(out,createJsonObject(result, Constants.ERR_CODE_NO_ILLEGAL, false, null)); return false; }
            }
        }else{
            Map<String, String[]> paramMap = request.getParameterMap();
            if (!paramMap.containsKey(Constants.SIGN_KEY)) {
                outPrint(out,createJsonObject(Constants.ERR_MSG_ACCESS_VIOLATION, Constants.ERR_CODE_NO_SIGN, false, null));
                return false;
            } else if (!paramMap.containsKey(Constants.APPID_KEY)) {
                outPrint(out,createJsonObject(Constants.ERR_MSG_ACCESS_VIOLATION, Constants.ERR_CODE_NO_APPID, false, null));
                return false;
            } else if (!paramMap.containsKey(Constants.NONCESTR_KEY)) {
                outPrint(out,createJsonObject(Constants.ERR_MSG_ACCESS_VIOLATION, Constants.ERR_CODE_NO_NONCESTR, false, null));
                return false;
            }
            String dd = checkAuthentication(paramMap);
            if (dd != "") {
                outPrint(out,createJsonObject(dd, Constants.ERR_CODE_NO_ILLEGAL, false, null));
                return false;
            }
        }
        response.reset();
        return true;
    }

2)MD5加密参数校验:

    /**
     * JSON格式请求参数加密是否一致
     *
     * @param sign
     * @return
     */
    public String checkSignByJson(String sign,String appid,String noncestr,String jsonStr) {
        JSONObject hander = new JSONObject();
        hander.put("appid",appid);
        hander.put("noncestr",noncestr);
        hander.put("key",Constants.AUTH_KEY);//签名加密密钥
        String encodeMd5 = MD5Util.MD5Encode(hander.toString() + jsonStr);
        if (sign.equals(encodeMd5)) {
            return "success";
        }
        return encodeMd5;
    }

  (3)如何加密关键数据?

  1)RSA:公开密钥加密也称为非对称密钥加密,该加密算法使用两个不一样的密钥:加密密钥和解密密钥。前者公开,又称公开密钥,简称公钥。后者保密,又称私有密钥,简称私钥。这两个密钥是数学相关的,用某用户加密密钥加密后所得的信息只能用该用户的解密密钥才能解密。那么http请求被抓起,没有对应的RSA私钥,也没法获取请求的数据,保证了传输过程当中的关键数据安全。

  基于约约项目的实现:

在web.xml配置了过滤器:

    <!-- 过滤全部对action的请求-->
    <filter>
        <filter-name>decryptFilter</filter-name>
        <filter-class>com.summersoft.filter.DecryptFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>decryptFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

核心代码:

过滤器:

  @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String isEncrypt = PropertiesLoader.getResourcesLoader().getProperty("isEncrypt");
        if(StringUtil.isEmpty(isEncrypt)||!"1".equals(isEncrypt)){
            if(null != request.getContentType() && -1 != request.getContentType().indexOf("json")) {//请求是JSON格式数据
                BodyRequestWrapper requestWrapper = new BodyRequestWrapper((HttpServletRequest)request);
                filterChain.doFilter(requestWrapper, response);
            }else {
                filterChain.doFilter(request, response);
            }
        }else {
            if(null != request.getContentType() && -1 != request.getContentType().indexOf("json")){//请求是JSON格式数据
                try {
                    BodyRequestWrapper requestWrapper = new BodyRequestWrapper((HttpServletRequest)request);
                    String bodyStr = getBodyString(requestWrapper.getReader());
                    System.out.println("解密前请求内容:" + bodyStr);
                    JSONObject jsonObject = JSONObject.fromObject(bodyStr);
                    if(jsonObject.containsKey("encryption")){
                        String urlDecryptStr = RsaUtil.decodeURL(jsonObject.getString("encryption"));//先进行url解码
                        String decryptStr =  RsaUtil.decryptByPrivate(urlDecryptStr,RsaUtil.SERVER_PRIVATE_KEY);//进行RSA解码
                        requestWrapper.setBody(decryptStr);
                     }
                    filterChain.doFilter(requestWrapper, response);
                } catch (Exception e) {
                    e.printStackTrace();
                    responseError(response);
                }
            }else {
                //这边不对上传文件接口加密
                String strUri = request.getRequestURI() ;
                System.out.println("-------------------------------strUri:"+strUri);
                if(StringUtil.isEmpty(isEncrypt)||!"1".equals(isEncrypt)
                        ||strUri.indexOf("/yue/share")!=-1||strUri.indexOf("/h5")!=-1||strUri.indexOf("/qrCodeView")!=-1
                        ||strUri.indexOf("/pay/wx/callback")!=-1||strUri.indexOf("/pay/alipay/callback")!=-1
                        ||strUri.indexOf("/img/")!=-1||strUri.indexOf("/css/")!=-1||strUri.indexOf("/share/")!=-1
                        ||strUri.indexOf("/js/")!=-1||strUri.indexOf("/admin/")!=-1||strUri.indexOf("/plugin/")!=-1
                        ||strUri.indexOf("/giftCollection")!=-1||strUri.indexOf("/driver_passenger")!=-1
                        ||strUri.indexOf("/invite/driver.yueyue")!=-1||strUri.indexOf("/token/share/info.yueyue")!=-1
                        ||strUri.indexOf("/drawQrCodeGift")!=-1
                        ||strUri.indexOf("/uploadImage.yueyue")!=-1||strUri.indexOf("/upload.yueyue")!=-1||strUri.indexOf("/api/v1/upload/image.yueyue")!=-1||strUri.indexOf("/api/v1/common/location/gpsKalmanFilter.yueyue")!=-1){
                    filterChain.doFilter(request, response);
                    return;
                }
                //获取加密的参数串
                String paramStr = request.getParameter("params");
                if(StringUtils.isNotEmpty(paramStr)){
                    try {
                        //先进行url解码
                        String urlDecryptStr = RsaUtil.decodeURL(paramStr);
                        //进行RSA解码
                        String decryptStr =  RsaUtil.decryptByPrivate(urlDecryptStr,RsaUtil.SERVER_PRIVATE_KEY);
                        //获取参数,拼接到请求中
                        ParameterRequestWrapper requestWrapper = new ParameterRequestWrapper((HttpServletRequest)request);
                        JSONObject jsonObject = JSONObject.fromObject(decryptStr);
                        for(Object key:jsonObject.keySet()){
                            requestWrapper.addParameter((String)key ,jsonObject.get(key));
                        }
                        filterChain.doFilter(requestWrapper, response);
                    }catch (Exception e){
                        e.printStackTrace();
                        responseError(response);
                    }
                }else {
                    filterChain.doFilter(request, response);
                }
            }
        }
    }

RsaUtil核心代码略解密部分。

2)https协议

  https是基于SSL基础的加密传输。使用Https加密协议访问网站,可激活客户端浏览器到网站服务器之间的"SSL加密通道"(SSL协议),实现高强度双向加密传输,防止传输数据被泄露或篡改。

  与htpp的区别:htpps须要到CA申请SSL证书,一般须要付费;htpp访问的端口80,htpps访问的端口443。

  使用:可使用nginx配置htpps

  一个例子,nginx配置

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;
    client_max_body_size    200m;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


 # HTTPS server # server { listen 443 ssl;
        server_name  app.jtgogo.cn;
        #证书
        ssl_certificate      /usr/local/certapi/lxcx.api.pem;
        #私钥
        ssl_certificate_key  /usr/local/certapi/lxcx.api.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        proxy_connect_timeout 500;
        proxy_send_timeout 500;
        proxy_read_timeout 500;
        client_max_body_size 200m;  
        location / {
           # root   html;
           # index  index.html index.htm;
       #反向代理,转发地址 proxy_pass http://172.27.0.11:7001/;
} } }
相关文章
相关标签/搜索