面向sql編程的探索之路

前言

在咱們JavaWeb開發過程當中,或多或少會有些只是幾行sql語句的service方法或是http請求,老是要反覆寫Controller層、service層、dao層。因而,我作了一個大膽的嘗試,對於此類方法,封裝出一個公共方法,不須要寫java代碼,寫幾行sql語句就能出各類接口及方法。java

表設計

id description SQL creator creattime updatetime
notices 獲取通知列表 select * from notices where reciever ={userNo} admin 2018-07-06 14:07:48 2018-07-06 14:07:53

咱們選取sql

  1. id爲url請求的方法名
  2. description爲描述
  3. sql爲具體sql語句
  4. 建立人
  5. 建立時間、修改時間

初版

url請求

暫定 http://a.com/common/sqls/方法名shell

注:方法名爲表中的id。數據庫

入參不限,這裏controller層都會傳到service層,固然啦,若是你想限制,也能夠作個入參白名單列表。編程

Controller層:

@RequestMapping(value = "/sql/{id}")
    public ResultObject getRules(@PathVariable(value = "id") String id) {
        ResultObject resultObject = new ResultObject();
        Map<String, Object> params=getRequestParams();
        validateParams(params, "token");
        User user = loginService.findByToken(params.get("token").toString());
        params.put("userNo",user.getUserNo());
        List<Map<String,Object>> mapList=commonService.querySql(id,params);
        resultObject.setData(mapList);
        return resultObject;
    }
複製代碼

controller層主要是將全部變量接收轉爲一個paramsMap,而後校驗用戶token,經過token獲取用戶No。當沒有token或者token沒法獲取到用戶No時,拋異常。 獲得用戶No,將用戶No的值 put進map,最後將方法名(id)和map傳入到service層。安全

CommonService:

/**
     * 經過id找到具體sql語句
     * @param id
     * @return sql
     */
    public String getSqlById(String id) {
        return commonDao.getSqlById(id);
    }

    /**
     * 返回通用sql執行結果
     * @param id 
     * @param params
     * @return list
     */
    public List<Map<String, Object>> querySql(String id,Map<String, Object> params) {
        String sql=getSqlById(id);
        for (Map.Entry<String, Object> stringObjectEntry : params.entrySet()) {
            sql=sql.replace("{"+stringObjectEntry.getKey()+"}","'"+(String)stringObjectEntry.getValue()+"'");
        }
        return commonDao.querySql(sql);
    }
複製代碼

初版,咱們先經過遍歷map裏的全部參數替換經過方法id得到到的sql,而後執行。bash

interface:

public interface CommonDao {
    String getSqlById(String id);

    List<Map<String,Object>> querySql(String sql);
}

複製代碼

mapper:

mybatis版mybatis

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="*.basic.dao.CommonDao" >
	<select id="getSqlById" resultType="string">
		SELECT sql from m_sql WHERE id =#{value}
	</select>
	<select id="querySql" resultType="map">
		${value}
	</select>
</mapper>
複製代碼

getRequestParams:

public Map<String, Object> getRequestParams() {
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

        try {
            request.setCharacterEncoding("UTF-8");
        } catch (UnsupportedEncodingException var5) {
            var5.printStackTrace();
        }

        Map<String, Object> params = new HashMap();
        Enumeration names = request.getParameterNames();

        while(names.hasMoreElements()) {
            String name = (String)names.nextElement();
            params.put(name, request.getParameter(name));
        }

        return params;
    }
複製代碼

好了到這裏,咱們初版就出來了,咱先試試效果!app

內部調用

java代碼內部調用的時候,先實例化一個CommonService,而後調用commonService.querySql(id,params)返回便可,不管是普通調用,仍是再次封裝暴露rpc接口都沒有問題。框架

新增請求

當咱們想新增一個,獲取用戶好友的接口,咱們只需在數據庫裏增長一條

id description SQL creator creattime updatetime
friends 獲取好友列表 select * from freindss where userNo ={userNo} admin 2018-07-06 14:07:48 2018-07-06 14:07:53

調用

http://localhost:8080/*/common/sql/friends?token=cc4771aebb444d6c928a61ba5fe1153e

出參:

{"data":[{"id":"1","name":"張三"}] ,"code":200,"message":"success"}

這樣一個獲取friends的接口就行了,固然啦,實際需求sql可能很複雜,但這個不會影響咱們項目執行。

第二版

雖說初版ok了,可是顯然有個致命bug,那就是會被注入,因此,這個版本咱們要解決注入問題。

思路一

過濾入參:

public List<Map<String, Object>> querySql(String id,Map<String, Object> params) {
        String sql=getSqlById(id);
        if (params.entrySet().size()>5) {
            throw new CommonException("參數太多了,請刪除一些");
        }
        for (Map.Entry<String, Object> stringObjectEntry : params.entrySet()) {
            if (checksql((String)stringObjectEntry.getValue())) {
                throw new CommonException("不安全的請求!");
            }
            sql=sql.replace("{"+stringObjectEntry.getKey()+"}","'"+(String)stringObjectEntry.getValue()+"'");
        }

        if (checksqlSecond(sql)) {
            throw new CommonException("sql參數不合法!不能包含update、delete等");
        }
        return commonDao.querySql(sql);
    }
    private boolean checksql(String sql) {
        if (sql.length()>50) {
            return true;
        }
        if (!sql.equals(transactSQLInjection(sql))) {
            return true;
        }
        if (sqlValidate(sql)) {
            return true;
        }
        return false;
    }

    private boolean checksqlSecond(String sql) {
        String temp_sql=sql.toLowerCase();
        if (temp_sql.contains("delete")||temp_sql.contains("update")||temp_sql.contains("truncate")||temp_sql.contains("drop")) {
            return true;
        }
        return false;
    }
    private  String transactSQLInjection(String str)
    {
        return str.replaceAll(".*([';]+|(--)+).*", " ");
    }
    private static boolean sqlValidate(String str) {
        str = str.toLowerCase();
        String badStr = "'|and|exec|execute|insert|select|delete|update|count|drop|*|%|chr|mid|master|truncate|" +
                "char|declare|sitename|net user|xp_cmdshell|;|or|-|+|,|like'|and|exec|execute|insert|create|drop|" +
                "table|from|grant|use|group_concat|column_name|" +
                "information_schema.columns|table_schema|union|where|select|delete|update|order|by|count|*|" +
                "chr|mid|master|truncate|char|declare|or|;|-|--|+|,|like|//|/|%|#";//過濾掉的sql關鍵字,能夠手動添加
        String[] badStrs = badStr.split("\\|");
        for (int i = 0; i < badStrs.length; i++) {
            if (str.indexOf(badStrs[i]) >= 0) {
                return true;
            }
        }
        return false;
    }
複製代碼

咱們使用正則去過濾敏感字符,爲了防止入參過多影響咱們正則匹配,全部限定5個入參,限定每一個參數值不超過50。

這樣一作立刻就遭來各類辱罵,好水的代碼,爲何不用預編譯?那咱們接下來繼續探索

思路二

預編譯參數:

String patt = "\\{.+?}";
String querySql=sql.replaceAll(patt,"?");
Pattern r = Pattern.compile(patt);
Matcher m = r.matcher(sql);
List<String> list= new ArrayList<String>();
while(m.find()){
    list.add(m.group());
}
try {
	PreparedStatement preparedStatement = conn.prepareStatement(querySql);
	for (int i = 0; i < list.size(); i++) {
        preparedStatement.setString(i+1,params.get(list.get(i).substring(1,list.get(i).length()-1));  
    }
	preparedStatement.executeUpdate(sql_update);
}catch(Exception e){
	//e.printStackTrace();
	logger.error(e.message());
}

複製代碼

先經過正則替換將

select * from notices where reciever={userNo} and isRead={isRead}
複製代碼

替換爲

select * from notices where reciever=? and isRead=?
複製代碼

再將{userNo}、{isRead}加入list,最後遍歷list,將list的元素userNoisRead值傳入preparedStatement

思路二的方案可完美解決sql注入問題,固然還有其它方案,好比利用mybatis的sql構造器;利用其它sql預編譯框架等。

第三版:

這下咱們的安全問題也解決了,咱們來追加一些公共方法,好比出參map中包含用戶id,不包含用戶姓名,而咱們須要顯示用戶姓名。若是使用sql關聯的話,各類關聯使得sql愈來愈複雜。這裏,咱們封裝一些公共方法,好比用戶id轉name、羣組id轉groupname。

表改造

增長 入參方法、出參方法 兩個字段

id description SQL inmethod outmethod creator
notices 獲取通知列表 select * from notices where reciever =#{userNo} usertoken2id userid2name,groupid2name admin

這裏咱們支持逗號隔開方法,入參識別方法並追加到params裏,代碼以下:

String[] inMethodsplit = inMethod.split(",");
        for (String s : inMethodsplit) {
            switch (s){
                case "usertoken2id":
                    params.add("userName",usertoken2id(params.get("userid")));
                    break;
                case "xxxx":
                    params.add("xx",xxmethod(params.get("userxx")));
                    break;
            }
        }
複製代碼

switch內能夠維護本身公司的內部公用方法,來減小sql書寫量。

至於出參,我想你們都懂了,這裏就不作介紹。

總結

至此,面向sql編程的一個框架就寫好了,寫一段sql、寫幾個公共方法(可選),便可完成一個http接口 或者 普通java方法,是否是很便捷,有沒有要試一下的衝動。

本文是一種開發上的新嘗試,也是一種面向sql編程的探索。(其實,我不知道能不能稱它是面向sql編程,這裏索性大膽這樣稱呼。)若是您有什麼新的想法和思路,歡迎留言。

記得關注我哦!

相關文章
相關標籤/搜索