签名验证

RSA签名工具类

 public static void getM() throws Exception {

        RSAPublicKey loadPublicKeyByStr = RSAEncrypt.loadPublicKeyByStr(
                "MIGfQAB");
        RSAPrivateKey loadPrivateKeyByStr;
        loadPrivateKeyByStr = RSAEncrypt.loadPrivateKeyByStr(
                "MIIC");
        
        String mobile = "15858236349";
        String encrypt = RSAEncrypt.encrypt(loadPublicKeyByStr, mobile, "UTF-8");
        System.out.println(encrypt);
        String mobiles = RSAEncrypt.decrypt(loadPrivateKeyByStr, encrypt, "UTF-8");
        System.out.println(mobiles);
        
        TreeMap<String, String> paramMap = new TreeMap<>();
        paramMap.put("mobile_tel", URLDecoder.decode(encrypt, "UTF-8"));
        paramMap.put("timestamp", "1559636223216");
        paramMap.put("partner_id", "20190604");
        String createLinkString = createLinkString(paramMap);
        System.out.println("createLinkString: " + createLinkString);
        String sign = RSASignature.sign(createLinkString, loadPrivateKeyByStr, "UTF-8");
        boolean doCheck = RSASignature.doCheck(createLinkString(paramMap), sign, loadPublicKeyByStr);
        System.out.println("doCheck: " + doCheck);
    }

    public static void threadLogin() throws Exception {
        String mobile = "18806513872";
        String ciphertext = RSAEncrypt.encrypt(RSAEncrypt.loadPublicKeyByStr(partnerPublicKey), mobile, "UTF-8");
        System.out.println("手机号加密: " + ciphertext);
        String phone = RSAEncrypt.decrypt(RSAEncrypt.loadPrivateKeyByStr(privateKey), ciphertext, "UTF-8");
        System.out.println("手机号解密: " + phone);

        TreeMap<String, String> paramMap = new TreeMap<>();
        paramMap.put("partner_id", "20190634");
        paramMap.put("timestamp", DateTime.now().toString("yyyyMMddHHmmss"));
        paramMap.put("mobile_tel", ciphertext);
        paramMap.put("car_num", "浙A641MY");
        // 拼接
        String urlParams = RSAEncrypt.createLinkString(paramMap);
        System.out.println("代签名字符串: " + urlParams);
        // 平台私钥获取
        RSAPrivateKey loadPrivateKeyByStr = RSAEncrypt.loadPrivateKeyByStr(privateKey);

        // 平台私钥签名
        String sign = RSASignature.sign(urlParams, loadPrivateKeyByStr,
                "UTF-8");

        boolean doCheck = RSASignature.doCheck(urlParams, sign, RSAEncrypt.loadPublicKeyByStr(partnerPublicKey));
        LogKit.info("验签状态: " + doCheck);

        paramMap.put("sign", sign);
        urlParams = RSAEncrypt.createLinkStringTo(paramMap);

        System.out.println("签名后: " + urlParams);

//        String url = "http://127.0.0.1:9094/user/thirdLogin?" + urlParams;
        String url = "http://192.168.2.92:4009?" + urlParams;
        System.out.println("url: " + url);

        String res = HttpUtil.get(url);
        LogKit.info(res);
        System.out.println("========结束=======");

    }

pc

package cn.inpark.consumer.app.io;

import cn.hutool.core.date.DateTime;
import cn.hutool.http.HttpUtil;
import cn.inpark.core.sign.RSAEncrypt;
import cn.inpark.core.sign.RSASignature;
import com.alibaba.fastjson.JSON;
import com.jfinal.kit.LogKit;

import java.net.URLEncoder;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.TreeMap;

/**
 * @author Wgs (ˇˍˇ)
 * @version 1.0
 * @create:2020/05/22
 */
public class IoPaySuccess {
    private static String privateKey = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANb90wVhDLg5RONfbjHK7mUyVcD6aEH6lrF0PNyhKc9AOaKdkCPT8YEyKcJFSHCD3zjr6N/zVyV7d+Haycfmf7NIR+eLP5hUYh5JfCcbgF5SOFZGU9y7hI5r1BXMaLisTTvc0IHLutIuiw/e7XgkNZsmZG2C3mCKXGjvWFZ2GyDzAgMBAAECgYAETdCLqwW2Km9WnfJ2zLk5DWjqFBlCOOILa2hMgm1z2LVOrBjWnseW7ooneXuZxQoS+aYP1a9I8M/Vlau0dMjZpJNuD0FNHFXS22jSsYsJ9ZpDz5yBrox9iQRjfNBzgDcXU7PazDENAPKTsg2GWVcxYrYzWm+QANwcArS6M1S4AQJBAPBW/27w05OeTibALrKPClHF2yVte17lBOx0OUNyYHhsPhSwDQqE/qcXgzq3yJ3i4z/c9hymntIiU4O2DOY7GOECQQDlAACP8dGztFxiBCU3bxVwA2A50EY6o2xTSrCXzbKb9V+T/mhCBTPrb4V+ghhU2f3F1FTvA8TCWenxu93X5BBTAkEAkKwt0oBRO/j9HkJtWbrVAWhShHMk4niFwDguCDWfcJ3nuNhI4V4xVC/JJN1ge/oJxXWjCJgb3m4fS5eYqDZGwQJBAJsjsD3UlrvHEds5cbknYX+cWeFw71Mkieu3qwF80mcECfdqIFmxyqXSzL6TBkbd1CXlHpE6nV8JqQsiuj1bfY8CQQCbm0d3GguFQM1iY5J0aaN/kvi+CMoArZNfdpdbB4NhR2d6C1jl14K+SDrcES6df+g/Vm7k9vJYnIZAnegamXqt";
    // 商户号
    private static String partnerPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDW/dMFYQy4OUTjX24xyu5lMlXA+mhB+paxdDzcoSnPQDminZAj0/GBMinCRUhwg9846+jf81cle3fh2snH5n+zSEfniz+YVGIeSXwnG4BeUjhWRlPcu4SOa9QVzGi4rE073NCBy7rSLosP3u14JDWbJmRtgt5gilxo71hWdhsg8wIDAQAB";
    private static String localhost_url = "127.0.0.1:9095/io/paySuccess";

    public static void main(String[] args) throws Exception {
        ioPay();
    }

    private static void ioPay() throws Exception {

        RSAPublicKey loadPublicKeyByStr = RSAEncrypt.loadPublicKeyByStr(partnerPublicKey);
        RSAPrivateKey loadPrivateKeyByStr = RSAEncrypt.loadPrivateKeyByStr(privateKey);
        TreeMap<String, String> paramMap = initParam();
        paramMap.put("parkId", "1");
        paramMap.put("carNo", "浙F12347");
        paramMap.put("ioType", "2");
        paramMap.put("ioTime", "2020-05-25 16:00:04");
        paramMap.put("passCode", "20181010162830");
        paramMap.put("passName", "南一");
        paramMap.put("recordId", "");
        paramMap.put("outTradeNo", "2020052122001428411445935438");
        paramMap.put("payType", "免密支付");
        paramMap.put("payCharge", "15.00");
        paramMap.put("realCharge", "15.00");

        String urlParams = RSAEncrypt.createLinkStringFrom(paramMap);


        String sign = RSASignature.sign(urlParams, loadPrivateKeyByStr, "UTF-8");
        paramMap.put("sign", sign);

        boolean doCheck = RSASignature.doCheck(urlParams, sign, loadPublicKeyByStr);
        LogKit.info("doCheck: " + doCheck);

        String url = localhost_url;
        // 通过表单提交需要sign Encode ; json提交不要sign不需要encode
        String param = RSAEncrypt.createLinkStringFrom(paramMap) +"&sign="+ URLEncoder.encode(sign.toString(), "utf-8");

        String realUrl = url +"?" +param;
        System.out.println(url + "?" + param);
        System.out.println("json: " + JSON.toJSONString(paramMap));
        String res = HttpUtil.get(realUrl);
        LogKit.info(res);
        System.out.println("========结束=======");

     // ============== json ===============

        String urlParams = RSAEncrypt.createLinkStringFrom(paramMap);
        String sign = RSASignature.sign(urlParams, loadPrivateKeyByStr, "UTF-8");
        paramMap.put("sign", sign);
        boolean doCheck = RSASignature.doCheck(urlParams, sign, loadPublicKeyByStr);
        LogKit.info("doCheck: " + doCheck);

        String url = localhost_url;
        String param = RSAEncrypt.createLinkStringFrom(paramMap);
        String res = HttpUtil.post(url ,JSON.toJSONString(paramMap));
        LogKit.info(res);
    }

    private static TreeMap<String, String> initParam() {
        TreeMap<String, String> paramMap = new TreeMap<>();
        paramMap.put("timestamp", DateTime.now().toString("yyyyMMddHHmmss"));
        paramMap.put("partnerId", "20200402");
        return paramMap;
    }

}

签名验证

签名验证涉及到客户端(比如一个 Web 应用)和服务器端,每个客户端在服务器上有一个对应的 app_id 和 app_key,大致步骤如下:
  1. 客户端使用 app_id + app_key + 其他参数生成签名字符串 sign

  2. 把 app_id、其他参数 和 sign 一起发送给服务器(app_key 不发送)

  3. 服务器接收到请求后,根据参数中的 app_id 查找到对应的 app_key,然后根据签名算法生成签名字符串 sign2


客户端和服务器端使用同样的签名算法生成签名字符串。
  1. 字符串比较参数中的 sign 和服务器生成的 sign2,如果相等则签名没问题,放行访问,否则签名无效,拒绝访问
签名算法
  1. 设要参与计算签名的数据为集合 M,将集合 M 内非空参数值的参数按照参数名 ASCII 码从小到大排序(字典序),使用 URL 键值对的格式(即key1=value1&key2=value2…)拼接成字符串 signTemp。
特别注意以下重要规则:

参数名 ASCII 码从小到大排序(字典序)
如果参数的值为空不参与签名
参数名区分大小写
验证调用返回或主动通知签名时,传送的 sign 参数不参与签名,将生成的签名与该 sign 值作校验
接口可能增加字段,验证签名时必须支持增加的扩展字段
  1. 对 signTemp 进行 MD5 运算,再将得到的字符串所有字符转换为大写,得到 sign 值 signValue。
例子
假设传送的参数如下:
app_id: 015B512C873648578FB2C32BD5677BD4
username: alice
productId: 1001
signedTime: 1499914521231
并且 app_key 为 927170905ECA42FC9813DD7EED21A5AF


第一步:对参数按照 key=value 的格式,并按照参数名 ASCII 字典序排序:
    
signTemp = "app_id=015B512C873648578FB2C32BD5677BD4&app_key=927170905ECA42FC9813DD7EED21A5AF&productId=1001&signedTime=1499914521231&username=alice";

第二步:计算 signTemp 的签名字符串:
sign = md5(signTemp).toUpperCase(); // 281879C9007C3698D1106F9CF6A097A3

第三步:发送参数(没有 app_key)
app_id=015B512C873648578FB2C32BD5677BD4&productId=1001&signedTime=1499914521231&username=alice&sign=33A62BBCEF9D4AF675ADC6BAEA468B99


Java 实现

import org.springframework.util.DigestUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class Sign {
    public static void main(String[] args) throws IOException {
        String appId = "015B512C873648578FB2C32BD5677BD4";
        String appKey = "927170905ECA42FC9813DD7EED21A5AF";
        // 参与签名的参数
        Map<String, String> params = new HashMap<>();
        params.put("app_id", appId);
        params.put("app_key", appKey);
        params.put("username", "alice");
        params.put("productId", "1001");
        params.put("signedTime", "1499914521231");
        String signValue = sign(params);
        System.out.println(signValue); // 281879C9007C3698D1106F9CF6A097A3
    }
    public static String sign(Map<String, String> params) {
        Map<String, String> temp = new TreeMap<>(params); // 对参数进行排序
        // 拼接参数
        StringBuilder sb = new StringBuilder();
        for (String key : temp.keySet()) {
            sb.append(key).append("=").append(temp.get(key)).append("&");
        }
        String signTemp = sb.deleteCharAt(sb.length() - 1).toString(); // 去掉最后一个 &
        String signValue = DigestUtils.md5DigestAsHex(signTemp.getBytes()).toUpperCase(); // 使用 MD5 计算签名字符串
        return signValue;
    }
}

签名有效期

如果不限制签名的使用时间,则生成的签名永远有效,如果别人拿到生成的链接后就能一直使用了,这是比较危险的,所以限制签名时长很有必要,有 2 种方式可以限制签名的有效时间:
  1. 客户端加签名生成时间:
    1. 生成签名时的时间加入计算签名
如果客户端的时间或则服务器端的时间不准,就有可能签名会无效,例如客户端的时间是 2016 年的,而服务器端的是 2017 年的,签名的过期时长为 5 分钟,则计算的签名就会是无效的。反过来签名的有效时间就变长了。
还有个办法,使用服务器时间进行签名:在计算签名前先向服务器请求一下服务器的时间,然后用此时间进行签名即可。服务器接收到请求时判断一下这个时间一定要比服务器当前时间早才行。
  1. 服务器记录签名时间:

    1. 客户端计算签名(不使用时间)
    1. 服务器收到请求后:
  • 如果签名有效且是第一次使用,则保存签名和时间 T1 到数据库或则 Redis 等,放行访问
  • 如果签名不是第一次访问(能从数据库或则 Redis 中查找到),并且 服务器当前时间 <= T1 + 过期时长 则放行访问,否则拒绝访问
优点:此种方式是最安全的。

缺点:服务器端需要一直保存签名,否则删除后再次使用时就变成第一次使用了,不过 Redis 之类的存储几亿条记录访问还是很快的。

代码实例

package com.demo.type;


import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SignTest {


    public static void main(String[] args) throws Exception {
        Map<String, String[]> params = new HashMap<>();

        params.put("name", new String[]{"tom"});
        params.put("type", new String[]{"3"});
        params.put("age", new String[]{"12"});
        params.put("a", new String[]{"a"});
        System.out.println(createLinkString(params));
        // a="a"&age="12"&name="tom"&type="3"
        System.out.println("------------加密--------------");
        String privvateKey = "purp3489u&*&";
        System.out.println(createSign(createLinkString(params),privvateKey));
        // 06cd7206aa20010f1b67f4dc17ec79c4

        // 判断请求时间有效性
        Long nowTimePoint = System.currentTimeMillis();
        if (nowTimePoint - Long.valueOf(timePoint) > 15 * 60) {
            
        }

    }


    public static String createSign(String params, String privateKey) throws UnsupportedEncodingException {

        return MD5Util.MD5Encode(params + privateKey, "utf-8");
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkString(Map<String, String[]> params) throws Exception {

        Map<String, String> paramsMap = new HashMap<>();
        for (String name : params.keySet()) {
            String[] value = params.get(name);
            for (int i = 0; i < value.length; i++) {
                paramsMap.put(name, value[i]);
            }
        }

        List<String> keys = new ArrayList<String>(paramsMap.keySet());
        Collections.sort(keys);

        String prestr = "";

        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = paramsMap.get(key);

            if (!"sign".equals(key)) {
                if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                    prestr = prestr + key + "=\"" + URLEncoder.encode(value, "utf-8") + "\"";
                } else {
                    prestr = prestr + key + "=\"" + URLEncoder.encode(value, "utf-8") + "\"" + "&";
                }
            }
        }

        return prestr;
    }

}

工具类


package com.demo.type;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SignUtils {

    public static String createSign(String params, String privateKey) throws UnsupportedEncodingException {

        return MD5Util.MD5Encode(params + privateKey, "utf-8");
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     * 
     * @param params
     *            需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkString(Map<String, String[]> params) throws Exception {

        Map<String, String> paramsMap = new HashMap<>();
        for (String name : params.keySet()) {
            String[] value = params.get(name);
            for (int i = 0; i < value.length; i++) {
                paramsMap.put(name, value[i]);
            }
        }

        List<String> keys = new ArrayList<String>(paramsMap.keySet());
        Collections.sort(keys);

        String prestr = "";

        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = paramsMap.get(key);

            if (!"sign".equals(key)) {
                if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
                    prestr = prestr + key + "=\"" + URLEncoder.encode(value, "utf-8") + "\"";
                } else {
                    prestr = prestr + key + "=\"" + URLEncoder.encode(value, "utf-8") + "\"" + "&";
                }
            }
        }

        return prestr;
    }   
}

三个类

package cn.inpark.common.util.sign;

public final class Base64 {

    static private final int BASELENGTH = 128;
    static private final int LOOKUPLENGTH = 64;
    static private final int TWENTYFOURBITGROUP = 24;
    static private final int EIGHTBIT = 8;
    static private final int SIXTEENBIT = 16;
    static private final int FOURBYTE = 4;
    static private final int SIGN = -128;
    static private final char PAD = '=';
    static private final boolean fDebug = false;
    static final private byte[] base64Alphabet = new byte[BASELENGTH];
    static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];

    static {
        for (int i = 0; i < BASELENGTH; ++i) {
            base64Alphabet[i] = -1;
        }
        for (int i = 'Z'; i >= 'A'; i--) {
            base64Alphabet[i] = (byte) (i - 'A');
        }
        for (int i = 'z'; i >= 'a'; i--) {
            base64Alphabet[i] = (byte) (i - 'a' + 26);
        }

        for (int i = '9'; i >= '0'; i--) {
            base64Alphabet[i] = (byte) (i - '0' + 52);
        }

        base64Alphabet['+'] = 62;
        base64Alphabet['/'] = 63;

        for (int i = 0; i <= 25; i++) {
            lookUpBase64Alphabet[i] = (char) ('A' + i);
        }

        for (int i = 26, j = 0; i <= 51; i++, j++) {
            lookUpBase64Alphabet[i] = (char) ('a' + j);
        }

        for (int i = 52, j = 0; i <= 61; i++, j++) {
            lookUpBase64Alphabet[i] = (char) ('0' + j);
        }
        lookUpBase64Alphabet[62] = '+';
        lookUpBase64Alphabet[63] = '/';

    }

    private static boolean isWhiteSpace(char octect) {
        return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
    }

    private static boolean isPad(char octect) {
        return (octect == PAD);
    }

    private static boolean isData(char octect) {
        return (octect < BASELENGTH && base64Alphabet[octect] != -1);
    }

    /**
     * Encodes hex octects into Base64
     *
     * @param binaryData Array containing binaryData
     * @return Encoded Base64 array
     */
    public static String encode(byte[] binaryData) {

        if (binaryData == null) {
            return null;
        }

        int lengthDataBits = binaryData.length * EIGHTBIT;
        if (lengthDataBits == 0) {
            return "";
        }

        int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
        int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
        int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
        char encodedData[] = null;

        encodedData = new char[numberQuartet * 4];

        byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;

        int encodedIndex = 0;
        int dataIndex = 0;
        if (fDebug) {
            System.out.println("number of triplets = " + numberTriplets);
        }

        for (int i = 0; i < numberTriplets; i++) {
            b1 = binaryData[dataIndex++];
            b2 = binaryData[dataIndex++];
            b3 = binaryData[dataIndex++];

            if (fDebug) {
                System.out.println("b1= " + b1 + ", b2= " + b2 + ", b3= " + b3);
            }

            l = (byte) (b2 & 0x0f);
            k = (byte) (b1 & 0x03);

            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
            byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);

            if (fDebug) {
                System.out.println("val2 = " + val2);
                System.out.println("k4   = " + (k << 4));
                System.out.println("vak  = " + (val2 | (k << 4)));
            }

            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
        }

        // form integral number of 6-bit groups
        if (fewerThan24bits == EIGHTBIT) {
            b1 = binaryData[dataIndex];
            k = (byte) (b1 & 0x03);
            if (fDebug) {
                System.out.println("b1=" + b1);
                System.out.println("b1<<2 = " + (b1 >> 2));
            }
            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
            encodedData[encodedIndex++] = PAD;
            encodedData[encodedIndex++] = PAD;
        } else if (fewerThan24bits == SIXTEENBIT) {
            b1 = binaryData[dataIndex];
            b2 = binaryData[dataIndex + 1];
            l = (byte) (b2 & 0x0f);
            k = (byte) (b1 & 0x03);

            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);

            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
            encodedData[encodedIndex++] = PAD;
        }

        return new String(encodedData);
    }

    /**
     * Decodes Base64 data into octects
     *
     * @param encoded string containing Base64 data
     * @return Array containind decoded data.
     */
    public static byte[] decode(String encoded) {

        if (encoded == null) {
            return null;
        }

        char[] base64Data = encoded.toCharArray();
        // remove white spaces
        int len = removeWhiteSpace(base64Data);

        if (len % FOURBYTE != 0) {
            return null;// should be divisible by four
        }

        int numberQuadruple = (len / FOURBYTE);

        if (numberQuadruple == 0) {
            return new byte[0];
        }

        byte decodedData[] = null;
        byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
        char d1 = 0, d2 = 0, d3 = 0, d4 = 0;

        int i = 0;
        int encodedIndex = 0;
        int dataIndex = 0;
        decodedData = new byte[(numberQuadruple) * 3];

        for (; i < numberQuadruple - 1; i++) {

            if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
                    || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) {
                return null;
            } // if found "no data" just return null

            b1 = base64Alphabet[d1];
            b2 = base64Alphabet[d2];
            b3 = base64Alphabet[d3];
            b4 = base64Alphabet[d4];

            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
        }

        if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) {
            return null;// if found "no data" just return null
        }

        b1 = base64Alphabet[d1];
        b2 = base64Alphabet[d2];

        d3 = base64Data[dataIndex++];
        d4 = base64Data[dataIndex++];
        if (!isData((d3)) || !isData((d4))) {// Check if they are PAD characters
            if (isPad(d3) && isPad(d4)) {
                if ((b2 & 0xf) != 0)// last 4 bits should be zero
                {
                    return null;
                }
                byte[] tmp = new byte[i * 3 + 1];
                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
                tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
                return tmp;
            } else if (!isPad(d3) && isPad(d4)) {
                b3 = base64Alphabet[d3];
                if ((b3 & 0x3) != 0)// last 2 bits should be zero
                {
                    return null;
                }
                byte[] tmp = new byte[i * 3 + 2];
                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
                tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
                tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
                return tmp;
            } else {
                return null;
            }
        } else { // No PAD e.g 3cQl
            b3 = base64Alphabet[d3];
            b4 = base64Alphabet[d4];
            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);

        }

        return decodedData;
    }

    /**
     * remove WhiteSpace from MIME containing encoded Base64 data.
     *
     * @param data the byte array of base64 data (with WS)
     * @return the new length
     */
    private static int removeWhiteSpace(char[] data) {
        if (data == null) {
            return 0;
        }

        // count characters that's not whitespace
        int newSize = 0;
        int len = data.length;
        for (int i = 0; i < len; i++) {
            if (!isWhiteSpace(data[i])) {
                data[newSize++] = data[i];
            }
        }
        return newSize;
    }
}

package cn.inpark.common.util.sign;

import org.springframework.util.CollectionUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.TreeMap;

public class RSAEncrypt {
    /**
     * 字节数据转字符串专用集合
     */
    private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
            'f'};

    /**
     * 随机生成密钥对
     */
    public static void genKeyPair(String filePath) {
        // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
        KeyPairGenerator keyPairGen = null;
        try {
            keyPairGen = KeyPairGenerator.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // 初始化密钥对生成器,密钥大小为96-1024位
        keyPairGen.initialize(1024, new SecureRandom());
        // 生成一个密钥对,保存在keyPair中
        KeyPair keyPair = keyPairGen.generateKeyPair();
        // 得到私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 得到公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        try {
            // 得到公钥字符串
            String publicKeyString = Base64.encode(publicKey.getEncoded());
            // 得到私钥字符串
            String privateKeyString = Base64.encode(privateKey.getEncoded());

            checkOrCreateFile(filePath + "/publicKey.keystore");
            checkOrCreateFile(filePath + "/privateKey.keystore");
            // 将密钥对写入到文件
            FileWriter pubfw = new FileWriter(filePath + "/publicKey.keystore");
            FileWriter prifw = new FileWriter(filePath + "/privateKey.keystore");
            BufferedWriter pubbw = new BufferedWriter(pubfw);
            BufferedWriter pribw = new BufferedWriter(prifw);
            pubbw.write(publicKeyString);
            pribw.write(privateKeyString);
            pubbw.flush();
            pubbw.close();
            pubfw.close();
            pribw.flush();
            pribw.close();
            prifw.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void checkOrCreateFile(String fileName) throws IOException {
        File file = new File(fileName);
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        if (!file.exists()) {
            file.createNewFile();
        }
    }

    /**
     * 从文件中输入流中加载公钥
     *
     * @param path 公钥输入流
     * @throws Exception 加载公钥时产生的异常
     */
    public static String loadPublicKeyByFile(String path) throws Exception {
        try {
            BufferedReader br = new BufferedReader(new FileReader(path + "/publicKey.keystore"));
            String readLine = null;
            StringBuilder sb = new StringBuilder();
            while ((readLine = br.readLine()) != null) {
                sb.append(readLine);
            }
            br.close();
            return sb.toString();
        } catch (IOException e) {
            throw new Exception("公钥数据流读取错误");
        } catch (NullPointerException e) {
            throw new Exception("公钥输入流为空");
        }
    }

    /**
     * 从字符串中加载公钥
     *
     * @param publicKeyStr 公钥数据字符串
     * @throws Exception 加载公钥时产生的异常
     */
    public static RSAPublicKey loadPublicKeyByStr(String publicKeyStr) throws Exception {
        try {
            byte[] buffer = Base64.decode(publicKeyStr);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
            return (RSAPublicKey) keyFactory.generatePublic(keySpec);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此算法");
        } catch (InvalidKeySpecException e) {
            throw new Exception("公钥非法");
        } catch (NullPointerException e) {
            throw new Exception("公钥数据为空");
        }
    }

    /**
     * 从文件中加载私钥
     *
     * @param path 私钥文件名
     * @return 是否成功
     * @throws Exception
     */
    public static String loadPrivateKeyByFile(String path) throws Exception {
        try {
            BufferedReader br = new BufferedReader(new FileReader(path + "/privateKey.keystore"));
            String readLine = null;
            StringBuilder sb = new StringBuilder();
            while ((readLine = br.readLine()) != null) {
                sb.append(readLine);
            }
            br.close();
            return sb.toString();
        } catch (IOException e) {
            throw new Exception("私钥数据读取错误");
        } catch (NullPointerException e) {
            throw new Exception("私钥输入流为空");
        }
    }

    public static RSAPrivateKey loadPrivateKeyByStr(String privateKeyStr) throws Exception {
        try {
            byte[] buffer = Base64.decode(privateKeyStr);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此算法");
        } catch (InvalidKeySpecException e) {
            throw new Exception("私钥非法");
        } catch (NullPointerException e) {
            throw new Exception("私钥数据为空");
        }
    }

    /**
     * 公钥加密过程
     *
     * @param publicKey     公钥
     * @param plainTextData 明文数据
     * @return
     * @throws Exception 加密过程中的异常信息
     */
    public static String encrypt(RSAPublicKey publicKey, String plainTextData, String encode) throws Exception {
        if (publicKey == null) {
            throw new Exception("加密公钥为空, 请设置");
        }
        Cipher cipher = null;
        try {
            // 使用默认RSA
            cipher = Cipher.getInstance("RSA");
            // cipher= Cipher.getInstance("RSA", new BouncyCastleProvider());
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] output = cipher.doFinal(plainTextData.getBytes(encode));
            return Base64.encode(output);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此加密算法");
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeyException e) {
            throw new Exception("加密公钥非法,请检查");
        } catch (IllegalBlockSizeException e) {
            throw new Exception("明文长度非法");
        } catch (BadPaddingException e) {
            throw new Exception("明文数据已损坏");
        }
    }

    /**
     * 私钥加密过程
     *
     * @param key           私钥
     * @param plainTextData 明文数据
     * @return
     * @throws Exception 加密过程中的异常信息
     */
    public static byte[] encrypt(Key key, byte[] plainTextData) throws Exception {
        if (key == null) {
            throw new Exception("加密私钥为空, 请设置");
        }
        Cipher cipher = null;
        try {
            // 使用默认RSA
            cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] output = cipher.doFinal(plainTextData);
            return output;
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此加密算法");
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeyException e) {
            throw new Exception("加密私钥非法,请检查");
        } catch (IllegalBlockSizeException e) {
            throw new Exception("明文长度非法");
        } catch (BadPaddingException e) {
            throw new Exception("明文数据已损坏");
        }
    }

    /**
     * 私钥解密过程
     *
     * @param key        私钥
     * @param cipherData 密文数据
     * @return 明文
     * @throws Exception 解密过程中的异常信息
     */
    public static String decrypt(Key key, String cipherData, String encode) throws Exception {
        // 64位解码加密后的字符串
        byte[] inputByte = Base64.decode(cipherData);
        if (key == null) {
            throw new Exception("解密私钥为空, 请设置");
        }
        Cipher cipher = null;
        try {
            // 使用默认RSA
            cipher = Cipher.getInstance("RSA");
            // cipher= Cipher.getInstance("RSA", new BouncyCastleProvider());
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] output = cipher.doFinal(inputByte);
            return new String(output, encode);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此解密算法");
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeyException e) {
            throw new Exception("解密私钥非法,请检查");
        } catch (IllegalBlockSizeException e) {
            throw new Exception("密文长度非法");
        } catch (BadPaddingException e) {
            throw new Exception("密文数据已损坏");
        }
    }

    /**
     * 公钥解密过程
     *
     * @param publicKey  公钥
     * @param cipherData 密文数据
     * @return 明文
     * @throws Exception 解密过程中的异常信息
     */
    public static byte[] decrypt(RSAPublicKey publicKey, byte[] cipherData) throws Exception {
        if (publicKey == null) {
            throw new Exception("解密公钥为空, 请设置");
        }
        Cipher cipher = null;
        try {
            // 使用默认RSA
            cipher = Cipher.getInstance("RSA");
            // cipher= Cipher.getInstance("RSA", new BouncyCastleProvider());
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            byte[] output = cipher.doFinal(cipherData);
            return output;
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此解密算法");
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            return null;
        } catch (InvalidKeyException e) {
            throw new Exception("解密公钥非法,请检查");
        } catch (IllegalBlockSizeException e) {
            throw new Exception("密文长度非法");
        } catch (BadPaddingException e) {
            throw new Exception("密文数据已损坏");
        }
    }

    /**
     * 字节数据转十六进制字符串
     *
     * @param data 输入数据
     * @return 十六进制内容
     */
    public static String byteArrayToString(byte[] data) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < data.length; i++) {
            // 取出字节的高四位 作为索引得到相应的十六进制标识符 注意无符号右移
            stringBuilder.append(HEX_CHAR[(data[i] & 0xf0) >>> 4]);
            // 取出字节的低四位 作为索引得到相应的十六进制标识符
            stringBuilder.append(HEX_CHAR[(data[i] & 0x0f)]);
            if (i < data.length - 1) {
                stringBuilder.append(' ');
            }
        }
        return stringBuilder.toString();
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkStringFrom(Map<String, String[]> params) throws Exception {

        TreeMap<String, String> paramsMap = new TreeMap<>();
        for (String name : params.keySet()) {
            String[] value = params.get(name);
            for (int i = 0; i < value.length; i++) {
                paramsMap.put(name, value[i]);
            }
        }

        StringBuffer sb = new StringBuffer();
        for (Map.Entry<String, String> entry : paramsMap.entrySet()) {

            if (!"sign".equals(entry.getKey())) {
                sb.append(entry.getKey()).append("=");
                if (entry.getValue() != null) {
                    sb.append(URLEncoder.encode(entry.getValue().toString(), "utf-8"));
                }
                sb.append("&");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }


    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkStringFrom(Map<String, String[]> params, boolean isSign) {
        StringBuffer sb = new StringBuffer();
        if (!CollectionUtils.isEmpty(params)){
            TreeMap<String, String> paramsMap = new TreeMap<>();
            for (String name : params.keySet()) {
                String[] value = params.get(name);
                for (int i = 0; i < value.length; i++) {
                    paramsMap.put(name, value[i]);
                }
            }

            for (Map.Entry<String, String> entry : paramsMap.entrySet()) {
                sb.append(entry.getKey()).append("=");
                if (entry.getValue() != null) {
                    sb.append(URLEncoder.encode(entry.getValue()));
                }
                sb.append("&");
            }
            sb.deleteCharAt(sb.length() - 1);

        }
        return sb.toString();
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkStringFromCheck(Map<String, String[]> params) throws Exception {

        TreeMap<String, String> paramsMap = new TreeMap<>();
        for (String name : params.keySet()) {
            String[] value = params.get(name);
            for (int i = 0; i < value.length; i++) {
                paramsMap.put(name, value[i]);
            }
        }

        StringBuffer sb = new StringBuffer();
        for (Map.Entry<String, String> entry : paramsMap.entrySet()) {
            if (!"sign".equals(entry.getKey())) {
                sb.append(entry.getKey()).append("=");
                if (entry.getValue() != null) {
                    if ("fromCouponId".equals(entry.getKey())) {
                        sb.append(URLEncoder.encode(entry.getValue(), "utf-8"));
                    } else {
                        sb.append(URLDecoder.decode(entry.getValue().toString(), "utf-8"));
                    }
                }
                sb.append("&");
            }

        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }


    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkStringFrom(TreeMap<String, String> params) throws Exception {

        StringBuffer sb = new StringBuffer();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (!"sign".equals(entry.getKey())) {
                sb.append(entry.getKey()).append("=");
                if (entry.getValue() != null) {
                    sb.append(URLDecoder.decode(entry.getValue().toString(), "utf-8"));
                    // sb.append(entry.getValue().toString());
                }
                sb.append("&");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkString(TreeMap<String, String> params) throws Exception {
        StringBuffer sb = new StringBuffer();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (!"sign".equals(entry.getKey())) {
                sb.append(entry.getKey()).append("=").append(entry.getValue().toString()).append("&");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }

    /**
     * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
     *
     * @param params 需要排序并参与字符拼接的参数组
     * @return 拼接后字符串
     * @throws Exception
     */
    public static String createLinkStringTo(TreeMap<String, String> params) throws Exception {
        StringBuffer sb = new StringBuffer();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            // if (entry.getValue() == null) {
            // throw new IllegalArgumentException("待签名值不能为空");
            // }
            sb.append(entry.getKey()).append("=");
            if (entry.getValue() != null) {
                sb.append(URLEncoder.encode(entry.getValue().toString(), "utf-8"));
                // sb.append(entry.getValue().toString());
            }
            sb.append("&");
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }
}

package cn.inpark.common.util.sign;

import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * RSA签名验签类
 */
public class RSASignature {

    /**
     * 签名算法
     */
    public static final String SIGN_ALGORITHMS = "SHA1WithRSA";

    public static String sign(String content, PrivateKey privateKey, String encode) {
        try {
            java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
            signature.initSign(privateKey);
            signature.update(content.getBytes(encode));
            byte[] signed = signature.sign();
            return Base64.encode(signed);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * RSA签名
     * 
     * @param content    待签名数据
     * @param privateKey 商户私钥
     * @return 签名值
     */
    public static String sign(String content, PrivateKey privateKey) {
        return sign(content, privateKey, "utf-8");
    }

    public static boolean doCheck(String content, String sign, PublicKey publicKey, String encode) {
        try {
            java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS);
            signature.initVerify(publicKey);
            signature.update(content.getBytes(encode));
            boolean bverify = signature.verify(Base64.decode(sign));
            return bverify;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * RSA验签名检查
     * 
     * @param content   待签名数据
     * @param sign      签名值
     * @param publicKey 分配给开发商公钥
     * @return 布尔值
     */
    public static boolean doCheck(String content, String sign, PublicKey publicKey) {
        return doCheck(content, sign, publicKey, "utf-8");
    }

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容