一、背景介绍
1.自动化的配置工具autoconfig介绍
项目开发过程中,有些配置会随着运行环境的变化而各不相同。如jdbc驱动的配置,在开发环境可能链接到开发本地的数据库,测试环境则有一套测试专用的数据库环境,如果一个应用要部署到多个idc中,那这些配置又有可能各不相同。如果每次上线时候人工的修改一下配置,比较容易出错,而且随着环境的增多成本会线性地增长。
Autoconfig提供了一种动态替换配置信息的手段。并且Maven的强大插件机制,可以和autoconfig机制结合起来,发挥巨大的威力。pom.xml中配置autoconfig方法如下:

<plugin> <groupId>com.alibaba.citrus.tool</groupId> <artifactId>autoconfig-maven-plugin</artifactId> <version>1.2</version> <configuration> <exploding>true</exploding> <includeDescriptorPatterns> <includeDescriptorPattern>autoconf/auto-config.xml</includeDescriptorPattern> <includeDescriptorPattern>autoconf/conf/auto-config.xml</includeDescriptorPattern> <includeDescriptorPattern>autoconf/conf/customize-autoconf/auto-config.xml</includeDescriptorPattern> </includeDescriptorPatterns> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>autoconfig</goal> </goals> </execution> </executions> </plugin>
Convention over Configuration(CoC),约定优于配置,上述配置文件中,充分体现了CoC原则。默认地插件会去扫描autoconf/auto-config.xml或者conf/META-INF/autoconf/auto-config.xml文件。autoconfig使用一套配置模板,为不同的环境生成相应的具体配置。它的核心思想是把一些可变的配置定义为一个模板,在autoconfig运行的时候从这些模板中生成具体的配置文件。

package test; import java.io.InputStream; import java.util.Properties; public class Testconfig { /** * 从classpath中读取配置文件component-beans.properties,然后输出到控制台 */ public static void main(String[] args) throws Exception { InputStream is = Testconfig.class.getClassLoader().getResourceAsStream("component-beans.properties"); if (is == null) { System.err.println("Can not load config resource component-beans.properties in classpath"); } else { Properties prop = new Properties(); prop.load(is); is.close(); for (String key : prop.stringPropertyNames()) { String value = prop.getProperty(key); if (value != null) { System.out.printf("%s = %s %n", key, value); } } } } }
在Testconfig.java文件中,模拟实现了从classpath中读取配置文件component-beans.properties,接下来创建antoconfig的描述文件auto-config.xml以及component-beans.properties.vm配置文件对应的模板文件(属性名中的点在autoconfig执行的时候会被替换成下划线)。
通常将配置集中管理,使用中央配置仓库,将可变内容存储在数据库中(文本,DB都可以),然后使用模版引擎(velocity,jsp等)生成最终的配置文件,并且提供http或者其他类型的接口给使用方在应用启动前调用。
2.Apache加密工具类DigestUtils.md5Hex(String str)
MD5算法是单向不可逆的,目前只可以通过暴力破解来破解。如果应用中需要采用可逆的加密方法,可以采用DES,3DES,AES,RSA 等。MD5常用于用户的登陆密码加密,每次把用户输入的密码用MD5加密后跟数据库中存储的密码进行比较。可逆加密常用于前端与后端参数交互,后端解密参数,查询数据返回给前端。
MD5加密原理是散列算法,散列算法也称哈希算法。比如10除以3余数为一,4除以3余数也为一,但余数为一的就不知道这个数是哪个了。所以md5不能解密。
二、鉴权配置和初始化
首先,在component-beans.xml.vm文件中配置了bean,并且在容器启动的时候,执行initial方法,将需要权限验证url添加到includeStr集合中。

<bean id="apiValve" class="com.alibaba.tboss.common.services.login.ApiAuthValve" init-method="initial"> <!-- 需要权限验证url --> <property name="includeStr"> <list> <value><![CDATA[^/tboss/web/api/.*.json$]]></value> </list> </property> <!-- 需要验证模块中跳过的url--> <property name="excludeStr"> <list> </list> </property> </bean>
然后在pipeline-web.xml中配置了ApiAuthValue,保证对系统进行API调用请求的时候,执行ApiAuthValue.java中的invoke回调方法。(回调函数的理解:将函数的一部分功能外包给别人。)

<?xml version="1.0" encoding="UTF-8" ?> <beans:beans> <services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves"> <!-- 初始化turbine rundata,并在pipelineContext中设置可能会用到的对象(如rundata、utils),以便valve取得。 --> <prepareForTurbine /> <!-- 设置日志系统的上下文,支持把当前请求的详情打印在日志中。 --> <setLoggingContext /> <!-- 分析URL,取得target。 --> <analyzeURL /> <valve class="com.alibaba.dragoon.patrol.webx3.DragoonStatValve" /> <!-- 配置ApiAuthValue API鉴权--> <valve class="com.alibaba.tboss.common.services.login.ApiAuthValve" /> <choose> <when> <!-- 判断当前的登录类型 --> <pl-conditions:condition class="com.alibaba.tboss.common.services.login.LoginTypeCondition" /> <valve class="com.alibaba.tboss.common.services.login.MobileAuthValve" /> </when> <otherwise> <valve class="com.alibaba.tboss.common.services.login.TbossAuthValve" /> </otherwise> </choose> <!-- 检查csrf token,防止csrf攻击和重复提交。假如request和session中的token不匹配,则出错,或显示expired页面。 --> <checkCsrfToken /> </services:pipeline> </beans:beans>
接着实现了ApiAuthValve.java中的回调方法

package com.alibaba.tboss.common.services.login; import static com.alibaba.tboss.common.auth.costants.ApiConstants.API_AUTH_NAME; import static com.alibaba.tboss.common.auth.costants.ApiConstants.API_AUTH_SIGNATURE; import static com.alibaba.tboss.common.auth.costants.ApiConstants.API_AUTH_TIEMSTAMP; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import com.alibaba.citrus.extension.rpc.impl.DefaultResultGenerator; import com.alibaba.citrus.extension.rpc.validation.DefaultErrorContext; import com.alibaba.citrus.extension.rpc.validation.ErrorContext; import com.alibaba.citrus.extension.rpc.validation.ErrorItem; import com.alibaba.citrus.service.pipeline.PipelineContext; import com.alibaba.citrus.service.pipeline.support.AbstractValve; import com.alibaba.fastjson.JSON; import com.alibaba.tboss.common.auth.bo.ApiUserBo; public class ApiAuthValve extends AbstractValve { private static Logger logger = LoggerFactory.getLogger(ApiAuthValve.class); @Resource ApiUserBo apiUserBo; @Autowired private HttpServletRequest request; @Autowired private HttpServletResponse response; // 必须定义成 protected static ,否则注入不进来 protected static String[] includeStr; protected static List<Pattern> includes = new ArrayList<Pattern>(); protected static String[] excludeStr; protected static List<Pattern> excludes = new ArrayList<Pattern>(); public void initial() { if (includeStr != null) { for (int i = 0; i < includeStr.length; i++) { includes.add(Pattern.compile(includeStr[i].toLowerCase())); } } if (excludeStr != null) { for (int i = 0; i < excludeStr.length; i++) { excludes.add(Pattern.compile(excludeStr[i].toLowerCase())); } } } @Override public void invoke(PipelineContext pipelineContext) throws Exception { String uri = request.getRequestURI(); try { // isInControl=true表示请求链接需要鉴权 if (isInControl(uri)) { // 外部调用,必须添加的三个参数:apiName、timestamp、signature String apiName = request.getParameter(API_AUTH_NAME); String timestamp = request.getParameter(API_AUTH_TIEMSTAMP); String signature = request.getParameter(API_AUTH_SIGNATURE); // 没有从api_user表匹配权限 apiUserBo.checkAuthorization(apiName, timestamp, signature, uri); } } catch (Exception e) { logger.error(uri + "fail - " + e.getMessage(), e); // 折衷方案,依赖rpc extention ErrorContext errorContext = new DefaultErrorContext(); errorContext.addError(ErrorItem.create("rpc_500", "500", String.format("API auth fail : " + e.getMessage(), request.getRequestURI()))); Object result = new DefaultResultGenerator.GenericWebRPCResult("API auth fail", null, errorContext); response.getWriter().print(JSON.toJSONString(result)); return; } pipelineContext.invokeNext(); } private boolean isInControl(String uri) { boolean control = false; for (Pattern pattern : includes) { if (pattern.matcher(uri.toLowerCase()).matches()) { control = true; break; } } if (control) { for (Pattern pattern : excludes) { if (pattern.matcher(uri.toLowerCase()).matches()) { control = false; break; } } } return control; } public void setRequest(HttpServletRequest request) { this.request = request; } public void setResponse(HttpServletResponse response) { this.response = response; } public String[] getIncludeStr() { return includeStr; } public void setIncludeStr(String[] includeStr) { this.includeStr = includeStr; } public String[] getExcludeStr() { return excludeStr; } public void setExcludeStr(String[] excludeStr) { this.excludeStr = excludeStr; } }
最后,在执行相应的业务逻辑。附鉴权方法的实现类如下:
import org.apache.commons.lang3.time.DateUtils; import com.alibaba.common.lang.MathUtil; import org.apache.commons.codec.digest.DigestUtils; public void checkAuthorization(String apiName, String timestamp, String signature, String uri) { if(StringUtils.isBlank(apiName) || StringUtils.isBlank(timestamp) || StringUtils.isBlank(signature)) { throw new ApiAuthException("apiName, timestamp, signature is missing"); } Date gmtRequest = null; try { gmtRequest = DateUtils.parseDate(timestamp, API_AUTH_TIMEFORMAT); } catch (ParseException e) { throw new ApiAuthException("Unsupported timestamp format, suggestting as " + API_AUTH_TIMEFORMAT); } if (MathUtil.abs(System.currentTimeMillis() - gmtRequest.getTime()) >= 15 * 60 * 1000) { throw new ApiAuthException("Request has been expired"); } ApiUser user = getApiUserByName(apiName); if(user == null) { throw new ApiAuthException("Api user is not exist"); } if(!isAuthorized(user, uri)) { throw new ApiAuthException(String.format("%s has no permission to access uri %s", apiName, uri)); } String realCode = decode(user.getCode()); String format = String.format("%s%s%s", apiName, timestamp, realCode); if(!signature.equals(DigestUtils.md5Hex(format))) { throw new ApiAuthException("Signature is not match"); } }
配合鉴权的表单提交测试Demo如下:首先需要md5.js和jquery-1.11.3.min.js两个js仓库。表单提交代码如下:

<!DOCTYPE html> <html lang="zh-cn"> <head> <title>测试Demo</title> <script src="jquery-1.11.3.min.js"></script> <script src="md5.js"></script> <script> function DateFormat(pattern, formatSymbols) { if(pattern == null || pattern == undefined) { pattern = "yyyy-MM-dd HH:mm:ss SSS"; } if(formatSymbols == null || formatSymbols == undefined) { formatSymbols = "yMdHmsS"; } this.pattern = pattern; this.formatSymbols = formatSymbols; } DateFormat.prototype.format = function(date) { var time = getTime(date); // 标记存入数组 var cs = this.formatSymbols.split(""); // 格式存入数组 var fs = this.pattern.split(""); // 构造数组 var ds = time.split(""); // 标志年月日的结束下标 var y = 3; var M = 6; var d = 9; var H = 12; var m = 15; var s = 18; var S = 22; // 逐位替换年月日时分秒和毫秒 for(var i = fs.length - 1; i > -1; i--) { switch (fs[i]) { case cs[0]: { fs[i] = ds[y--]; break; } case cs[1]: { fs[i] = ds[M--]; break; } case cs[2]: { fs[i] = ds[d--]; break; } case cs[3]: { fs[i] = ds[H--]; break; } case cs[4]: { fs[i] = ds[m--]; break; } case cs[5]: { fs[i] = ds[s--]; break; } case cs[6]: { fs[i] = ds[S--]; break; } } } return fs.join(""); } DateFormat.prototype.parse = function(date) { var y = ""; var M = ""; var d = ""; var H = ""; var m = ""; var s = ""; var S = ""; // 标记存入数组 var cs = this.formatSymbols.split(""); // 格式存入数组 var ds = this.pattern.split(""); // date = "2005-08-22 12:12:12 888"; // format = "yyyy-MM-dd HH:mm:ss SSS"; // sign = "yMdHmsS"; var size = Math.min(ds.length, date.length); for(var i=0; i<size; i++) { switch (ds[i]) { case cs[0]: { y += date.charAt(i); break; } case cs[1]: { M += date.charAt(i); break; } case cs[2]: { d += date.charAt(i); break; } case cs[3]: { H += date.charAt(i); break; } case cs[4]: { m += date.charAt(i); break; } case cs[5]: { s += date.charAt(i); break; } case cs[6]: { S += date.charAt(i); break; } } } if(y.length < 1) y = 0; else y = parseInt(y); if(M.length < 1) M = 0; else M = parseInt(M); if(d.length < 1) d = 0; else d = parseInt(d); if(H.length < 1) H = 0; else H = parseInt(H); if(m.length < 1) m = 0; else m = parseInt(m); if(s.length < 1) s = 0; else s = parseInt(s); if(S.length < 1) S = 0; else S = parseInt(S); var d = new Date(y, M - 1, d, H, m, s, S); return d; } // 返回当前时间 function getTime(date) { if(date == null) { date = new Date(); } var y = date.getFullYear(); var M = date.getMonth() + 1; var d = date.getDate(); var h = date.getHours(); var m = date.getMinutes(); var s = date.getSeconds(); var S = date.getTime()%1000; var html = y + "-"; if(M < 10) { html += "0"; } html += M + "-"; if(d < 10) { html += "0"; } html += d + " "; if(h < 10) { html += "0"; } html += h + ":"; if(m < 10) { html += "0"; } html += m + ":"; if(s < 10) { html += "0"; } html += s; html += " "; if(S < 100) { html += "0" } if(S < 10) { html += "0"; } html += S; return html; } </script> </head> <body> <br> <input id="url" style="100%" value=""/> <script type="text/javascript"> function getdate(){ var mydate = new Date().format("yyyyMMddhhmm"); return mydate; } function getsingname(){ //apiName 为:DiskTwistApi //code为:+r3UghilkTSfOFLfubg== //var str="test201511251000testcode"; //拼接字符串(按照`apiName+timestamp+code`顺序) var mydate = new Date().format("yyyyMMddhhmm"); var str="DiskTwistApi"+mydate+"+r3UghilkTSfOFLfubg=="; var signname=hex_md5(str); alert( signname); } function check(){ var timestamp = new DateFormat("yyyyMMddHHmm").format(new Date()); var str="DiskTwistApi"+timestamp+"+r3UghilkTSfOFLfubg=="; var signature=hex_md5(str); $("#timestamp").val(timestamp); $("#signature").val(signature); $("#forminfo").submit(); } </script> <form id="forminfo" action="http://idcm.alitest.com/tboss/web/api/workorder/diskDegauss/saveSnapshot.xhtml" method="post" enctype="multipart/form-data" > apiName:<input id="apiName" name="apiName" value="DiskTwistApi"> <br> code:<input id="code" name="code" value="+r3UghilkTSfOFLfubg=="> <br> timestamp:<input id="timestamp" name="timestamp" value=""> <br> signature<input id="signature" name="signature" value=""> <br> sn:<input id="sn" name="sn" style="500px" value="AUGUST281"/> <br> orderId:<input id="orderId" name="orderId" style="500px" value="8136"/> <br> img:<input id="img" name="fileInput" type="file"/> <br> <button type="button" onclick="check()">保存</button> </form> </body> </html>