Spring Security 解析(三) —— 個性化認證 以及 RememberMe 實現(xiàn)

Spring Security 解析(三) —— 個性化認證 以及 RememberMe 實現(xiàn)

??在學習Spring Cloud 時棕兼,遇到了授權(quán)服務oauth 相關內(nèi)容時,總是一知半解,因此決定先把Spring Security 球匕、Spring Security Oauth2 等權(quán)限、認證相關的內(nèi)容椿争、原理及設計學習并整理一遍逾柿。本系列文章就是在學習的過程中加強印象和理解所撰寫的缀棍,如有侵權(quán)請告知宅此。

項目環(huán)境:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

一、個性化認證

(一) 配置登錄

?? 在 授權(quán)過程 和 認證過程 中我們都是使用的 Security 默認的一個登錄頁面(/login)爬范,那么如果我們想自定義一個登錄頁面該如何實現(xiàn)呢父腕?其實很簡單,我們新建 FormAuthenticationConfig 配置類青瀑,然后在configure(HttpSecurity http) 方法中實現(xiàn)以下設置:

        http.formLogin()
                //可以設置自定義的登錄頁面 或者 (登錄)接口
                // 注意1: 一般來說設置成(登錄)接口后璧亮,該接口會配置成無權(quán)限即可訪問,所以會走匿名filter, 也就意味著不會走認證過程了斥难,所以我們一般不直接設置成接口地址
                // 注意2: 這里配置的 地址一定要配置成無權(quán)限訪問枝嘶,否則將出現(xiàn) 一直重定向問題(因為無權(quán)限后又會重定向到這里配置的登錄頁url)
                .loginPage(securityProperties.getLogin().getLoginPage())
                //.loginPage("/loginRequire")
                // 指定驗證憑據(jù)的URL(默認為 /login) ,
                // 注意1:這里修改后的 url 會意味著  UsernamePasswordAuthenticationFilter 將 驗證此處的 url
                // 注意2: 與 loginPage設置的接口地址是有 區(qū)別, 一但 loginPage 設置了的是訪問接口url,那么此處配置將無任何意義
                // 注意3: 這里設置的 Url 是有默認無權(quán)限訪問的
                .loginProcessingUrl(securityProperties.getLogin().getLoginUrl())
                //分別設置成功和失敗的處理器
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler);

??最后在 SpringSecurityConfig 的 configure(HttpSecurity http) 方法中 調(diào)用 formAuthenticationConfig.configure(http) 即可;

?? 正如看到的一樣哑诊,我們通過 loginPage()設置 登錄頁面接口, 通過 loginProcessingUrl() 設置 UsernamePasswordAuthenticationFilter 要匹配的 接口地址(一定是Post)(看過授權(quán)過程的同學應該都知道其默認的是/login)群扶。 這里有以下幾點值得注意:

  • loginPage() 這里配置的 地址(不管是接口url還是登錄頁面)一定要配置成無權(quán)限訪問,否則將出現(xiàn) 一直重定向問題(因為無權(quán)限后又會重定向到這里配置的登錄頁url
  • loginPage() 一般來說不直接設置成(登錄)接口镀裤,因為設置了接口會配置成無權(quán)限即可訪問(當然設置成登錄頁面也需要配置無權(quán)限訪問)竞阐,所以會走匿名filter, 也就意味著不會走認證過程了,所以我們一般不直接設置成接口地址
  • loginProcessingUrl() 這里修改后的 url 會意味著 UsernamePasswordAuthenticationFilter 將 驗證此處的 url
  • loginProcessingUrl() 這里設置的 Url 是有默認無權(quán)限訪問的,與 loginPage設置的接口地址是有 區(qū)別, 一但 loginPage 設置了的是接口url暑劝,那么此處配置將無任何意義
  • successHandler() 和 failureHandler 分別 設置認證成功處理器 和 認證失敗處理器 (如果對這2個處理器沒印象的話馁菜,建議回顧下授權(quán)過程)

(二) 配置成功和失敗處理器

?? 在授權(quán)過程中,我們增簡單提及到過這2個處理器铃岔,在Security中默認的處理器分別是SavedRequestAwareAuthenticationSuccessHandler 和 SimpleUrlAuthenticationFailureHandler 汪疮,這次我們自定義這2個處理器,分別為 CustomAuthenticationSuccessHandler ( extends SavedRequestAwareAuthenticationSuccessHandler ) 重寫 onAuthenticationSuccess() 方法 :

@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private SecurityProperties securityProperties;

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登錄成功");
        // 如果設置了loginSuccessUrl毁习,總是跳到設置的地址上
        // 如果沒設置智嚷,則嘗試跳轉(zhuǎn)到登錄之前訪問的地址上,如果登錄前訪問地址為空纺且,則跳到網(wǎng)站根路徑上
        if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
            requestCache.removeRequest(request, response);
            setAlwaysUseDefaultTargetUrl(true);
            setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }

}

和 CustomAuthenticationFailureHandler( extends SimpleUrlAuthenticationFailureHandler) 重寫 onAuthenticationFailure() 方法 :

@Component("customAuthenticationFailureHandler")
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        logger.info("登錄失敗");
        if (StringUtils.isEmpty(securityProperties.getLogin().getLoginErrorUrl())){

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));

        } else {
            // 跳轉(zhuǎn)設置的登陸失敗頁面
            redirectStrategy.sendRedirect(request,response,securityProperties.getLogin().getLoginErrorUrl());
        }

    }
}

(三) 自定義的登陸頁面

這里就不再描述盏道,直接貼代碼:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
</head>
<body>
<h2>登錄頁面</h2>
<form action="/loginUp" method="post">  
    <table>
        <tr>
            <td>用戶名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>記住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登錄</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

??注意這里請求的地址是 loginProcessingUrl() 配置的地址

(四)測試驗證

??這里就不在貼結(jié)果圖了,只要我們明白結(jié)果流程就行是這樣的就可以:
localhost:8080 ——> 點擊 測試驗證Security 權(quán)限控制 ————> 跳轉(zhuǎn)到 我們自定義的 /loginUp.html 登錄頁,登錄后 ————> 有配置loginSuccessUrl,則跳轉(zhuǎn)到 loginSuccess.html;反之則直接跳轉(zhuǎn)到 /get_user/test 接口返回結(jié)果载碌。 整個流程就全面涉及到了我們自定義的登錄頁面猜嘱、自定義的登錄成功/失敗處理器。

二嫁艇、 RememberMe (記住我)功能解析

(一)RememberMe 功能實現(xiàn)配置

首先我們一股腦的將rememberMe配置加上朗伶,然后看下現(xiàn)象:

1、 創(chuàng)建 persistent_logins 表步咪,用于存儲token和用戶的關聯(lián)信息:

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

2 论皆、 添加rememberMe配置 信息

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 如果token表不存在,使用下面語句可以初始化 persistent_logins(ddl在db目錄下) 表;若存在点晴,請注釋掉這條語句感凤,否則會報錯。
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {

        formAuthenticationConfig.configure(http);
        http.   ....
                .and()
                // 開啟 記住我功能粒督,意味著 RememberMeAuthenticationFilter 將會 從Cookie 中獲取token信息
                .rememberMe()
                // 設置 tokenRepository 陪竿,這里默認使用 jdbcTokenRepositoryImpl,意味著我們將從數(shù)據(jù)庫中讀取token所代表的用戶信息
                .tokenRepository(persistentTokenRepository())
                // 設置  userDetailsService , 和 認證過程的一樣屠橄,RememberMe 有專門的 RememberMeAuthenticationProvider ,也就意味著需要 使用UserDetailsService 加載 UserDetails 信息
                .userDetailsService(userDetailsService)
                // 設置 rememberMe 的有效時間萨惑,這里通過 配置來設置
                .tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds())
                .and()
                .csrf().disable(); // 關閉csrf 跨站(域)攻擊防控
    }

這里解釋下配置:

  • rememberMe() 開啟 記住我功能,意味著 RememberMeAuthenticationFilter 將會 從Cookie 中獲取token信息
  • tokenRepository() 配置 token的獲取策略仇矾,這里配置成從數(shù)據(jù)庫中讀取
  • userDetailsService() 配置 UserDetaisService (如果不熟悉該對象庸蔼,建議回顧認證過程)
  • tokenValiditySeconds() 設置 rememberMe 的有效時間,這里通過 配置來設置

另一個重要的配置在登錄頁面贮匕,這里的 必須是 name="remember-me" 姐仅,rememberMe就是通過驗證這個配置來開啟remermberMe功能的。

<input name="remember-me" type="checkbox" value="true"/>記住我</td>

??實操結(jié)果應該為:進入登陸頁面 ——> 勾選記住我后登錄 ——> 成功后刻盐,查看persistent_logins 表發(fā)現(xiàn)有一條數(shù)據(jù)——> 重啟項目 ——> 重新訪問需要登錄才能訪問的頁面,發(fā)現(xiàn)無需登錄即可訪問——> 刪除 persistent_logins 表數(shù)據(jù)掏膏,等待token設置的有效時間過期,然后重新刷新頁面發(fā)現(xiàn)跳轉(zhuǎn)到登陸頁面敦锌。

(二) RembemberMe 實現(xiàn)源碼解析

?? 首先我們查看UsernamePasswordAuthenticationFiler(AbstractAuthenticationProcessingFilter) 的 successfulAuthentication() 方法內(nèi)部源碼:

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        
        // 1 設置 認證成功的Authentication對象到SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(authResult);
        
        // 2 調(diào)用 RememberMe 相關service處理
        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
        //3 調(diào)用成功處理器
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

其中我們發(fā)現(xiàn)我們本次重點關注的一行代碼: rememberMeServices.loginSuccess(request, response, authResult) , 查看這個方法內(nèi)部源碼:

@Override
    public final void loginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // 這里就在判斷用戶是否勾選了記住我
        if (!rememberMeRequested(request, parameter)) {
            logger.debug("Remember-me login not requested.");
            return;
        }

        onLoginSuccess(request, response, successfulAuthentication);
    }

通過 rememberMeRequested() 判斷是否勾選了記住我馒疹。
onLoginSuccess() 方法 最終會調(diào)用到 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法,貼出其方法源碼如下:

protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // 1 獲取賬戶名
        String username = successfulAuthentication.getName();
        
        // 2 創(chuàng)建  PersistentRememberMeToken 對象
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
            // 3 通過 tokenRepository 存儲 persistentRememberMeToken 信息
            tokenRepository.createNewToken(persistentToken);
            // 4 將 persistentRememberMeToken 信息添加到Cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

分析下源碼步驟:

  • 獲取 賬戶信息 username
  • 傳入 username 創(chuàng)建 PersistentRememberMeToken 對象
  • 通過 tokenRepository 存儲 persistentRememberMeToken信息
  • 將 persistentRememberMeToken 信息添加到Cookie中

??這里的 tokenRepository 就是我們配置 rememberMe功能所設置的乙墙。經(jīng)過上面的解析我們看到了rememberServices 將 創(chuàng)建一個 token 信息颖变,并存儲到數(shù)據(jù)庫(因為我們配置的是數(shù)據(jù)庫存儲方式 JdbcTokenRepositoryImpl )中,并將token信息添加到Cookie中了听想。到這里腥刹,我們看到了RememberMe實現(xiàn)前的一些業(yè)務處理,那么后面如何實現(xiàn)RememberMe汉买,我想大家心里大概都有個底了衔峰。這里直接拋出之前授權(quán)過程中我們沒有提及到的 filter 類 RememberMeAuthenticationFilter,它是介于 UsernamePasswordAuthenticationFilter 和 AnonymousAuthenticationFilter 之間的一個filter蛙粘,它主要負責的就是前面的filter都沒有認證成功后從Cookie中獲取token信息然后再通過tokenRepository 獲取 登錄用戶名垫卤,然后UserDetailsServcie 加載 UserDetails 信息 ,最后創(chuàng)建 Authticaton(RememberMeAuthenticationToken) 信息再調(diào)用 AuthenticationManager.authenticate() 進行認證過程出牧。

RememberMeAuthenticationFilter

??我們來看下 RememberMeAuthenticationFilter 的dofiler方法源碼:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //  1 調(diào)用 rememberMeServices.autoLogin() 獲取Authtication 信息
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);

            if (rememberMeAuth != null) {
                // Attempt authenticaton via AuthenticationManager
                try {
                    // 2 調(diào)用 authenticationManager.authenticate() 認證
                    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    
                    ......
                    }

                }
                catch (AuthenticationException authenticationException) {
                .....
            }

            chain.doFilter(request, response);
        }

我們主要關注 rememberMeServices.autoLogin(request,response) 方法實現(xiàn)穴肘,查看器源碼:

@Override
    public final Authentication autoLogin(HttpServletRequest request,
            HttpServletResponse response) {
        // 1 從Cookie 中獲取 token 信息
        String rememberMeCookie = extractRememberMeCookie(request);

        if (rememberMeCookie == null) {
            return null;
        }
        
        if (rememberMeCookie.length() == 0) {
            cancelCookie(request, response);
            return null;
        }

        UserDetails user = null;

        try {
            // 2 解析 token信息
            String[] cookieTokens = decodeCookie(rememberMeCookie);
            // 3 通過 token 信息 生成 Uerdetails 信息
            user = processAutoLoginCookie(cookieTokens, request, response);
            userDetailsChecker.check(user);

            logger.debug("Remember-me cookie accepted");
            // 4 通過 UserDetails 信息創(chuàng)建 Authentication 
            return createSuccessfulAuthentication(request, user);
        } 
        .....
    }

內(nèi)部實現(xiàn)步驟:

  • 從Cookie中獲取 token 信息并解析
  • 通過 解析的token 生成 UserDetails (processAutoLoginCookie() 方法實現(xiàn) )
  • 通過 UserDetails 生成 Authentication ( createSuccessfulAuthentication() 創(chuàng)建 RememberMeAuthenticationToken )

其中最關鍵的一部是 processAutoLoginCookie() 方法是如何生成UserDetails 對象的,我們查看這個方法源碼實現(xiàn):

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
            HttpServletRequest request, HttpServletResponse response) {
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
        // 1 通過 tokenRepository 加載數(shù)據(jù)庫token信息
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);

        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());
        // 2 判斷 用戶傳入token和數(shù)據(jù)中的token是否一致崔列,不一致可能存在安全問題
        if (!presentedToken.equals(token.getTokenValue())) {
            tokenRepository.removeUserTokens(token.getUsername());
            throw new CookieTheftException(
                    messages.getMessage(
                            "PersistentTokenBasedRememberMeServices.cookieStolen",
                            "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        }
        try {
            // 3 更新 token 并添加到Cookie中
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            addCookie(newToken, request, response);
        }
        catch (Exception e) {
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }
        // 4 通過 UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息并返回
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

我們看下其內(nèi)部步驟:

  • 通過 tokenRepository 加載數(shù)據(jù)庫token信息
  • 判斷 用戶傳入token和數(shù)據(jù)中的token是否一致淆衷,不一致可能存在安全問題
  • 更新 token 并添加到Cookie中
  • 通過 UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息并返回

?? 看到這里相信大家以下就明白了,當初為啥在啟用rememberMe功能時要配置 tokenRepository 和 UserDetailsService了顿痪。

這里我就不再演示整個實現(xiàn)的流程了蘑斧,老規(guī)矩,上流程圖:

https://upload-images.jianshu.io/upload_images/19297733-fe2fbb9aba985fa9.jpeg

?? 本文介紹個性化認證和RememberMe的代碼可以訪問代碼倉庫中的 security 模塊 边翼,項目的github 地址 : https://github.com/BUG9/spring-security

?? ?? ?? 如果您對這些感興趣鱼响,歡迎star、follow组底、收藏丈积、轉(zhuǎn)發(fā)給予支持!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末债鸡,一起剝皮案震驚了整個濱河市江滨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌厌均,老刑警劉巖唬滑,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異棺弊,居然都是意外死亡晶密,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門模她,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稻艰,“玉大人,你說我怎么就攤上這事侈净∽鹞穑” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵畜侦,是天一觀的道長运怖。 經(jīng)常有香客問我,道長夏伊,這世上最難降的妖魔是什么摇展? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮溺忧,結(jié)果婚禮上咏连,老公的妹妹穿的比我還像新娘。我一直安慰自己鲁森,他們只是感情好祟滴,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歌溉,像睡著了一般垄懂。 火紅的嫁衣襯著肌膚如雪骑晶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天草慧,我揣著相機與錄音桶蛔,去河邊找鬼。 笑死漫谷,一個胖子當著我的面吹牛仔雷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舔示,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼碟婆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惕稻?” 一聲冷哼從身側(cè)響起竖共,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俺祠,沒想到半個月后公给,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡锻煌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年妓布,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宋梧。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡匣沼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出捂龄,到底是詐尸還是另有隱情释涛,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布倦沧,位于F島的核電站唇撬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏展融。R本人自食惡果不足惜窖认,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望告希。 院中可真熱鬧扑浸,春花似錦、人聲如沸燕偶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽指么。三九已至酝惧,卻和暖如春榴鼎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晚唇。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工巫财, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缺亮。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓翁涤,卻偏偏與公主長得像桥言,于是被迫代替她去往敵國和親萌踱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355

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