SpringSecurity源碼整體解析

核心思想

spring對請求的處理過程如下:


image.png

而security所有認(rèn)證邏輯作為特殊的一個Filter加入到spring處理servelet的過濾鏈中,即FilterChainProxy;
而這個FilterChainProxy內(nèi)部又有多個過濾器鏈FilterChain打却,每個鏈有matcher衷快,用于匹配請求帚桩,請求來時選擇最先匹配的一條過濾器鏈做權(quán)限認(rèn)證等龙,每條過濾器鏈又由多個多濾器Filter依序連接而成闽坡。如下圖:


image.png

編程范式

configuator配置類裝配到builder類中,builder類借助confuguator類構(gòu)造filter或者filterChain
SpringSecurity有兩個重要的builder:

  • WebSecurity:構(gòu)造FilterChainProxy(由多條過濾器鏈SecurityFilterChain組成的代理)
  • HttpSecurity:構(gòu)造過濾器鏈SecurityFilterChain(DefaultSecurityFilterChain)

Spring如何加載FilterChainProxy

首先生成FilterChainProxy實例迅脐,將FilterChainProxy實例再封裝到DelegatingFilterProxy(java web的標(biāo)準(zhǔn)過濾器)芍殖,作為一個web的Filter再注冊到spring上下文。

生成FilterChainProxy實例

  1. 通過SpringBootApplication注解加載EnableAutoConfiguration注解
@SpringBootApplication
public class AuthCodeServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthCodeServerApplication.class, args);
    }
}
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class,
        attribute = "exclude"
    )
......
  1. 自動加載SecurityAutoConfiguation配置類
    在spring.factories文件中有
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
......
org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\
......
  1. 加載SpringBootWebSecurityConfiguration配置類
@Import({SpringBootWebSecurityConfiguration.class, AuthenticationManagerConfiguration.class, BootGlobalAuthenticationConfiguration.class, SecurityDataConfiguration.class})
public class SecurityAutoConfiguration {
......
  1. 加載EnableWebSecurity注解
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass({EnableWebSecurity.class, AuthenticationEntryPoint.class})
@ConditionalOnMissingBean({WebSecurityConfiguration.class})
@ConditionalOnWebApplication
@EnableWebSecurity
public class SpringBootWebSecurityConfiguration {
......
  1. 加載WebSecurityConfiguation配置類
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}
  1. 通過WebSecurityConfiguation配置類的springSecurityFilterChain方法 產(chǎn)生FilterChainProxy 實例(實例名為springSecurityFilterChain)
@Configuration
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
    private WebSecurity webSecurity;
    private Boolean debugEnabled;
    private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
    private ClassLoader beanClassLoader;
......
    @Bean(
        name = {"springSecurityFilterChain"}
    )
    public Filter springSecurityFilterChain() throws Exception {
        boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
        if (!hasConfigurers) {
            WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
            });
            this.webSecurity.apply(adapter);
        }

        return (Filter)this.webSecurity.build();
    }

至于this.webSecurity.build()內(nèi)部怎么實現(xiàn)的仪际,后面再講围小。

將FilterChainProxy實例注冊到spring

將FilterChainProxy再封裝到DelegatingFilterProxy,在注入到spring上下文树碱。有兩種方式肯适,如下:

  1. 通過SecurityFilterAutoConfiguration配置類
    @Bean
    @ConditionalOnBean(
        name = {"springSecurityFilterChain"}
    )
    public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {
        DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);
        registration.setOrder(securityProperties.getFilterOrder());
        registration.setDispatcherTypes(this.getDispatcherTypes(securityProperties));
        return registration;
    }
  1. 通過繼承實現(xiàn)AbstractSecurityWebApplicationInitializer抽象類
    在onStartUp方法里調(diào)用insertSpringSecurityFilterChain方法注入
    public final void onStartup(ServletContext servletContext) throws ServletException {
        this.beforeSpringSecurityFilterChain(servletContext);
        if (this.configurationClasses != null) {
            AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
            rootAppContext.register(this.configurationClasses);
            servletContext.addListener(new ContextLoaderListener(rootAppContext));
        }

        if (this.enableHttpSessionEventPublisher()) {
            servletContext.addListener("org.springframework.security.web.session.HttpSessionEventPublisher");
        }

        servletContext.setSessionTrackingModes(this.getSessionTrackingModes());
        this.insertSpringSecurityFilterChain(servletContext);
        this.afterSpringSecurityFilterChain(servletContext);
    }
......
    private void insertSpringSecurityFilterChain(ServletContext servletContext) {
        String filterName = "springSecurityFilterChain";
        DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName);
        String contextAttribute = this.getWebApplicationContextAttribute();
        if (contextAttribute != null) {
            springSecurityFilterChain.setContextAttribute(contextAttribute);
        }

        this.registerFilter(servletContext, true, filterName, springSecurityFilterChain);
    }

詳解FilterChainProxy實例如何產(chǎn)生

上面講到了通過調(diào)用this.webSecurity.build()方法產(chǎn)生FilterChainProxy實例,現(xiàn)在仔細(xì)分析具體怎么實現(xiàn)的成榜。

  1. 實例化并配置WebSecurity
    在WebSecurityConfiguration配置類框舔,通過setFilterChainProxySecurityConfigurer方法實例化WebSecurity,并搜索實現(xiàn)了WebSecurityConfigurer(WebSecurityConfigurerAdapter)的所有配置類赎婚,加載到WebSecurity中
    @Autowired(
        required = false
    )
    public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor, @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception {
        this.webSecurity = (WebSecurity)objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
        if (this.debugEnabled != null) {
            this.webSecurity.debug(this.debugEnabled);
        }

        Collections.sort(webSecurityConfigurers, WebSecurityConfiguration.AnnotationAwareOrderComparator.INSTANCE);
        Integer previousOrder = null;
        Object previousConfig = null;

        Iterator var5;
        SecurityConfigurer config;
        for(var5 = webSecurityConfigurers.iterator(); var5.hasNext(); previousConfig = config) {
            config = (SecurityConfigurer)var5.next();
            Integer order = WebSecurityConfiguration.AnnotationAwareOrderComparator.lookupOrder(config);
            if (previousOrder != null && previousOrder.equals(order)) {
                throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of " + order + " was already used on " + previousConfig + ", so it cannot be used on " + config + " too.");
            }

            previousOrder = order;
        }

        var5 = webSecurityConfigurers.iterator();

        while(var5.hasNext()) {
            config = (SecurityConfigurer)var5.next();
            this.webSecurity.apply(config);
        }

        this.webSecurityConfigurers = webSecurityConfigurers;
    }
  1. 調(diào)用WebSucurity 的build方法構(gòu)造FilterChainProxy實例
    見上面第5步
    最終會調(diào)到doBuild方法刘绣。
    protected final O doBuild() throws Exception {
        synchronized(this.configurers) {
            this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
            this.beforeInit();
            this.init();
            this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
            this.beforeConfigure();
            this.configure();
            this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
            O result = this.performBuild();
            this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
            return result;
        }
    }

由上可見,主要是init挣输、configure纬凤、performBuild三個方法
2.1. init(WebSecurity)
會遍歷所有之前加載好的配置類configuator(adaptor),調(diào)用其init撩嚼。

    private void init() throws Exception {
        Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
        Iterator var2 = configurers.iterator();

        SecurityConfigurer configurer;
        while(var2.hasNext()) {
            configurer = (SecurityConfigurer)var2.next();
            configurer.init(this);
        }

        var2 = this.configurersAddedInInitializing.iterator();

        while(var2.hasNext()) {
            configurer = (SecurityConfigurer)var2.next();
            configurer.init(this);
        }

    }

其中配置類的init方法停士,主要是構(gòu)造了HttpSecurity,放入到securityFilterChainBuilders完丽;并在postBuild之后設(shè)置inteceptor到websecurity恋技。可見WebSecurityConfigurerAdapter類的init方法實現(xiàn):

    public void init(final WebSecurity web) throws Exception {
        final HttpSecurity http = this.getHttp();
        web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
            public void run() {
                FilterSecurityInterceptor securityInterceptor = (FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
                web.securityInterceptor(securityInterceptor);
            }
        });
    }

2.2. configure(WebSecurity)
會遍歷所有之前加載好的配置類configuator(adaptor)逻族,調(diào)用其configure

    private void configure() throws Exception {
        Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
        Iterator var2 = configurers.iterator();

        while(var2.hasNext()) {
            SecurityConfigurer<O, B> configurer = (SecurityConfigurer)var2.next();
            configurer.configure(this);
        }
    }

配置類的configure方法丢烘,主要是配置上一步構(gòu)造好的HttpSecurity實例卖词,將其相關(guān)的configuator配置類裝配到HttpSecurity實例校哎∏鲁可見WebSecurityConfigurerAdapter類的configure方法實現(xiàn):

    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
    }

我們在定義自己的過濾器鏈的時候,可以繼承WebSecurityConfigurerAdapter重寫configure方法抠璃,比如:

protected void configure(HttpSecurity http) throws Exception {        
  http.requestMatchers() // 指定當(dāng)前`SecurityFilterChain`實例匹配哪些請求                
    .anyRequest().and()            
    .authorizeRequests() // 攔截請求长搀,創(chuàng)建FilterSecurityInterceptor                
    .anyRequest().authenticated() // 在創(chuàng)建過濾器的基礎(chǔ)上的一些自定義配置                
    .and() // 用and來表示配置過濾器結(jié)束,以便進(jìn)行下一個過濾器的創(chuàng)建和配置            
    .formLogin().and() // 設(shè)置表單登錄鸡典,創(chuàng)建UsernamePasswordAuthenticationFilter            
    .httpBasic(); // basic驗證源请,創(chuàng)建BasicAuthenticationFilter
}

2.3. performBuild(WebSecurity)
遍歷securityFilterChainBuilders(其實就是HttpSecurity)列表調(diào)用其build方法,生成SecurityFilterChain實例,最后利用多個SecurityFilterChain實例組成List谁尸,再封裝到FilterChainProxy舅踪。

    protected Filter performBuild() throws Exception {
        Assert.state(!this.securityFilterChainBuilders.isEmpty(), "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke " + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly");
        int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
        List<SecurityFilterChain> securityFilterChains = new ArrayList(chainSize);
        Iterator var3 = this.ignoredRequests.iterator();

        while(var3.hasNext()) {
            RequestMatcher ignoredRequest = (RequestMatcher)var3.next();
            securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest, new Filter[0]));
        }

        var3 = this.securityFilterChainBuilders.iterator();

        while(var3.hasNext()) {
            SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder = (SecurityBuilder)var3.next();
            securityFilterChains.add(securityFilterChainBuilder.build());
        }

        FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
        if (this.httpFirewall != null) {
            filterChainProxy.setFirewall(this.httpFirewall);
        }

        filterChainProxy.afterPropertiesSet();
        Filter result = filterChainProxy;
        if (this.debugEnabled) {
            this.logger.warn("\n\n********************************************************************\n**********        Security debugging is enabled.       *************\n**********    This may include sensitive information.  *************\n**********      Do not use in a production system!     *************\n********************************************************************\n\n");
            result = new DebugFilter(filterChainProxy);
        }

        this.postBuildAction.run();
        return (Filter)result;
    }

securityFilterChainBuilders(其實就是HttpSecurity)的build方法,內(nèi)部最后也是調(diào)用自己的init良蛮、configure抽碌、performBuild。

2.3.1. init(HttpSecurity)
會遍歷所有之前加載好的配置類configuator决瞳,調(diào)用其init
配置類的init一般是配置HttpSecurity货徙。以HttpBasicConfigurer配置類為例:

    public void init(B http) throws Exception {
        this.registerDefaults(http);
    }

    private void registerDefaults(B http) {
        ContentNegotiationStrategy contentNegotiationStrategy = (ContentNegotiationStrategy)http.getSharedObject(ContentNegotiationStrategy.class);
        if (contentNegotiationStrategy == null) {
            contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
        }

        MediaTypeRequestMatcher restMatcher = new MediaTypeRequestMatcher((ContentNegotiationStrategy)contentNegotiationStrategy, new MediaType[]{MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML});
        restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
        RequestMatcher notHtmlMatcher = new NegatedRequestMatcher(new MediaTypeRequestMatcher((ContentNegotiationStrategy)contentNegotiationStrategy, new MediaType[]{MediaType.TEXT_HTML}));
        RequestMatcher restNotHtmlMatcher = new AndRequestMatcher(Arrays.asList(notHtmlMatcher, restMatcher));
        RequestMatcher preferredMatcher = new OrRequestMatcher(Arrays.asList(X_REQUESTED_WITH, restNotHtmlMatcher));
        this.registerDefaultEntryPoint(http, preferredMatcher);
        this.registerDefaultLogoutSuccessHandler(http, preferredMatcher);
    }

2.3.2. configure(HttpSecurity)
會遍歷所有之前加載好的配置類configuator,調(diào)用其configure
配置類的configure一般構(gòu)造Filter皮胡,添加到HttpSecurity的Filter列表中痴颊,作為過濾器鏈的其中一個。以HttpBasicConfigurer配置類為例:

    public void configure(B http) throws Exception {
        AuthenticationManager authenticationManager = (AuthenticationManager)http.getSharedObject(AuthenticationManager.class);
        BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager, this.authenticationEntryPoint);
        if (this.authenticationDetailsSource != null) {
            basicAuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
        }

        RememberMeServices rememberMeServices = (RememberMeServices)http.getSharedObject(RememberMeServices.class);
        if (rememberMeServices != null) {
            basicAuthenticationFilter.setRememberMeServices(rememberMeServices);
        }

        basicAuthenticationFilter = (BasicAuthenticationFilter)this.postProcess(basicAuthenticationFilter);
        http.addFilter(basicAuthenticationFilter);
    }

2.3.3. performBuild(HttpSecurity)
將List<Filter>以及matcher封裝成SecurityFilterChain

    protected DefaultSecurityFilterChain performBuild() throws Exception {
        Collections.sort(this.filters, this.comparator);
        return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
    }

請求來時屡贺,F(xiàn)ilterChainProxy如何起作用

請求到達(dá)的時候蠢棱,F(xiàn)ilterChainProxy的dofilter()方法內(nèi)部,會遍歷所有的SecurityFilterChain甩栈,匹配url泻仙,第一個匹配到之后則調(diào)用該SecurityFilterChain中的List<filter>依次做認(rèn)證或鑒權(quán),執(zhí)行最后調(diào)用外層傳入的filterchain量没,將控制權(quán)交給上層過濾鏈玉转,即spring 的過濾鏈。
核心邏輯如下:

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
        HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
        List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);//選擇匹配的過濾器鏈
        if (filters != null && filters.size() != 0) {
            FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
            vfc.doFilter(fwRequest, fwResponse);//將請求交給該過濾器鏈殴蹄,逐個通過filter進(jìn)行驗證
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));
            }

            fwRequest.reset();
            chain.doFilter(fwRequest, fwResponse);//security邏輯已執(zhí)行完冤吨,交給上層
        }
    }
//取第一個匹配的過濾器鏈
    private List<Filter> getFilters(HttpServletRequest request) {
        Iterator var2 = this.filterChains.iterator();

        SecurityFilterChain chain;
        do {
            if (!var2.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var2.next();
        } while(!chain.matches(request));

        return chain.getFilters();
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市饶套,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌垒探,老刑警劉巖妓蛮,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異圾叼,居然都是意外死亡蛤克,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門夷蚊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來构挤,“玉大人,你說我怎么就攤上這事惕鼓〗钕郑” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長矾飞。 經(jīng)常有香客問我一膨,道長,這世上最難降的妖魔是什么洒沦? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任豹绪,我火速辦了婚禮,結(jié)果婚禮上申眼,老公的妹妹穿的比我還像新娘瞒津。我一直安慰自己,他們只是感情好括尸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布巷蚪。 她就那樣靜靜地躺著,像睡著了一般姻氨。 火紅的嫁衣襯著肌膚如雪钓辆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天肴焊,我揣著相機(jī)與錄音前联,去河邊找鬼。 笑死娶眷,一個胖子當(dāng)著我的面吹牛似嗤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播届宠,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼烁落,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了豌注?” 一聲冷哼從身側(cè)響起伤塌,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎轧铁,沒想到半個月后每聪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡齿风,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年药薯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片救斑。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡童本,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脸候,到底是詐尸還是另有隱情穷娱,我是刑警寧澤绑蔫,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站鄙煤,受9級特大地震影響晾匠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜梯刚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一凉馆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧亡资,春花似錦澜共、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瘦黑,卻和暖如春京革,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背幸斥。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工匹摇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人甲葬。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓廊勃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親经窖。 傳聞我的和親對象是個殘疾皇子坡垫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容