首页 > 代码库 > RestFul接口的安全验证事例

RestFul接口的安全验证事例

这次要写一些restful的接口,在访问安全这一块最开始我想到用redis存储模拟session,客户端访问会带token过来模拟jsessionid,然后比对,但是这样会让token暴露在网络中,很不安全而且没有意义。

  • 其实可以用签名的方法来解决这个问题:

首先:client开通服务的时候,server会给它创建authKey和一个token,authKey相当于是公钥可以暴露在网络中,token是私钥不会暴露在网络中

然后:client请求服务端的时候在header中添加请求的时间戳,并且对发送的信息以及时间戳和token拼接起来的字符串进行签名(可以采用MD5或者SHA-0、SHA-1等)

最后:把签名串以及信息和header的信息发送到服务端,然后服务端会取出header信息以及签名串和信息,先对header里面的时间和服务器接收到的时间校验然后求差值如果大于多少秒就返回时间错误(防止重复攻击),然后在服务器端也对数据拼接签名,最后对比签名是否相等,如果不等就返回错误信息,如果相等下面就可以开始处理业务信息。

  • 刚写了份demo代码 结构如下:

技术分享

  • 工程中使用springmvc发布服务,其中controller中的代码如下:
package com.lijie.api;

import java.util.Calendar;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.lijie.utils.Signature;

/**
 * 
 * @author Lijie
 *
 */
@Controller
public class ApiAuth {

    /**
     * 测试auth
     * @param request
     * @param authKey
     * @param sign
     * @param info
     * @return
     * @throws Exception
     */
    @RequestMapping(value = http://www.mamicode.com/"/api/auth/test", method = RequestMethod.POST)
    @ResponseBody
    public String testAuth( HttpServletRequest request, String authKey, String sign,
                            String info) throws Exception {

        String reqTime = request.getHeader("ReqTime-Time");

        String token = "lijieauthtest01";

        String check = check(reqTime, info, token, sign);

        System.out.println("服务端:" + check);

        //do service

        return check;
    }

    /**
     * 校验参数和签名
     * 
     * @param reqTime
     * @param info
     * @param token
     * @param sign
     * @return
     * @throws Exception
     */
    private String check(String reqTime, String info, String token, String sign) throws Exception {

        if (StringUtils.isEmpty(reqTime)) {
            return "头部时间为空";
        }
        if (StringUtils.isEmpty(info)) {
            return "信息为空";
        }
        if (StringUtils.isEmpty(token)) {
            return "没有授权";
        }
        if (StringUtils.isEmpty(sign)) {
            return "签名为空";
        }

        long serverTime = Calendar.getInstance().getTimeInMillis();

        long clientTime = Long.parseLong(reqTime);

        long flag = serverTime - clientTime;

        if (flag < 0 || flag > 5000) {
            return "时间错误";
        }

        String allStr = info + reqTime + token;

        String md5 = Signature.md5(allStr);

        if (sign.equals(md5)) {

            System.out.println("服务端未签名时为:" + allStr);

            System.out.println("服务端签名之后为:" + md5);

            return "签名成功";
        }

        return "签名错误";
    }

}
  • 测试httpclient请求的代码如下:
package com.lijie.test;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import com.lijie.utils.Signature;

public class MyTest {

    private static final String token = "lijieauthtest01";

    @Test
    public void authTest() throws Exception {
        //创建一个httpclient对象
        CloseableHttpClient client = HttpClients.createDefault();

        //创建一个post对象
        HttpPost post = new HttpPost("http://localhost:8080/api/auth/test");

        //创建一个Entity,模拟表单数据
        List<NameValuePair> formList = new ArrayList<>();

        //添加表单数据
        long clientTime = Calendar.getInstance().getTimeInMillis();
        String info = "这是一个测试 restful api的 info";
        String reqStr = info + clientTime + token;
        formList.add(new BasicNameValuePair("authKey", "1000001"));
        formList.add(new BasicNameValuePair("info", info));
        String md5 = Signature.md5(reqStr);
        formList.add(new BasicNameValuePair("sign", md5));
        //包装成一个Entity对象
        StringEntity entity = new UrlEncodedFormEntity(formList, "utf-8");

        //设置请求的内容
        post.setEntity(entity);

        //设置请求的报文头部的编码
        post.setHeader(
            new BasicHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"));

        //设置期望服务端返回的编码
        post.setHeader(new BasicHeader("Accept", "text/plain;charset=utf-8"));

        //设置时间
        post.setHeader(new BasicHeader("ReqTime-Time", clientTime + ""));

        System.out.println("客户端未签名时为:" + reqStr);

        System.out.println("客户端签名之后为:" + md5);

        //执行post请求
        CloseableHttpResponse response = client.execute(post);

        //获取响应码
        int statusCode = response.getStatusLine().getStatusCode();

        if (statusCode == 200) {

            //获取数据
            String resStr = EntityUtils.toString(response.getEntity());

            //输出
            System.out.println("请求成功,请求返回内容为: " + resStr);
        } else {

            //输出
            System.out.println("请求失败,错误码为: " + statusCode);
        }

        //关闭response和client
        response.close();
        client.close();
    }
}
  • Signature类其实就是MD5加密的一个工具类,代码如下:
package com.lijie.utils;

import java.security.MessageDigest;

/**
 * 
 * @author Lijie
 *
 */
public class Signature {

    public static String md5(String data) throws Exception {
        // 将字符串转为字节数组
        byte[] strBytes = (data).getBytes();
        // 加密器
        MessageDigest md = MessageDigest.getInstance("MD5");
        // 执行加密
        md.update(strBytes);
        // 加密结果
        byte[] digest = md.digest();
        // 返回
        return byteArrayToHex(digest);
    }

    private static String byteArrayToHex(byte[] byteArray) {
        // 首先初始化一个字符数组,用来存放每个16进制字符

        char[] hexDigits = {    ‘0‘, ‘1‘, ‘2‘, ‘3‘, ‘4‘, ‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘, ‘A‘, ‘B‘, ‘C‘, ‘D‘,
                                ‘E‘, ‘F‘ };

        // new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方))

        char[] resultCharArray = new char[byteArray.length * 2];

        // 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去

        int index = 0;

        for (byte b : byteArray) {

            resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];

            resultCharArray[index++] = hexDigits[b & 0xf];

        }

        // 字符数组组合成字符串返回

        return new String(resultCharArray);
    }

}
  • 开启服务,测试访问,其中服务端输出:

技术分享

  • 客户端输出:

技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    RestFul接口的安全验证事例