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/;
} } }
相關文章
相關標籤/搜索