Spring Security项目Spring MVC开发RESTful API(二)

查询请求

常用注解

  • @RestController 标明此Controller提供RestAPI
  • @RequestMapping 映射http请求url到java方法
  • @RequestParam 映射请求参数到java方法到参数
  • @PageableDefault 指定分页参数默认值

编写一个简单的UserController类

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @RequestMapping(method = RequestMethod.GET)
    public List<User> query(@RequestParam(name = "username",required = true) String username, @PageableDefault(page = 1,size = 20,sort = "username",direction = Sort.Direction.DESC)Pageable pageable){

        System.out.println(pageable.getSort());
        List<User>users=new ArrayList<>();
        users.add(new User("aaa","111"));
        users.add(new User("bbb","222"));
        users.add(new User("ddd","333"));
        return  users;
    }
}

@PageableDefault SpingData分页参数 page当前页数默认0开始 sizi每页个数默认10 sort 排序

Srping boot 测试用例

在demo的pom.xml里面引入spirngboot的测试

    <!--spring测试框架-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

测试/user接口

@RunWith(SpringRunner.class) //运行器
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void stup(){
        mockMvc= MockMvcBuilders.webAppContextSetup(wac).build();
    }
  //测试用例
    @Test
    public void whenQuerSuccess() throws Exception {
        String result=mockMvc.perform(MockMvcRequestBuilders.get("/user")
                //传过去的参数
                .param("username","admin")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                //判断请求的状态吗是否成功,200
                .andExpect(MockMvcResultMatchers.status().isOk())
                //判断返回的集合的长度是否是3
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3))
                //打印信息
                .andDo(MockMvcResultHandlers.print())
                .andReturn().getResponse().getContentAsString();
        //打印返回结果
        System.out.println(result);
    }

jsonPath文档语法查询地址

用户详情请求

常用注解

  • @PathVariable 映射url片段到java方法参数
  • @JsonView 控制json输出内容

实体对象

@NoArgsConstructor
@AllArgsConstructor
public class User {

    public interface UserSimpleView{};
    public interface UserDetailView extends UserSimpleView{};

    private String username;
    private String password;

    @JsonView(UserSimpleView.class)
    public String getUsername() {
        return username;
    }


    public void setUsername(String username) {
        this.username = username;
    }

    @JsonView(UserDetailView.class)
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

Controller类

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @RequestMapping(value = "/{id:\\d+}",method = RequestMethod.GET)
    // 正则表达式 :\\d+ 表示只能输入数字
    //用户名密码都显示
    @JsonView(User.UserDetailView.class)
    public User  userInfo(@PathVariable String id){
        User user=new User();
        user.setUsername("tom");
        return user;
    }
}

测试用例

@RunWith(SpringRunner.class) //运行器
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void stup(){
        mockMvc= MockMvcBuilders.webAppContextSetup(wac).build();
    }

    //用户详情用例
    @Test
    public void whenUserInfoSuccess() throws Exception {
        String result=mockMvc.perform(MockMvcRequestBuilders.get("/user/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                //判断请求的状态吗是否成功,200
                .andExpect(MockMvcResultMatchers.status().isOk())
                //判断返回到username是不是tom
                .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("tom"))
                //打印信息
                .andDo(MockMvcResultHandlers.print())
                .andReturn().getResponse().getContentAsString();
        //打印返回结果
        System.out.println(result);
    }
}

用户处理创建请求

常用注解

  • @RequestBody 映射请求体到java方法到参数
  • @Valid注解和BindingResult验证请求参数合法性并处理校验结果

实体对象

@NoArgsConstructor
@AllArgsConstructor
public class User {

    public interface UserSimpleView{};
    public interface UserDetailView extends UserSimpleView{};
    private String id;
    private String username;

    //不允许password为null
    @NotBlank
    private String password;

    private Date birthday;

    @JsonView(UserSimpleView.class)
    public String getId() { return id; }

    public void setId(String id) { this.id = id; }

    @JsonView(UserSimpleView.class)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }



    @JsonView(UserDetailView.class)
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @JsonView(UserSimpleView.class)
    public Date getBirthday() { return birthday; }

    public void setBirthday(Date birthday) { this.birthday = birthday; }
}


Controller类

    @RequestMapping(method = RequestMethod.POST)
    @JsonView(User.UserSimpleView.class)
    //@Valid启用校验password不允许为空
    public User createUser(@Valid @RequestBody User user, BindingResult errors){
        //如果校验有错误是true并打印错误信息
        if(errors.hasErrors()){
            errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
        }
        System.out.println(user.getUsername());
        System.out.println(user.getPassword());
        System.out.println(user.getBirthday());
        user.setId("1");
        return user;
    }

测试用例

@RunWith(SpringRunner.class) //运行器
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void stup(){
        mockMvc= MockMvcBuilders.webAppContextSetup(wac).build();
    }

    //用户创建用例
    @Test
    public void whenCreateSuccess() throws Exception {
        Date date=new Date();
        String content="{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";
        String result=mockMvc.perform(MockMvcRequestBuilders.post("/user")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                //判断请求的状态吗是否成功,200
                .andExpect(MockMvcResultMatchers.status().isOk())
                //判断返回到username是不是tom
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
                .andReturn().getResponse().getContentAsString();
        //打印返回结果
        System.out.println(result);
    }
}

修改和删除请求

验证注解

注解 解释
@NotNull 值不能为空
@Null 值必须为空
@Pattern(regex=) 字符串必须匹配正则表达式
@Size(min=,max=) 集合的元素数量必须在min和max之间
@Email 字符串必须是Email地址
@Length(min=,max=) 检查字符串长度
@NotBlank 字符串必须有字符
@NotEmpty 字符串不为null,集合有元素
@Range(min=,max=) 数字必须大于等于min,小于等于max
@SafeHtml 字符串是安全的html
@URL 字符串是合法的URL
@AssertFalse 值必须是false
@AssertTrue 值必须是true
@DecimalMax(value=,inclusive) 值必须小于等于(inclusive=true)/小于(inclusive=false) value指定的值
@DecimalMin(value=,inclusive) 值必须大于等于(inclusive=true)/大于(inclusive=false) value指定的值
@Digits(integer=,fraction=) integer指定整数部分最大长度,fraction小数部分最大长度
@Future 被注释的元素必须是一个将来的日期
@Past 被注释的元素必须是一个过去的日期
@Max(value=) 值必须小于等于value值
@Min(value=) 值必须大于等于value值

自定义注解修改请求

实体对象

@NoArgsConstructor
@AllArgsConstructor
public class User {

    public interface UserSimpleView{};
    public interface UserDetailView extends UserSimpleView{};
    private String id;
    
    //自定义注解
    @MyConstraint(message = "账号必须是tom")
    private String username;

    //不允许password为null
    @NotBlank(message = "密码不能为空")
    private String password;

    //加验证生日必须是过去的时间
    @Past(message = "生日必须是过去的时间")
    private Date birthday;

    @JsonView(UserSimpleView.class)
    public String getId() { return id; }

    public void setId(String id) { this.id = id; }

    @JsonView(UserSimpleView.class)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }



    @JsonView(UserDetailView.class)
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @JsonView(UserSimpleView.class)
    public Date getBirthday() { return birthday; }

    public void setBirthday(Date birthday) { this.birthday = birthday; }
}


Controller类

    @RequestMapping(value = "/{id:\\d+}",method = RequestMethod.PUT)
    @JsonView(User.UserSimpleView.class)
    //@Valid启用校验password不允许为空
    public User updateUser(@Valid @RequestBody User user, BindingResult errors){
        //如果校验有错误是true并打印错误信息
        if(errors.hasErrors()){
            errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
        }
        System.out.println(user.getUsername());
        System.out.println(user.getPassword());
        System.out.println(user.getBirthday());
        user.setId("1");
        return user;
    }

测试用例

@RunWith(SpringRunner.class) //运行器
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void stup(){
        mockMvc= MockMvcBuilders.webAppContextSetup(wac).build();
    }

    //用户修改用例
    @Test
    public void whenUpdateSuccess() throws Exception {
        //当前时间加一年
        Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
        String content = "{\"id\":\"1\",\"username\":\"44\",\"password\":null,\"birthday\":" + date.getTime() + "}";
        String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                //判断请求的状态吗是否成功,200
                .andExpect(MockMvcResultMatchers.status().isOk())
                //判断返回到username是不是tom
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
                .andReturn().getResponse().getContentAsString();
        //打印返回结果
        System.out.println(result);
    }

自定义注解

image.png

MyConstraint类

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//作用在字段跟方法上面
@Target({ElementType.FIELD,ElementType.METHOD})
//运行时注解
@Retention(RetentionPolicy.RUNTIME)
//需要校验注解的类
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
    String message() default "{org.hibernate.validator.constraints.NotBlank.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

MyConstraintValidator类

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

//范型1.验证的注解 2.验证的数据类型
public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> {

    @Override
    public void initialize(MyConstraint myConstraint) {
        //校验器初始化的规则
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        //校验username如果是tom验证通过
        if (value.equals("tom")){
            return true;
        }else{
            return false;
        }

    }
}

删除请求

Controller类

    @RequestMapping(value = "/{id:\\d+}",method = RequestMethod.DELETE)
    //@Valid启用校验password不允许为空
    public void deleteUser(@PathVariable String id){
        System.out.println(id);
    }

测试用例

@RunWith(SpringRunner.class) //运行器
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void stup(){
        mockMvc= MockMvcBuilders.webAppContextSetup(wac).build();
    }

    //用户删除用例
    @Test
    public void whenDeleteSuccess() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.delete("/user/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                //判断请求的状态吗是否成功,200
                .andExpect(MockMvcResultMatchers.status().isOk());

    }

服务异常处理

把BindingResult errors去掉

    @RequestMapping(method = RequestMethod.POST)
    @JsonView(User.UserSimpleView.class)
    //@Valid启用校验password不允许为空
    public User createUser(@Valid @RequestBody User user){
        //如果校验有错误是true并打印错误信息
//        if(errors.hasErrors()){
//            errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
//        }
        System.out.println(user.getUsername());
        System.out.println(user.getPassword());
        System.out.println(user.getBirthday());
        user.setId("1");
        return user;
    }

查看返回的异常信息


image.png

处理状态码错误

创建文件结构如下404错误将跳转对应页面


image.png

RESTful API的拦截

过滤器(Filter)

image.png

创建filter文件

@Component
public class TimeFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("TimeFilter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("TimeFilter doFilter");
        long start=new Date().getTime();
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("耗时"+(new Date().getTime()-start));
    }

    @Override
    public void destroy() {
        System.out.println("TimeFilter destroy");
    }
}

自定义filter

需要吧filter文件@Component标签去除


image.png
@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean timeFilterRegistration(){
        FilterRegistrationBean registration=new FilterRegistrationBean();
        TimeFilter timeFilter=new TimeFilter();
        registration.setFilter(timeFilter);
        //filter作用的地址
        List<String>urls=new ArrayList<>();
        urls.add("/user");
        registration.setUrlPatterns(urls);
        return  registration;
    }
}

拦截器(Interceptor)

image.png

创建Interceptor文件

@Component
public class TimeInterceptor implements HandlerInterceptor {

    //控制器方法调用之前
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        System.out.println("preHandle");
        System.out.println("进入方法"+((HandlerMethod)o).getMethod().getName());
        httpServletRequest.setAttribute("startTime",new Date().getTime());
        //是否调用后面的方法调用是true
        return true;
    }
    //控制器方法被调用
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
        Long start= (Long) httpServletRequest.getAttribute("startTime");
        System.out.println("time interceptor耗时"+(new Date().getTime()-start));
    }
    //控制器方法完成之后
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        System.out.println("afterCompletion");
        System.out.println("exception is"+e);
    }
}

把过滤器添加到webconfig文件


image.png
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private TimeInterceptor timeInterceptor;

    //过滤器
    @Bean
    public FilterRegistrationBean timeFilterRegistration(){
        FilterRegistrationBean registration=new FilterRegistrationBean();
        TimeFilter timeFilter=new TimeFilter();
        registration.setFilter(timeFilter);
        //filter作用的地址
        List<String>urls=new ArrayList<>();
        urls.add("/user/*");
        registration.setUrlPatterns(urls);
        return  registration;
    }

    //拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(timeInterceptor);
    }
}

切片(Aspect)

image.png
@Aspect
@Component
public class TimeAspect {

    //@Befor方法调用之前
    //@After()方法调用
    //@AfterThrowing方法调用之后
    //包围,覆盖前面三种
    @Around("execution(* com.guosh.web.controller.UserController.*(..))")//表达式表示usercontroller里所有方法其他表达式可以查询切片表达式
    public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("time aspect start");

        //可以获取到传入参数
        Object[]args=pjp.getArgs();
        for (Object arg: args) {
            System.out.println("arg is"+arg);
        }

        long start=new Date().getTime();
        //相当于filter里doFilter方法
        Object object=pjp.proceed();
        System.out.println("time aspect耗时"+(new Date().getTime()-start));

        System.out.println("time aspect end");
        return object;
    }
}

总结

过滤器Filter :可以拿到原始的http请求与响应信息
拦截器Interceptor :可以拿到原始的http请求与响应信息还可以拿到处理请求方法的信息
切片Aspect :可以拿到方法调用传过来的值

使用rest方式处理文件服务

返回的上传文件后路径对象
在application.yml里添加上传地址

#上传文件路径
uploadfiledir:
  filePath: /Users/shaohua/webapp/guoshsecurity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FileInfo {
    private String path;
}

@RestController
@RequestMapping("/file")
public class FileController {

    @Value("${uploadfiledir.filePath}")
    private String fileDataStorePath;//文件上传地址

    @RequestMapping(method = RequestMethod.POST)
    public FileInfo upload(@RequestParam("file") MultipartFile file) throws IOException {
        //文件名
        System.out.println(file.getOriginalFilename());
        //文件大小
        System.out.println(file.getSize());
        //获取文件后缀名
        String ext=StringUtils.getFilenameExtension(file.getOriginalFilename());

        File fileDir = new File(fileDataStorePath);
        //判断是否创建目录
        if (!fileDir.exists()) {
            if (!fileDir.mkdirs() || !fileDir.exists()) { // 创建目录失败
                throw new RuntimeException("无法创建目录!");
            }
        }

        File localFile=new File(fileDataStorePath, UUID.randomUUID().toString().replace("-", "")+"."+ext);
        file.transferTo(localFile);
        //返回上传的路径地址
        return new FileInfo(localFile.getAbsolutePath());

    }
    //下载文件
    @RequestMapping(value ="/{id}" ,method = RequestMethod.GET)
    public void download(@PathVariable String id, HttpServletResponse response){
      //模拟下载直接填好了下载文件名称
        try(InputStream inputStream = new FileInputStream(new File(fileDataStorePath,"13a2c075b7f44025bbb3c590f7f372eb.txt"));
            OutputStream outputStream=response.getOutputStream();){
            response.setContentType("application/x-download");
            response.addHeader("Content-Disposition","attachment;filename="+"13a2c075b7f44025bbb3c590f7f372eb.txt\"");
            IOUtils.copy(inputStream,outputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

使用Swagger工具

在demo模块引入

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

添加swagger的配置类


image.png

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Value("${sys.swagger.enable-swgger}")
    private Boolean enableSwgger;
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enableSwgger)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.guosh.web"))  //swgger插件作用范围
                 //.paths(PathSelectors.regex("/api/.*"))
                .paths(PathSelectors.any()) //过滤接口
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SpringSecurityDemo") //标题
                .description("API描述") //描述
                .contact(new Contact("guoshaohua", "http://www.guoshaohua.cn", ""))//作者
                .version("1.0")
                .build();
    }
}

常用注解

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

推荐阅读更多精彩内容