shiro原理之过滤器

Shiro原理-过滤器

前言

这几天一直在研究Shiro到底是如何工作的,即一个请求过来了,它是如何做到知道这个请求应该用什么方式来鉴权的?应该调用哪个过滤器?自己定义的过滤器该如何才能生效?

带着这样的疑问,我做了一些测试与研究,并记录于此文。

实现原理

Shiro对于请求的鉴权的实现也是通过过滤器(或者说是拦截器)来实现的,但是Spring项目中有拦截链机制,会有多个拦截器生效,包括系统内置的以及Shiro注入的,所以需要搞懂他的过滤的实现机制就需要去弄明白这些过滤器是如何过滤的。

那就开始吧

ApplicationFilterChain 简介

Tomcat的类ApplicationFilterChain是一个Java Servlet API规范javax.servlet.FilterChain的实现,用于管理某个请求request的一组过滤器Filter的执行。当针对一个request所定义的一组过滤器Filter处理完该请求后,最后一个doFilter()调用才会执行目标Servlet的方法service(),然后响应对象response会按照相反的顺序依次被这些Filter处理,最终到达客户端。

ApplicationFilterChaindoFilter方法下打上断点

  // org.apache.catalina.core.ApplicationFilterChain.java
  
  // 执行过滤器链中的下一个过滤器Filter。如果链中所有过滤器都执行过,则调用servlet的service()方法。
  public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
      // 这个if-else分支主要是根据Globals.IS_SECURITY_ENABLED是true还是false决定
        //      如何调用目标逻辑,但两种情况下,目标逻辑最终都是 internalDoFilter(req,res)    
        if (Globals.IS_SECURITY_ENABLED) {
              final ServletRequest req = request;
              final ServletResponse res = response;
  
              try {
                  AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                      public Void run() throws ServletException, IOException {
                         // 调用internalDoFilter
                          ApplicationFilterChain.this.internalDoFilter(req, res);
                          return null;
                      }
                  });
              } catch (PrivilegedActionException var7) {
                  Exception e = var7.getException();
                  if (e instanceof ServletException) {
                      throw (ServletException)e;
                  }
  
                  if (e instanceof IOException) {
                      throw (IOException)e;
                  }
  
                  if (e instanceof RuntimeException) {
                      throw (RuntimeException)e;
                  }
  
                  throw new ServletException(e.getMessage(), e);
              }
          } else {
              // 调用internalDoFilter
              this.internalDoFilter(request, response);
          }
  
      }
filters

我们可以看到 filters中包含了5个过滤器:

CharacterEncodingFilter:spring内置过滤器,用来指定请求或者响应的编码格式。

FormContentFilter:该过滤器针对DELETE,PUTPATCH这三种HTTP method分析其FORM表单参数,将其暴露为Servlet请求参数。

RequestContextFilter:该过滤器将当前请求暴露到当前线程。

SpringShiroFilter:shiro内置过滤器,包装 Request 和 Response,使它们由原来的 HttpServlet 系列包装为 ShiroHttpServletRequest等。

Tomcat WebSocket Filter:webSocket 相关过滤器。

这些注入的过滤器会通过internalDoFilter来执行过滤工作,如下:

// org.apache.catalina.core.ApplicationFilterChain.java
  private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
      if (this.pos < this.n) {
         // 如果过滤链中还有过滤器需要过滤
        
          ApplicationFilterConfig filterConfig = this.filters[this.pos++]; 
          try {
             // 找到目标的Filter
              Filter filter = filterConfig.getFilter();
              if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                  request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
              }
              // 执行目标 Filter 对象的 doFilter方法,
              // 注意,这里当前ApplicationFilterChain对象被传递到了目标
              // Filter对象的doFilter方法,而目标Filter对象的doFilter在执行完自己
              // 被指定的逻辑之后会反过来调用这个ApplicationFilterChain对象的
              // doFilter方法,只是pos向前推进了一个过滤器。这个ApplicationFilterChain
              // 和Filter之间反复调用彼此doFilter方法的过程一直持续直到当前链发现所有的
              // Filter都已经被执行
              if (Globals.IS_SECURITY_ENABLED) {
                  Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                  Object[] args = new Object[]{request, response, this};
                  SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
              } else {
                  filter.doFilter(request, response, this);
              }
  
          } catch (ServletException | RuntimeException | IOException var15) {
              throw var15;
          } catch (Throwable var16) {
              Throwable e = ExceptionUtils.unwrapInvocationTargetException(var16);
              ExceptionUtils.handleThrowable(e);
              throw new ServletException(sm.getString("filterChain.filter"), e);
          }
      } else {
         // 调用servlet的service()方法
        
          // We fell off the end of the chain -- call the servlet instance
          // 这里是过滤器链中所有的过滤器都已经被执行的情况,现在需要调用servlet实例本身了。
          // !!! 注意 : 虽然这里开始调用servlet实例了,但是从当前方法执行堆栈可以看出,过滤器链
          // 和链中过滤器的doFilter方法的执行帧还在堆栈中并未退出,他们会在servlet实例的逻辑
          // 执行完后,分别执行完自己剩余的的逻辑才会逐一结束。
          try {
              if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                  lastServicedRequest.set(request);
                  lastServicedResponse.set(response);
              }
  
              if (request.isAsyncSupported() && !this.servletSupportsAsync) {
                  request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
              }
  
              if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
                  Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                  Object[] args = new Object[]{request, response};
                  SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
              } else {
                  this.servlet.service(request, response);
              }
          } catch (ServletException | RuntimeException | IOException var17) {
              throw var17;
          } catch (Throwable var18) {
              Throwable e = ExceptionUtils.unwrapInvocationTargetException(var18);
              ExceptionUtils.handleThrowable(e);
              throw new ServletException(sm.getString("filterChain.servlet"), e);
          } finally {
              if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                  lastServicedRequest.set((Object)null);
                  lastServicedResponse.set((Object)null);
              }
  
          }
  
      }
  }
责任链原理图

Shiro Filter 注册

Shiro 中对请求的配置需要在Shiro的配置文件中配置的,可以在这里添加自定义的过滤器以及配置相关的URL过滤信息,而ShiroFilter是在ShiroFilterFactoryBean中创建的,所以我们首先需要配置注入好ShiroFilterFactoryBean。

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
      
        
        // 自定义过滤器
        Map<String, Filter> filterMap = shiroFilterFactoryBean.getFilters();
        filterMap.put("hasToken", accessTokenFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
      
        
        /**
         * anon:匿名用户可访问
         * authc:认证用户可访问ShiroFilterFactoryBean
         * user:使用rememberMe可访问
         * perms:对应权限可访问
         * role:对应角色权限可访问
         **/
        // URL的过滤
        Map<String, String> filterChainMap = new LinkedHashMap<>();
        // 登录接口开放
        filterChainMap.put("/auth/login", "anon");
        // 获取用户信息需要认证用户
        filterChainMap.put("/user/**", "authc");

        ... 
          
        bean.setFilterChainDefinitionMap(filterChainMap);
        return bean;
    }

Filter注入

默认过滤器

public enum DefaultFilter {
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    authcBearer(BearerHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
    ...
}
Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
// DefaultFilterChainManager
// 加载默认过滤器
protected void addDefaultFilters(boolean init) {
  DefaultFilter[] var2 = DefaultFilter.values();
  int var3 = var2.length;

  for(int var4 = 0; var4 < var3; ++var4) {
    DefaultFilter defaultFilter = var2[var4];
    this.addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
  }

}
        // org.apache.shiro.spring.web.ShiroFilterFactoryBean
    // 创建实体的方法中调用了加载过滤器链的方法createFilterChainManager
        protected AbstractShiroFilter createInstance() throws Exception {
      log.debug("Creating Shiro Filter instance.");
      SecurityManager securityManager = this.getSecurityManager();
      String msg;
      if (securityManager == null) {
        msg = "SecurityManager property must be set.";
        throw new BeanInitializationException(msg);
      } else if (!(securityManager instanceof WebSecurityManager)) {
        msg = "The security manager does not implement the WebSecurityManager interface.";
        throw new BeanInitializationException(msg);
      } else {
        FilterChainManager manager = this.createFilterChainManager();
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);
        return new ShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver);
      }
    }
        
      // 匿名内部类 SpringShiroFilter
        private static final class SpringShiroFilter extends AbstractShiroFilter {
        protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            } else {
                this.setSecurityManager(webSecurityManager);
                if (resolver != null) {
                    this.setFilterChainResolver(resolver);
                }

            }
        }
    }
   
   // 创建过滤器链
   protected FilterChainManager createFilterChainManager() {
        // 创建DefaultFilterChainManager
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
            // 先获取默认的过滤器
        Map<String, Filter> defaultFilters = manager.getFilters();
        Iterator var3 = defaultFilters.values().iterator();
                
        // 对每个默认的过滤器执行applyGlobalPropertiesIfNecessary方法
            // applyGlobalPropertiesIfNecessary的作用:
            //          -  设置customAuthenticationFilter中的loginUrl,SuccessUrl和unauthorizedUrl。
        while(var3.hasNext()) {
            Filter filter = (Filter)var3.next();
            this.applyGlobalPropertiesIfNecessary(filter);
        }

        Map<String, Filter> filters = this.getFilters();
        String name;
        Filter filter;
        // 加载自定义的过滤器,进行设置并添加到过滤器管理器DefaultFilterChainManager中
            // 如果不为空的话也要执行applyGlobalPropertiesIfNecessary方法
        if (!CollectionUtils.isEmpty(filters)) {
            for(Iterator var10 = filters.entrySet().iterator(); var10.hasNext(); manager.addFilter(name, filter, false)) {
                Entry<String, Filter> entry = (Entry)var10.next();
                name = (String)entry.getKey();
                filter = (Filter)entry.getValue();
                this.applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable)filter).setName(name);
                }
            }
        }
                // 加载URL的过滤,并调用createChain方法构造过滤链
        Map<String, String> chains = this.getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            Iterator var12 = chains.entrySet().iterator();

            while(var12.hasNext()) {
                Entry<String, String> entry = (Entry)var12.next();
                String url = (String)entry.getKey();
                String chainDefinition = (String)entry.getValue();
                manager.createChain(url, chainDefinition);
            }
        }

        return manager;
    }
        
    // 构造过滤链,通过URL过滤规则
        public void createChain(String chainName, String chainDefinition) {
        if (!StringUtils.hasText(chainName)) {
            throw new NullPointerException("chainName cannot be null or empty.");
        } else if (!StringUtils.hasText(chainDefinition)) {
            throw new NullPointerException("chainDefinition cannot be null or empty.");
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Creating chain [" + chainName + "] from String definition [" + chainDefinition + "]");
            }
                        // 如果指定了多个过滤器
            String[] filterTokens = this.splitChainDefinition(chainDefinition);
            String[] var4 = filterTokens;
            int var5 = filterTokens.length;
                        // 将所有过滤器加到过滤链中去
            for(int var6 = 0; var6 < var5; ++var6) {
                String token = var4[var6];
                String[] nameConfigPair = this.toNameConfigPair(token);
                this.addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
            }

        }
    }
        // 添加到过滤链中去
    public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
            if (!StringUtils.hasText(chainName)) {
                throw new IllegalArgumentException("chainName cannot be null or empty.");
            } else {
                // 根据过滤器的名字找到过滤器
                Filter filter = this.getFilter(filterName);
                if (filter == null) {
                  // 如果不存在就抛出异常
                    throw new IllegalArgumentException("There is no filter with name '" + filterName + "' to apply to chain [" + chainName + "] in the pool of available Filters.  Ensure a filter with that name/path has first been registered with the addFilter method(s).");
                } else {
                    this.applyChainConfig(chainName, filter, chainSpecificFilterConfig);
                    NamedFilterList chain = this.ensureChain(chainName);
                    chain.add(filter);
                }
            }
        }

Shiro Filter 匹配

刚刚我们分析Spring Shiro的Shiro注入的时候,我们可以看到createInstance方法返回的是AbstractShiroFilter的子类SpringShiroFilter,而AbstractShiroFilter也是OncePerRequestFilter的子类,我们可以看看继承图:

SpringShiroFilter继承图

我们来看看AbstractShiroFilter的部分源码:

  // org.apache.shiro.web.servlet.AbstractShiroFilter.java
        
        // OncePerRequestFilter 执行 doFilter 方法时调用了该方法
        protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
    Throwable t = null;

    try {
      final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
      final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
      Subject subject = this.createSubject(request, response);
      // 执行该方法自动将subject绑定到线程的subject中
      subject.execute(new Callable() {
        public Object call() throws Exception {
          // 更新session相关信息
          AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
          // 执行过滤链
          AbstractShiroFilter.this.executeChain(request, response, chain);
          return null;
        }
      });
    } catch (ExecutionException var8) {
      t = var8.getCause();
    } catch (Throwable var9) {
      t = var9;
    }

    if (t != null) {
      if (t instanceof ServletException) {
        throw (ServletException)t;
      } else if (t instanceof IOException) {
        throw (IOException)t;
      } else {
        String msg = "Filtered request failed.";
        throw new ServletException(msg, t);
      }
    }
  }
    
    // 执行过滤链,
  protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException {
      // 获取需要执行的过滤链
      FilterChain chain = this.getExecutionChain(request, response, origChain);
      chain.doFilter(request, response);
    }
    // 获取需要执行的过滤链
  protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
    FilterChain chain = origChain;
    // 获取解析器,这里获取到的是 PathMatchingFilterChainResolver
    FilterChainResolver resolver = this.getFilterChainResolver();
    if (resolver == null) {
      log.debug("No FilterChainResolver configured.  Returning original FilterChain.");
      return origChain;
    } else {
      // 获取过滤链 就是在这里通过请求的url选择了相应的过滤链的
      FilterChain resolved = resolver.getChain(request, response, origChain);
      if (resolved != null) {
        log.trace("Resolved a configured FilterChain for the current request.");
        chain = resolved;
      } else {
        log.trace("No FilterChain configured for the current request.  Using the default.");
      }

      return chain;
    }
  }  
   // org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver
        public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        // 获取Manager
        FilterChainManager filterChainManager = this.getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        } else {
            // 接下来就是先获取请求的url,然后去匹配过滤链,然后再返回。
            String requestURI = this.getPathWithinApplication(request);
            if (requestURI != null && !"/".equals(requestURI) && requestURI.endsWith("/")) {
                requestURI = requestURI.substring(0, requestURI.length() - 1);
            }

            Iterator var6 = filterChainManager.getChainNames().iterator();
                        // 遍历匹配
            String pathPattern;
            do {
                if (!var6.hasNext()) {
                    return null;
                }

                pathPattern = (String)var6.next();
                if (pathPattern != null && !"/".equals(pathPattern) && pathPattern.endsWith("/")) {
                    pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
                }
            } while(!this.pathMatches(pathPattern, requestURI));

            if (log.isTraceEnabled()) {
                log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + Encode.forHtml(requestURI) + "].  Utilizing corresponding filter chain...");
            }

            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }
PathMatchingFilterChainResolver

返回的是一个ProxiedFilterChain的实例,该实例包含了PathMatchingFilterChainResolver所匹配出来的过滤器,如下图,匹配的是系统内置的名为anon过滤器,至于它所对应的过滤器是什么可以见上面默认过滤器的表。

getExecutionChain结果

接着,在AbstractShiroFilter中的executeChain就会执行它的doFilter方法。

        // org.apache.shiro.web.servlet.ProxiedFilterChain
        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        // 执行所代理的过滤器的doFilter方法。
        if (this.filters != null && this.filters.size() != this.index) {
            if (log.isTraceEnabled()) {
                log.trace("Invoking wrapped filter at index [" + this.index + "]");
            }

            ((Filter)this.filters.get(this.index++)).doFilter(request, response, this);
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Invoking original filter chain.");
            }

            this.orig.doFilter(request, response);
        } 
    }

总结

刚开始学习看源码,IDEA的调试工具也不是很会用,好在确实IDEA很强大,不然这么多过滤链跳来跳去是真的难懂。经过这次的阅读以及网上的博客的研究,Shiro的过滤链如果有自定义的过滤链的话,一定不能像平常的拦截器那样注入,必须要在注入ShiroFilterFactoryBean时使用如下方式注入才能生效。

Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt",new CustomAuthenticationFilter());
bean.setFilters(filterMap);
bean.setSecurityManager(securityManager);

因为很可能像平常注入过滤器那样注入先后顺序可能会存在问题。

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