一、背景介紹
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"); } }