Spring Boot之整合Spring Security: 访问认证

前言

在过往的一些Spring Boot学习项目中,我们会发现,我们开发的API都不需要认证,对所有人开放,连登录都不需要,毫无安全可言。
在项目实战中往往需要做好认证、授权、攻击防护,Spring Boot在这方面也提供了快速解决方案,即:推荐使用Spring Security

  • Spring Boot为Spring Security提供了自动化配置方案,可零配置使用 Spring Security。

项目代码已上传Git Hub,欢迎取阅:

简单入门

1. 添加依赖;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

2. 编写Controller;

package com.github.dylanz666.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String sayHello() throws Exception {
        return "Hello!";
    }
}

3. 启动项目并访问API;

  • 启动项目:

注意一条log:Using generated security password: e10ac5ca-d3ab-4f0e-8e25-cbcf6afce611,下文会使用到。

启动项目
  • 在浏览器中访问API:

API如:http://127.0.0.1:8080/hello

默认登录页面
访问API
  • 输入用户名密码登录:
    1). Username: 默认用户名为user;
    2). Password: 默认密码为log中打印的密码,e10ac5ca-d3ab-4f0e-8e25-cbcf6afce611;
    户名密码登录
我们一起来分析一下:

1). 未登录时访问API会重定向到登录页面:http://127.0.0.1:8080/login
2). Spring Security为我们提供了默认的登录页面,登录页面还算美观;
3). 登录后,后续的请求中,会在请求头中带上含有JESSIONID的Cookie;

Cookie

可在项目application.properties中提前配置好用户名和密码,如:

server.port=8080
spring.security.user.name=dylanz
spring.security.user.password=666
用户名密码登录

至此,我们就实现了最简单的登录认证。


自定义登录页面实例

  • 未登录状态下API请求重定向到登录页面还是比较奇怪的,一般来说,API未登录状态下的请求应该显示状态码:401
  • 通常情况下,应该是进入某个有访问限制的页面,当未登录时,重定向到登录页面;

因此,我们将场景变为:

我们将采用视图技术,简单做个案例。Spring Boot框架内使用视图技术可参考:

thymeleaf使用准备:

1). 添加thymeleaf依赖;
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2). 修改配置文件;
server.port=8080
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8

1. 定义主页home.html;

  • 在resources下创建templates文件夹,并创建home.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>

<p>Click <a th:href="@{/hello.html}">here</a> to see a greeting.</p>
</body>
</html>
  • 前往hello.html页面的代码:<a th:href="@{/hello.html}">here</a>

2. 定义hello.html页面;

  • 在templates文件夹下创建hello.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
    <input type="submit" value="Sign Out"/>
</form>
</body>
</html>
  • hello.html页面上提供一个登出入口"Sign Out";

3. 自定义login/logout页面;

  • 在templates文件夹下创建login.html文件:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
    Invalid username and password.
</div>
<div th:if="${param.logout}">
    You have been logged out.
</div>
<form th:action="@{/login}" method="post">
    <div><label th:style="'background:red;'"> User Name: <input type="text" name="username"/> </label></div>
    <div><label th:style="'background:red;'"> Password: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
  • 当用户名密码错误时提示信息:Invalid username and password.
  • 当登出时提示信息:You have been logged out.
  • 为了演示自定义页面,我还特地改了下页面元素样式,把User Name和Password label的背景色改为红色:th:style="'background:red;'"
    (笔者没有花过多的时间处理样式哈,此处只做简单演示)

4. 组织页面行为;

1). 配置模板匹配规则;

目的是使网站的url指向具体视图,而不是当作API来访问;
在项目下创建config包,并在config包内创建WebMvcConfig类,编写WebMvcConfig类如下:

package com.github.dylanz666.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home.html").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello.html").setViewName("hello");
        registry.addViewController("/login.html").setViewName("login");
    }
}
  • 访问/和/home.html路径时,使用模板:home.html
  • 访问/hello.html路径时,使用模板:hello.html
  • 访问/login.html路径时,使用模板:login.html
2). 页面访问权限设置;

在config包下创建类:WebSecurityConfig,编写类如下:

package com.github.dylanz666.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
                .anyRequest().authenticated()//其他url都需要访问认证
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面的url
                .loginProcessingUrl("/login")//登录表使用的API
                .permitAll()//login.html和login不需要访问认证
                .and()
                .logout()
                .permitAll();//logout不需要访问认证
    }
}

几点解释:

  • @EnableWebSecurity:官网说这是为了开启Web Security支持,并提供Spring MVC集成,具体咋回事咱也不知道呀,跟着用就是对了!
  • .antMatchers("/", "/home.html").permitAll():配置不需要认证的url,也即任何人都可以访问的url;
  • .loginPage("/login.html"):配置登录页面的url,由于我们自定义了登录页面,因此需使用这个配置,如果不是用此配置,则使用Spring Security提供的默认登录页面;
  • .loginProcessingUrl("/login"): 配置登录表单使用的API,Spring Security默认提供"/login"接口,用于登录验证;

3). 启动项目查看效果;

  • 访问主页:http://127.0.0.1:8080/

访问主页
  • 点击页面中的"here"链接;

点击链接

此时尝试访问http://127.0.0.1:8080/hello.html,但由于我们没有登录,因此Spring Security自动帮我们跳转到登录页面:http://127.0.0.1:8080/login.html

  • 登录;

登录

登录后
  • 登录后访问项目写好的API;

笔者在项目中的controller包中写了个HelloController类,类中写了个get类型的API,代码如下:

package com.github.dylanz666.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String sayHello() throws Exception {
        return "Hello!";
    }
}

此时在浏览器中直接访问API:http://127.0.0.1:8080/hello

登录访问API

  • 登出;

点击hello.html页面上的"Sign Out"按钮登出;


登出

此时退出到登录页面,且页面有提示信息:You have been logged out.

  • 登出后访问项目写好的API;

再次在浏览器中直接访问API:http://127.0.0.1:8080/hello
此时我们会发现API被重定向到登录页面了;

登出访问API

通过本案例,我们学会了如何使用Spring Security进行基本的访问限制和自定义登录页面。


用户管理;

用户管理有几种方式:

1. 在resources底下的application.properties内配置可登录的用户信息:

spring.security.user.name=dylanz
spring.security.user.password=666
这种方式有个弊端:只能配置一个用户信息;

2. 在config底下的WebSecurityConfig配置类内添加可登录的用户信息userDetailsService,如:

package com.github.dylanz666.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
                .anyRequest().authenticated()//其他url都需要访问认证
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面的url
                .loginProcessingUrl("/login")//登录表使用的API
                .permitAll()//login.html和login不需要访问认证
                .and()
                .logout()
                .permitAll();//logout不需要访问认证
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails dylanz =
                User.withUsername("dylanz")
                        .password(bCryptPasswordEncoder.encode("666"))
                        .roles("ADMIN")
                        .build();
        return new InMemoryUserDetailsManager(user);
    }
}

3. WebSecurityConfig配置类内可配置多个可登录的用户信息:

package com.github.dylanz666.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
                .anyRequest().authenticated()//其他url都需要访问认证
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面的url
                .loginProcessingUrl("/login")//登录表使用的API
                .permitAll()//login.html和login不需要访问认证
                .and()
                .logout()
                .permitAll();//logout不需要访问认证
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        UserDetails dylanz =
                User.withUsername("dylanz")
                        .password(bCryptPasswordEncoder.encode("666"))
                        .roles("ADMIN")
                        .build();
        UserDetails ritay =
                User.withUsername("ritay")
                        .password(bCryptPasswordEncoder.encode("888"))
                        .roles("USER")
                        .build();
        UserDetails jonathanw =
                User.withUsername("jonathanw")
                        .password(bCryptPasswordEncoder.encode("999"))
                        .roles("USER")
                        .build();
        return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
    }
}
我在WebSecurityConfig配置类内设置了3个可登录的用户,我们可以通过这种方式相对灵活的添加N个用户。

4. 在数据库中保存可登录的用户信息:

这是更常见的保存用户信息的方式,我们仍以最简单的方式来Demo从中心化的用户信息池获取用户信息,即:模拟数据库查询过程;
1). 项目下创建domain包、service包;
2). domain包内创建User实体类、service包下创建UserDetailsImpl类和UserDetailsServiceImpl类;

创建类

3). 编写User实体类;

package com.github.dylanz666.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author : dylanz
 * @since : 08/31/2020
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String username;
    private String password;
}

4). 编写UserDetailsImpl类;

package com.github.dylanz666.service;

import com.github.dylanz666.domain.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;

/**
 * @author : dylanz
 * @since : 08/31/2020
 */
@Service
public class UserDetailsImpl implements UserDetails {
    private User currentUser;

    public UserDetailsImpl() {
    }

    public UserDetailsImpl(User user) {
        if (user != null) {
            this.currentUser = user;
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
        authorities.add(authority);
        return authorities;
    }

    @Override
    public String getPassword() {
        return currentUser.getPassword();
    }

    public String getUsername() {
        return currentUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

5). 编写UserDetailsServiceImpl类;

package com.github.dylanz666.service;

import com.github.dylanz666.domain.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author : dylanz
 * @since : 08/31/2020
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private UserDetailsImpl userService;
    @Autowired
    private UserDetails userDetails;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //Spring Security要求必须加密密码
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        //模拟从数据库中取出用户信息,使用的sql如: SELECT * FROM USER WHERE USER_NAME='cherrys'
        List<User> userList = new ArrayList<>();
        User firstUser = new User();
        firstUser.setUsername("cherrys");
        firstUser.setPassword(passwordEncoder.encode("123"));
        userList.add(firstUser);
        User secondUser = new User();
        secondUser.setUsername("randyh");
        secondUser.setPassword(passwordEncoder.encode("456"));
        userList.add(secondUser);

        List<User> mappedUsers = userList.stream().filter(s -> s.getUsername().equals(username)).collect(Collectors.toList());

        //判断用户是否存在
        User user;
        if (CollectionUtils.isEmpty(mappedUsers)) {
            logger.info(String.format("The user %s is not found !", username));
            throw new UsernameNotFoundException(String.format("The user %s is not found !", username));
        }
        user = mappedUsers.get(0);
        return new UserDetailsImpl(user);
    }
}

解释一下:

  • UserDetailsServiceImpl: 用于模拟从数据库查询出用户信息,且模拟数据库中存储了加密的字符串;

  • UserDetailsImpl:用于使用从数据库查询出的用户信息,设置可登录的用户名、密码,设置过程要配合使用WebSecurityConfig;

6). 修改WebSecurityConfig配置类;

package com.github.dylanz666.config;

import com.github.dylanz666.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author : dylanz
 * @since : 08/30/2020
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers("/", "/home.html").permitAll()//这2个url不用访问认证
                .anyRequest().authenticated()//其他url都需要访问认证
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面的url
                .loginProcessingUrl("/login")//登录表使用的API
                .permitAll()//login.html和login不需要访问认证
                .and()
                .logout()
                .permitAll();//logout不需要访问认证
        httpSecurity.userDetailsService(userDetailsService());
        httpSecurity.userDetailsService(userDetailsService);
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        UserDetails dylanz =
                User.withUsername("dylanz")
                        .password(bCryptPasswordEncoder.encode("666"))
                        .roles("ADMIN")
                        .build();
        UserDetails ritay =
                User.withUsername("ritay")
                        .password(bCryptPasswordEncoder.encode("888"))
                        .roles("USER")
                        .build();
        UserDetails jonathanw =
                User.withUsername("jonathanw")
                        .password(bCryptPasswordEncoder.encode("999"))
                        .roles("USER")
                        .build();
        return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

特别注意:

  • 必须在WebSecurityConfig中声明PasswordEncoder;

  • 在WebSecurityConfig的configure方法中使用:

httpSecurity.userDetailsService(userDetailsService);

至此,我们在内存中添加了dylanz,ritay,jonathanw三个用户,并且数据库中也存储了cherrys、randyh两个用户,一共5个用户;

我们来测试一下:

randyh+正确密码1
randyh+正确密码2
randyh+错误密码
dylanz+正确密码1
dylanz+正确密码2
dylanz+错误密码
不存在的账户

这个认证过程还是比较初级的,真实案例中会比这个认证过程复杂许多,我们开卷有益,再接再厉!


如果本文对您有帮助,麻烦动动手指点点赞?

谢谢!

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

推荐阅读更多精彩内容