一、背景介绍
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>
pom.xml
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
在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>
apiValue定义
然后在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>
pipeline-web.xml
接着实现了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;
} }
ApiAuthValue.java
最后,在执行相应的业务逻辑。附鉴权方法的实现类如下:
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="width: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="width:500px" value="AUGUST281"/>
<br>
orderId:<input id="orderId" name="orderId" style="width:500px" value="8136"/>
<br>
img:<input id="img" name="fileInput" type="file"/>
<br>
<button type="button" onclick="check()">保存</button>
</form>
</body>
</html>