ApiAuthValue鑒權機制總結,apiauthvalue權機制
一、背景介紹
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");
}
}