Spring Security整合JWT,实现单点登录,So Easy~!

前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况下,实现了登陆登出的功能,亮点就在于以JSON的形式接收返回参数。这个是针对单个后台服务的, 登录信息都存储在SecurityContextHolder缓存里。如果是两个或两个以上的应用呢,那该怎么办?Session是不能用了,Cookie自然也不能用,毕竟它俩是一对的。

曾想过用OAuth2来解决这个问题,但是OAuth2太复杂,首先理解概念就需要花费一些时间,而且里面的授权服务器、资源服务器、客户端等等让人傻傻分不清,还有四种授权模式,要反复衡量,到底要用哪一种,概念还没有扯清楚就开始纠结使用哪一个了。从概念入手不是个好主意,也不是个轻松的主意。在理解OAuth2的过程中,想到自己的项目是前后端分离的,离不开JSON,无意中遇见JWT。JWT是什么玩意,咦,难道是自己苦苦寻求的吗?!

那么,什么是JWT呢?看看专家介绍 阮一峰的网络日志,才知道,JWT 是JSON Web Token的简称,它解决的就是跨域问题。看来,要找的就是它,简单的,但也是管用的。

继续深究,JWT到底是怎样和SpringSecurity结合的呢。下面上代码,在上代码前先说明一下,在本次实例中,涉及到两个项目,一个项目是登录的项目A,另一个项目是根据token进行访问的项目B。其中B项目没有登录,也不会涉及登录,只要有Token就可以访问,Token失效了就访问不了了。

A项目是登录的项目,也是一个只能通过登录进行访问的后台服务。B项目就是一个服务,只要用户在A项目登录了,就可以访问。


设计图-1

A项目配置,代码如下

第一步,A项目 POM.xml 引入文件
 <!-- spring-security 和 jwt 引入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>            
            <groupId>io.jsonwebtoken</groupId>            
            <artifactId>jjwt</artifactId>            
            <version>0.9.0</version>        
        </dependency>
第二步,A项目SecurityConfig配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.example.demo.filter.JWTAuthenticationFilter;
import com.example.demo.filter.JWTLoginFilter;

/**
 * SpringSecurity的配置
 * 参考网址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
 * @author 程就人生
 * @date 2019年5月26日
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private UserDetailsService myCustomUserService;

    @Autowired
    private MyPasswordEncoder myPasswordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //关闭跨站请求防护
            .cors().and().csrf().disable()
            //允许不登陆就可以访问的方法,多个用逗号分隔
            .authorizeRequests().antMatchers("/test").permitAll()
            //其他的需要授权后访问
            .anyRequest().authenticated()
            
            .and()         
            //增加登录拦截
            .addFilter(new JWTLoginFilter(authenticationManager()))     
            //增加是否登陸过滤
            .addFilter(new JWTAuthenticationFilter(authenticationManager()))
            // 前后端分离是无状态的,所以暫時不用session,將登陆信息保存在token中。
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        

    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //覆盖UserDetailsService类
        auth.userDetailsService(myCustomUserService)
        //覆盖默认的密码验证类
        .passwordEncoder(myPasswordEncoder);
    }
}
第三步,实现配置文件中自定义的类
  1. MyPasswordEncoder类实现了默认的PasswordEncoder 接口,可以对密码加密和密码对比进行个性化定制
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

 /**
 * 自定义的密码加密方法,实现了PasswordEncoder接口
 * @author 程就人生
 * @date 2019年5月26日
 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        //加密方法可以根据自己的需要修改
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return encode(charSequence).equals(s);
    }
}
  1. MyCustomUserService 实现了框架默认的UserDetailsService,可以根据username从数据库获取用户,查看用户是否存在
/**
 * 登录专用类,用户登陆时,通过这里查询数据库
 * 自定义类,实现了UserDetailsService接口,用户登录时调用的第一类
 * @author 程就人生
 * @date 2019年5月26日
 */
@Component
public class MyCustomUserService implements UserDetailsService {

    /**
     * 登陆验证时,通过username获取用户的所有权限信息
     * 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //在这里可以自己调用数据库,对username进行查询,看看在数据库中是否存在
        MyUserDetails myUserDetail = new MyUserDetails();
        myUserDetail.setUsername(username);
        myUserDetail.setPassword("123456");
        return myUserDetail;
    }
}
  1. MyUserDetails 实现了框架的UserDetails接口,可以在该类中根据需要添加自己必需的属性
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * 实现了UserDetails接口,只留必需的属性,也可添加自己需要的属性
 * @author 程就人生
 * @date 2019年5月26日
 */
public class MyUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    //登录用户名
    private String username;
    //登录密码
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

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

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

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

    @Override
    public String getUsername() {
        return this.username;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. JWTLoginFilter 实现了框架自带的UsernamePasswordAuthenticationFilter 接口,对拦截做处理,以便登录成功后,在头部设置token返回;不管登录成功还是失败,都有JSON数据返回
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.example.demo.entity.User;
import com.example.demo.security.MyUserDetails;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; 

/**
 * 验证用户名密码正确后,生成一个token,放在header里,返回给客户端 
 * @author 程就人生
 * @date 2019年5月26日
 */
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter { 
    
    private AuthenticationManager authenticationManager;     
    
    public JWTLoginFilter(AuthenticationManager authenticationManager) { 
        
        this.authenticationManager = authenticationManager;    
        
    } 
    
    /**
     * 接收并解析用户凭证,出現错误时,返回json数据前端
     */
    @Override    
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res){        
        try {            
            User user =new ObjectMapper().readValue(req.getInputStream(), User.class);             
            return authenticationManager.authenticate(                    
                    new UsernamePasswordAuthenticationToken(                            
                            user.getUsername(),                            
                            user.getPassword(),                            
                            new ArrayList<>())            
                    );        
            } catch (Exception e) {
                try {
                    //未登錄出現賬號或密碼錯誤時,使用json進行提示
                    res.setContentType("application/json;charset=utf-8");
                    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    PrintWriter out = res.getWriter();                  
                    Map<String,Object> map = new HashMap<String,Object>();
                    map.put("code",HttpServletResponse.SC_UNAUTHORIZED);
                    map.put("message","账号或密码错误!");
                    out.write(new ObjectMapper().writeValueAsString(map));
                    out.flush();
                    out.close();
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
                throw new RuntimeException(e);        
            }    
    }
    
    /**
     * 用户登录成功后,生成token,并且返回json数据给前端
     */
    @Override    
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth){         
        
        //json web token构建
        String token = Jwts.builder()    
                //此处为自定义的、实现org.springframework.security.core.userdetails.UserDetails的类,需要和配置中设置的保持一致
                //此处的subject可以用一个用户名,也可以是多个信息的组合,根据需要来定
                .setSubject(((MyUserDetails) auth.getPrincipal()).getUsername())    
                //设置token过期时间,24小時
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000)) 
                
                //设置token签名、密钥
                .signWith(SignatureAlgorithm.HS512, "MyJwtSecret")       
                
                .compact();  
        
        //返回token
        res.addHeader("Authorization", "Bearer " + token); 
        
        try {
            //登录成功時,返回json格式进行提示
            res.setContentType("application/json;charset=utf-8");
            res.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = res.getWriter();                  
            Map<String,Object> map = new HashMap<String,Object>();
            map.put("code",HttpServletResponse.SC_OK);
            map.put("message","登陆成功!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
}
  1. JWTAuthenticationFilter 类实现了BasicAuthenticationFilter 接口,对Controller中需要登录后才能访问的方法进行了拦截,没有登录,则不能访问,返回JSON信息进行提示
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Jwts;

/**
 * 是否登陆验证方法
 * @author 程就人生
 * @date 2019年5月26日
 */
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
    
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * 對請求進行過濾
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {        
        try {
            //请求体的头中是否包含Authorization
            String header = request.getHeader("Authorization");     
            //Authorization中是否包含Bearer,有一个不包含时直接返回
            if (header == null || !header.startsWith("Bearer ")) {
                chain.doFilter(request, response);
                responseJson(response);
                return;        
            } 
            //获取权限失败,会抛出异常
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request); 
            //获取后,将Authentication写入SecurityContextHolder中供后序使用
            SecurityContextHolder.getContext().setAuthentication(authentication); 
            chain.doFilter(request, response);
        } catch (Exception e) {
            responseJson(response);
            e.printStackTrace();
        }     
    }

    /**
     * 未登錄時的提示
     * @param response
     */
    private void responseJson(HttpServletResponse response){
        try {
            //未登錄時,使用json進行提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();                 
            Map<String,Object> map = new HashMap<String,Object>();
            map.put("code",HttpServletResponse.SC_FORBIDDEN);
            map.put("message","请登录!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
    
    /**
     * 通过token,获取用户信息
     * @param request
     * @return
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {        
        String token = request.getHeader("Authorization");        
        if (token != null) {            
            //通过token解析出用户信息            
            String user = Jwts.parser()   
                    //签名、密钥
                    .setSigningKey("MyJwtSecret")                    
                    .parseClaimsJws(token.replace("Bearer ", ""))                    
                    .getBody()                    
                    .getSubject();     
            //不为null,返回
            if (user != null) {                
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());            
            }           
            return null;        
        }        
        return null;    
    } 
        
}
  1. 在登录过滤器中接收参数的实体类,也可以直接接收,这一个类不是必须的
public class User {
    
    private long id;
    private String username;
    private String password;

    public long getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

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

B项目的配置

第一步,在pom中引入必须的架包
<!-- spring-security 和 jwt 引入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>            
            <groupId>io.jsonwebtoken</groupId>            
            <artifactId>jjwt</artifactId>            
            <version>0.9.0</version>        
        </dependency>
第二步,增加SecurityConfig配置文件
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

import com.example.demo.filter.JWTAuthenticationFilter;

/**
 * SpringSecurity的配置
 * 参考网址:https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
 * @author 程就人生
 * @date 2019年5月26日
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //关闭跨站请求防护
            .cors().and().csrf().disable()
            //允许不登陆就可以访问的方法,多个用逗号分隔
            .authorizeRequests()
            //其他的需要授权后访问
            .anyRequest().authenticated()
            
            .and()
            //增加是否登陸过滤
            .addFilter(new JWTAuthenticationFilter(authenticationManager()))
            // 前后端分离是无状态的,所以暫時不用session,將登陆信息保存在token中。
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    }
    
}
第三步,在增加对方法是否登录进行拦截的过滤器
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.Jwts;

/**
 * 是否登陆验证方法
 * @author 程就人生
 * @date 2019年5月26日
 */
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
    
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * 對請求進行過濾
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {        
        try {
            //请求体的头中是否包含Authorization
            String header = request.getHeader("Authorization");     
            //Authorization中是否包含Bearer,有一个不包含时直接返回
            if (header == null || !header.startsWith("Bearer ")) {
                chain.doFilter(request, response);
                responseJson(response);
                return;        
            } 
            //获取权限失败,会抛出异常
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request); 
            //获取后,将Authentication写入SecurityContextHolder中供后序使用
            SecurityContextHolder.getContext().setAuthentication(authentication); 
            chain.doFilter(request, response);
        } catch (Exception e) {
            responseJson(response);
            e.printStackTrace();
        }     
    }

    /**
     * 未登錄時的提示
     * @param response
     */
    private void responseJson(HttpServletResponse response){
        try {
            //未登錄時,使用json進行提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();                 
            Map<String,Object> map = new HashMap<String,Object>();
            map.put("code",HttpServletResponse.SC_FORBIDDEN);
            map.put("message","请登录!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
    
    /**
     * 通过token,获取用户信息
     * @param request
     * @return
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {        
        String token = request.getHeader("Authorization");        
        if (token != null) {            
            //通过token解析出用户信息            
            String user = Jwts.parser()   
                    //签名盐
                    .setSigningKey("MyJwtSecret")                    
                    .parseClaimsJws(token.replace("Bearer ", ""))                    
                    .getBody()                    
                    .getSubject();     
            //不为null,返回
            if (user != null) {                
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());            
            }           
            return null;        
        }        
        return null;    
    } 
        
}

从B项目的配置中,可以看出,B项目配置的太简洁了,只需要拦截一下没有登录的请求,连登录也都省了。

A和B项目中分别添加一个Controller,用于测试

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

/**
 * 测试用例
 * @author 程就人生
 * @date 2019年5月26日
 */
@RestController
public class IndexController {

    @GetMapping("/index")
    public Object index(){
        
        return "index";
    }
}

使用测试工具进行测试

第一步,测试A项目和B项目的index是否能访问,结果都不能访问,测试结果OK
测试结果-1

测试结果-2
第二步,通过登录获取token,登录成功后,返回了JSON格式的提示,返回的token在头部,点击响应头,获取token
测试结果-3

测试结果-4
第三步,将token拷贝至A项目index的头部,B项目index的头部,测试结果ok,都可以访问,也可以把token时间设置的短一些,测试一下token过期了,是否还能访问。
测试结果-5

测试结果-6

最后,感觉一下Token的结构,去掉前面固定的Bearer ,后面的分成三个部分,中间用点隔开,这个就简单了解下吧。

  • Header(头部)
  • Payload(负载)
  • Signature(签名)
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmZW5nIiwiZXhwIjoxNTU4OTUxMjM5fQ.X7lOhHJljxnVcNEckYSX22rgTDN0ToRJLaPb_1dAoPzx6q_eN5B5iOxO2GXoNUllIfQG6SrdJhgYzKZPTMsDIg

Spring Security整合JWT,实现单点登录的功能,到此就告一段落了,看起来是不是很简单呢,那就动手试一试吧。

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

推荐阅读更多精彩内容