在咱們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
id
爲url請求的方法名description
爲描述sql
爲具體sql語句暫定 http://a.com/common/sqls/方法名shell
注:方法名爲表中的id。數據庫
入參不限,這裏controller
層都會傳到service
層,固然啦,若是你想限制,也能夠作個入參白名單列表。編程
@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層。安全
/**
* 經過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
public interface CommonDao {
String getSqlById(String id);
List<Map<String,Object>> querySql(String sql);
}
複製代碼
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>
複製代碼
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的元素userNo
、isRead
值傳入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編程,這裏索性大膽這樣稱呼。)若是您有什麼新的想法和思路,歡迎留言。
記得關注我哦!