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");
        }
    }

配合鉴权的表单提交测试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>
View Code
原文地址:https://www.cnblogs.com/RunForLove/p/5481950.html