Spring Cloud 集成 License 许可证授权模块

由于客户需要对公司销售的软件进行试用,公司考虑只允许客户在指定服务器上进去短期试用,所以在系统中集成了License授权模块。

一、设计思路

首先,我们需要获取要部署的服务器的基本信息,然后根据服务器信息生成对应的许可证,许可证采用DSA加密算法进行加密,再通过网关过滤器,对所有权限进行校验。

二、获取服务器基本信息

创建获取服务器基本信息模块,打成jar包,在要授权的机器上运行,生成 device.json 文件

Maven依赖

<dependency>
    <groupId>net.sourceforge.nekohtml</groupId>
    <artifactId>nekohtml</artifactId>
    <version>1.9.18</version>
</dependency>

<dependency>
    <groupId>de.schlichtherle.truelicense</groupId>
    <artifactId>truelicense-core</artifactId>
    <version>1.33</version>
</dependency>

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

主类:

通过 DeviceUtil 获取对应的设备信息并生成 device.json 文件到 ./license 目录下,可自行定义目录地址。

public class LicenseDeviceApplication {

    public static void main(String[] args) {
        try {
            DeviceInfo deviceInfo = DeviceUtil.getDeviceInfo();
            String json = new Gson().toJson(deviceInfo);
            Files.createDirectories(Paths.get("license"));
            Files.writeString(Paths.get("license", "device.json"), json);
            System.out.println("success");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

DeviceInfo 类:

@Data
@Accessors(chain = true)
public class DeviceInfo {

    private String cpu;

    private String mainBoard;

    private List<String> ipList;

    private List<String> macList;

}

DeviceUtil 类:

DeviceUtil 根据不同的操作系统采用不同的方式获取服务器数据

public class DeviceUtil {
    private static final String WIN_PRE = "win";

    public static DeviceInfo getDeviceInfo() throws Exception {
        String os = System.getProperty("os.name").toLowerCase();
        Device device;
        if (os.startsWith(WIN_PRE)) {
            device = new WinDevice();
        } else {
            device = new LinuxDevice();
        }
        return device.getDeviceInfo();
    }
}

DeviceUtil 依赖的几个类:

获取设备信息的抽象类,将不同操作系统的通用信息获取方法抽象出来

public abstract class Device {

    public DeviceInfo getDeviceInfo() throws Exception {
        return new DeviceInfo()
                .setIpList(this.getIpAddress())
                .setMacList(this.getMacAddress())
                .setCpu(this.getCpuSerial())
                .setMainBoard(this.getMainBoardSerial());
    }

    List<String> getIpAddress() throws Exception {
        List<InetAddress> inetAddresses = getLocalAllInetAddress();
        return Optional.ofNullable(inetAddresses).orElse(new ArrayList<>(0))
                .stream()
                .map(e -> e.getHostAddress().toLowerCase())
                .distinct()
                .collect(Collectors.toList());
    }

    List<String> getMacAddress() throws Exception {
        List<InetAddress> inetAddresses = getLocalAllInetAddress();
        return Optional.ofNullable(inetAddresses).orElse(new ArrayList<>(0))
                .stream()
                .map(this::getMacByInetAddress)
                .distinct()
                .collect(Collectors.toList());
    }

    /**
     * 获取 CpuSerial
     *
     * @return CpuSerial
     * @throws Exception Exception
     */
    abstract String getCpuSerial() throws Exception;

    /**
     * 获取 MainBoardSerial
     *
     * @return MainBoardSerial
     * @throws Exception Exception
     */
    abstract String getMainBoardSerial() throws Exception;

    List<InetAddress> getLocalAllInetAddress() throws Exception {
        List<InetAddress> inetAddressList = new ArrayList<>();
        for (Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
             networkInterfaces.hasMoreElements(); ) {
            NetworkInterface networkInterface = networkInterfaces.nextElement();
            for (Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
                 inetAddresses.hasMoreElements(); ) {
                InetAddress inetAddr = inetAddresses.nextElement();
                if (!inetAddr.isLoopbackAddress()
                        && !inetAddr.isLinkLocalAddress()
                        && !inetAddr.isMulticastAddress()) {
                    inetAddressList.add(inetAddr);
                }
            }
        }
        return inetAddressList;
    }

    String getMacByInetAddress(InetAddress inetAddr) {
        try {
            byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
            StringBuilder buf = new StringBuilder();
            for (int i = 0; i < mac.length; i++) {
                if (i != 0) {
                    buf.append("-");
                }
                String temp = Integer.toHexString(mac[i] & 0xff);
                if (temp.length() == 1) {
                    buf.append("0").append(temp);
                } else {
                    buf.append(temp);
                }
            }
            return buf.toString().toUpperCase();
        } catch (SocketException e) {
            e.printStackTrace();
        }
        return null;
    }
}

获取Linux操作系统服务器信息

public class LinuxDevice extends Device {

    @Override
    protected String getCpuSerial() throws Exception {
        String serialNumber = "";
        String[] shell = {"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
        Process process = Runtime.getRuntime().exec(shell);
        process.getOutputStream().close();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line = reader.readLine().trim();
        if (isNotBlank(line)) {
            serialNumber = line;
        }
        reader.close();
        return serialNumber;
    }

    @Override
    protected String getMainBoardSerial() throws Exception {
        String serialNumber = "";
        String[] shell = {"/bin/bash", "-c", "dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
        Process process = Runtime.getRuntime().exec(shell);
        process.getOutputStream().close();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line = reader.readLine().trim();
        if (isNotBlank(line)) {
            serialNumber = line;
        }
        reader.close();
        return serialNumber;
    }

    private boolean isNotBlank(String str) {
        return null != str && !"".equals(str.trim());
    }
}

获取Windows操作系统的服务器信息

public class WinDevice extends Device {

    @Override
    protected String getCpuSerial() throws Exception {
        String serialNumber = "";
        Process process = Runtime.getRuntime().exec("wmic cpu get processorid");
        process.getOutputStream().close();
        Scanner scanner = new Scanner(process.getInputStream());
        if (scanner.hasNext()) {
            scanner.next();
        }
        if (scanner.hasNext()) {
            serialNumber = scanner.next().trim();
        }
        scanner.close();
        return serialNumber;
    }

    @Override
    protected String getMainBoardSerial() throws Exception {
        String serialNumber = "";
        Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
        process.getOutputStream().close();
        Scanner scanner = new Scanner(process.getInputStream());
        if (scanner.hasNext()) {
            scanner.next();
        }
        if (scanner.hasNext()) {
            serialNumber = scanner.next().trim();
        }
        scanner.close();
        return serialNumber;
    }
}

三、生成许可证

创建一个生成许可证模块,可以使用本地运行获取的方式,也可以采用接口的方式,通过上传 device.json 文件获取授权文件。这里演示下本地获取的方式:

Maven依赖

<dependency>
    <groupId>net.sourceforge.nekohtml</groupId>
    <artifactId>nekohtml</artifactId>
    <version>1.9.18</version>
</dependency>

<dependency>
    <groupId>de.schlichtherle.truelicense</groupId>
    <artifactId>truelicense-core</artifactId>
    <version>1.33</version>
</dependency>

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

主类

public class LicenseGenApplication {

    public static void main(String[] args) {
        //生成公私钥对
        //项目名称
        String subject = "subject";
        //公钥密码
        String pubPwd = "a123456";
        //私钥密码
        String priPwd = "a123457";
        LicenseGenUtils.licenseGen(subject, pubPwd, priPwd);
    }
}

LicenseGenUtils 类

生成许可证,并输出到 ./license 目录下

public class LicenseGenUtils {
    public static void licenseGen(String subject, String pubPwd, String priPwd) {
        try {
            KeyUtil.createKey(subject, pubPwd, priPwd);

            //读取客户端设备信息
            //需要将device.json文件放到./license目录下
            File device = new File("./license/device.json");
            String json = Files.readAllLines(device.toPath(), StandardCharsets.UTF_8).get(0);

            //生成license
            LicenseCreatorParam param = new LicenseCreatorParam()
                    .setSubject(subject)
                    .setPubPass(pubPwd)
                    .setPriPass(priPwd)
                    .setDeviceInfo(json);
            new MyLicenseCreator()
                    .setCreatorParam(param)
                    .execute();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

LicenseGenUtils 类依赖的类

利用jdk中的 keytool 采用DSA加密算法,生成对应的公私钥。

@Slf4j
public class KeyUtil {

    public static void createKey(String name, String pubPwd, String priPwd) {
        Process process = null;
        try {
            //生成私匙库
            String cmd = "keytool -genkeypair -keysize 1024 -validity 3650 -alias \"privateKey\" -keystore \"./license/private.key\" -keyalg \"DSA\" -storetype \"JKS\" -storepass \"{0}\" -keypass \"{1}\" -dname \"CN={2}, OU={3}, O={4}, L=SZ, ST=SZ, C=CN\"";
            System.out.println("生成私匙库:" + MessageFormat.format(cmd, pubPwd, priPwd, name, name, name));
            process = Runtime.getRuntime().exec(MessageFormat.format(cmd, pubPwd, priPwd, name, name, name));
            handleBuffer(process);
            process.waitFor();
            process.destroy();

            //导出证书
            cmd = "keytool -exportcert -alias \"privateKey\" -keystore \"./license/private.key\" -storepass \"{0}\" -file \"./license/cert.cer\"";
            System.out.println("导出证书:" + MessageFormat.format(cmd, pubPwd));
            process = Runtime.getRuntime().exec(MessageFormat.format(cmd, pubPwd));
            handleBuffer(process);
            process.waitFor();
            process.destroy();

            //把这个证书文件导入到公匙库
            //-noprompt 指定无交互,默认yes
            cmd = "keytool -import -alias \"publicKey\" -file \"./license/cert.cer\" -keystore \"./license/public.key\" -storepass \"{0}\" -noprompt";
            System.out.println("把这个证书文件导入到公匙库:" + MessageFormat.format(cmd, pubPwd));
            process = Runtime.getRuntime().exec(MessageFormat.format(cmd, pubPwd));
            handleBuffer(process);
            process.waitFor();
            process.destroy();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != process) {
                process.destroy();
            }
        }
    }

    private static void handleBuffer(Process process) {
        try (InputStream input = process.getInputStream();
             BufferedReader inputReader = new BufferedReader(new InputStreamReader(input, Charset.forName("GBK")));
             InputStream error = process.getErrorStream();
             BufferedReader errorReader = new BufferedReader(new InputStreamReader(error, Charset.forName("GBK")))) {
            String inputLog, errorLog;
            while ((inputLog = inputReader.readLine()) != null) {
                System.out.println(inputLog);
            }
            while ((errorLog = errorReader.readLine()) != null) {
                System.out.println(errorLog);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

配置密钥库参数

public class MyKeyStoreParam extends AbstractKeyStoreParam {

    private final String priKeyPath;
    private final String alias;
    private final String storePwd;
    private final String keyPwd;

    public MyKeyStoreParam(Class clazz, String resource, String alias, String storePwd, String keyPwd) {
        super(clazz, resource);
        this.priKeyPath = resource;
        this.alias = alias;
        this.storePwd = storePwd;
        this.keyPwd = keyPwd;
    }

    @Override
    public String getAlias() {
        return alias;
    }

    @Override
    public String getStorePwd() {
        return storePwd;
    }

    @Override
    public String getKeyPwd() {
        return keyPwd;
    }

    @Override
    public InputStream getStream() throws IOException {
        return new FileInputStream(priKeyPath);
    }
}

配置许可证创建的参数

@Data
@Accessors(chain = true)
public class LicenseCreatorParam {

    private String subject;

    private String priAlias = "privateKey";

    /**
     * 私钥文件路径
     */
    private String priKeyPath = "./license/private.key";

    /**
     * 私钥密码
     */
    private String priPass;

    /**
     * 公钥密码
     */
    private String pubPass;

    /**
     * 证书地址
     */
    private String licensePath = "./license/license.lic";

    /**
     * 证书生效时间
     */
    private Date issuedTime = new Date();

    /**
     * 证书失效时间,180天后
     */
    private Date expiryTime = new Date(System.currentTimeMillis() + 180L * 24L * 3600L * 1000L);

    /**
     * 额外的服务器硬件校验信息
     */
    private String deviceInfo;

    public LicenseParam toLicenseParam(Class<?> c) {
        Preferences preferences = Preferences.userNodeForPackage(c);
        //设置对证书内容加密的秘钥
        CipherParam cipherParam = new DefaultCipherParam(getPubPass());
        KeyStoreParam privateStoreParam = new MyKeyStoreParam(MyLicenseCreator.class, getPriKeyPath(), getPriAlias(),
                getPubPass(), getPriPass());
        return new DefaultLicenseParam(getSubject(), preferences, privateStoreParam, cipherParam);
    }

}

许可证生成执行器

@Data
@Accessors(chain = true)
public class MyLicenseCreator {

    private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=YUBEST, OU=YUBEST, O=YUBEST, L=SZ, ST=SZ, C=CN");

    private LicenseCreatorParam creatorParam;

    public void execute() throws Exception {
        LicenseParam param = creatorParam.toLicenseParam(MyLicenseCreator.class);
        MyLicenseManager manager = new MyLicenseManager(param);
        manager.setCreator(true);
        LicenseContent content = wrapContent();
        manager.store(content, new File(creatorParam.getLicensePath()));
    }

    private LicenseContent wrapContent() {
        LicenseContent content = new LicenseContent();
        content.setHolder(DEFAULT_HOLDER_AND_ISSUER);
        content.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
        content.setSubject(creatorParam.getSubject());
        content.setIssued(creatorParam.getIssuedTime());
        content.setNotBefore(creatorParam.getIssuedTime());
        content.setNotAfter(creatorParam.getExpiryTime());
        content.setConsumerType("User");
        content.setInfo("颁发给" + creatorParam.getSubject() + "的证书");
        //扩展校验服务器硬件信息
        content.setExtra(creatorParam.getDeviceInfo());
        return content;
    }

}

许可证管理器,负责证书的注册校验等操作

@Setter
public class MyLicenseManager extends LicenseManager {

    private boolean creator;

    public MyLicenseManager(LicenseParam param) {
        super(param);
    }

    @Override
    protected synchronized LicenseContent verify(final LicenseNotary notary)
            throws Exception {
        GenericCertificate certificate = getCertificate();
        if (null != certificate) {
            return (LicenseContent) certificate.getContent();
        }

        // Load license key from preferences,
        final byte[] key = getLicenseKey();
        if (null == key) {
            throw new NoLicenseInstalledException(getLicenseParam().getSubject());
        }
        certificate = getPrivacyGuard().key2cert(key);
        notary.verify(certificate);
        //重写获取内容的方法
        final LicenseContent content = this.readXml(certificate.getEncoded());
        this.validate(content);
        setCertificate(certificate);

        return content;
    }

    private LicenseContent readXml(String encoded) throws Exception {
        try (BufferedInputStream inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(StandardCharsets.UTF_8)));
             XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(inputStream, 4096), null, null)) {
            return (LicenseContent) decoder.readObject();
        }
    }

    @Override
    protected synchronized void validate(final LicenseContent content)
            throws LicenseContentException {
        super.validate(content);
        if (creator) {
            return;
        }
        String deviceJson = (String) content.getExtra();
        if (isNull(deviceJson)) {
            return;
        }
        DeviceInfo localDevice;
        try {
            localDevice = DeviceUtil.getDeviceInfo();
        } catch (Exception e) {
            throw new LicenseContentException("无法获取当前服务器设备信息");
        }
        if (null == localDevice) {
            throw new LicenseContentException("无法获取当前服务器设备信息");
        }
        DeviceInfo licDevice = new Gson().fromJson(deviceJson, DeviceInfo.class);
        boolean isCpuOk = true;
        if (!isNull(licDevice.getCpu())) {
            isCpuOk = licDevice.getCpu().equals(localDevice.getCpu());
        }
        boolean isMainBoardOk = true;
        if (!isNull(licDevice.getMainBoard())) {
            isMainBoardOk = licDevice.getMainBoard().equals(localDevice.getMainBoard());
        }
        boolean isIpOk = true;
        if (isNotNull(licDevice.getIpList())) {
            isIpOk = licDevice.getIpList().removeAll(localDevice.getIpList());
        }
        boolean isMacOk = true;
        if (isNotNull(licDevice.getMacList())) {
            isMacOk = licDevice.getMacList().removeAll(localDevice.getMacList());
        }

        if (!isCpuOk || !isMainBoardOk || !isIpOk || !isMacOk) {
            throw new LicenseContentException("当前服务器不在授权范围内");
        }
    }

    private boolean isNull(String str) {
        return null == str || "".equals(str.trim());
    }

    private boolean isNotNull(List<?> list) {
        return null != list && !list.isEmpty();
    }

}

四、在网关集成license模块

在网关启动时校验许可证,并且通过过滤器的方式,对所有请求都进行许可证校验。网关采用的是gateway网关。

Maven依赖

<dependency>
    <groupId>net.sourceforge.nekohtml</groupId>
    <artifactId>nekohtml</artifactId>
    <version>1.9.18</version>
</dependency>

<dependency>
    <groupId>de.schlichtherle.truelicense</groupId>
    <artifactId>truelicense-core</artifactId>
    <version>1.33</version>
</dependency>

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

LicenseConfig

注册 LicenseManager

@Data
@EqualsAndHashCode(callSuper = true)
@Component
@ConfigurationProperties(prefix = "license")
public class LicenseConfig extends LicenseVerifierParam {
    private boolean enabled = true;

    @Bean
    public LicenseManager licenseManager() {
        LicenseParam param = toLicenseParam(LicenseConfig.class);
        return new MyLicenseManager(param);
    }
}

许可证校验参数配置类

@Data
public class LicenseVerifierParam {

    private String subject;

    private String publicAlias = "publicKey";

    private String storePass;

    private String licPath;

    private String pubKeyPath;

    public LicenseParam toLicenseParam(Class<?> c) {
        Preferences preferences = Preferences.userNodeForPackage(c);
        CipherParam cipherParam = new DefaultCipherParam(getStorePass());
        KeyStoreParam publicStoreParam = new MyKeyStoreParam(c, getPubKeyPath(),
                getPublicAlias(), getStorePass(), null);
        return new DefaultLicenseParam(getSubject(), preferences, publicStoreParam, cipherParam);
    }

}

application.yml配置

license:
  subject: subject
  storePass: a123456
  licPath: /etc/license/license.lic
  pubKeyPath: /etc/license/public.key

LicenseFilter过滤器

过滤所有接口,对证书进行校验,校验做了缓存处理,半个小时校验一次。

@Slf4j
@Component
@RefreshScope
@RequiredArgsConstructor
public class LicenseFilter implements GlobalFilter, Ordered {
    private final LicenseConfig licenseConfig;
    private final LicenseManager licenseManager;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            new MyLicenseVerifier().verify(licenseManager);
            return chain.filter(exchange);
        } catch (Exception e) {
            log.error("证书校验失败", e);
        }
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        //自定义响应数据
        Object result = null;
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
        return response.writeWith(Mono.just(dataBuffer));
    }

    @Override
    public int getOrder() {
        return -300;
    }
}

LicenseRegister 注册许可证

启动时校验许可证并注册

@Component
@Slf4j
@RequiredArgsConstructor
public class LicenseRegister implements ApplicationRunner {
    private final LicenseConfig licenseConfig;
    private final LicenseManager licenseManager;

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

推荐阅读更多精彩内容