SpringSecurity登錄原理(源碼級講解)

一、簡單敘述

首先會進入UsernamePasswordAuthenticationFilter并且設置權限為null和是否授權為false,然后進入ProviderManager查找支持UsernamepasswordAuthenticationTokenprovider并且調用provider.authenticate(authentication);再然后就是UserDetailsService接口的實現類(也就是自己真正具體的業務了),這時候都檢查過了后,就會回調UsernamePasswordAuthenticationFilter并且設置權限(具體業務所查出的權限)和設置授權為true(因為這時候確實所有關卡都檢查過了)。

PS:云里霧繞的?沒關系,接下里看我們每一步驟都具體的深入到源碼級別的去分析。


二、源碼分析

UsernamePasswordAuthenticationFilter

// 繼承了AbstractAuthenticationProcessingFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        // 認證請求的方式必須為POST
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 獲取用戶名
        String username = obtainUsername(request);
        // 獲取密碼
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }
        // 用戶名去空白
        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

可以發現繼承了AbstractAuthenticationProcessingFilter,那我們就來看下此類

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
    
    // 過濾器doFilter方法
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        /*
         * 判斷當前filter是否可以處理當前請求,若不行,則交給下一個filter去處理。
         */
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            // 很關鍵!!!調用了子類(UsernamePasswordAuthenticationFilter)的方法
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            // 最終認證成功后,會處理一些與session相關的方法(比如將認證信息存到session等操作)。
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            // 認證失敗后的一些處理。
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        /*
         * 最終認證成功后的相關回調方法,主要將當前的認證信息放到SecurityContextHolder中
         * 并調用成功處理器做相應的操作。
         */
        successfulAuthentication(request, response, chain, authResult);
    }
}

PS:看到這里估計很多人在罵娘了,什么玩意,直接復制粘貼也不講解,不要急,上面只是看下類結構,下面來具體分析!這里只分析主要代碼,不是很主要也不是很相關的不作講解,有興趣的自己去讀。

(一)、 父類的處理流程

1、繼承了父類,父類是個過濾器,所以肯定先執行AbstractAuthenticationProcessingFilter.doFilter(),此方法首先判斷當前的filter是否可以處理當前請求,不可以的話則交給下一個filter處理。

/*
* 判斷當前filter是否可以處理當前請求,若不行,則交給下一個filter去處理。
*/
if (!requiresAuthentication(request, response)) {
    chain.doFilter(request, response);
    return;
}

2、調用此抽象類的子類UsernamePasswordAuthenticationFilter.attemptAuthentication(request, response)方法做具體的操作。

// 很關鍵!!!調用了子類(UsernamePasswordAuthenticationFilter)的方法
authResult = attemptAuthentication(request, response);

3、最終認證成功后做一些成功后的session操作,比如將認證信息存到session等。

// 最終認證成功后,會處理一些與session相關的方法(比如將認證信息存到session等操作)。
sessionStrategy.onAuthentication(authResult, request, response);

4、最終認證成功后的相關回調方法,主要將當前的認證信息放到SecurityContextHolder中并調用成功處理器做相應的操作。

successfulAuthentication(request, response, chain, authResult);

protected void successfulAuthentication(HttpServletRequest request,
    HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
    }
    
    // 將當前的認證信息放到SecurityContextHolder中
    SecurityContextHolder.getContext().setAuthentication(authResult);
    rememberMeServices.loginSuccess(request, response, authResult);
    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }
    // 調用成功處理器,可以自己實現AuthenticationSuccessHandler接口重寫方法寫自己的邏輯
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

(二)、子類的處理流程

1、父類的authResult = attemptAuthentication(request, response);觸發了自類的方法。

2、此方法首先判斷請求方式是不是POST提交,必須是POST

// 認證請求的方式必須為POST
if (postOnly && !request.getMethod().equals("POST")) {
    throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
}

3、從請求中獲取usernamepassword,并做一些處理

// 獲取用戶名
String username = obtainUsername(request);
// 獲取密碼
String password = obtainPassword(request);

if (username == null) {
    username = "";
}

if (password == null) {
    password = "";
}
// 用戶名去空白
username = username.trim();

4、封裝Authenticaiton類的實現類UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super((Collection)null);
    this.principal = principal;
    this.credentials = credentials;
    this.setAuthenticated(false);
}

PS:為什么這個構造器設置權限為null?super((Collection)null);,并且設置是否授權為false?this.setAuthenticated(false);

道理很簡單,因為我們這是剛剛登陸過來,你的賬號密碼對不對我們都沒驗證呢,所以這里是未授權,權限null。

5、調用AuthenticationManagerauthenticate方法進行驗證

return this.getAuthenticationManager().authenticate(authRequest);

(三)、AuthenticationManager處理流程

1、怎么觸發的?

return this.getAuthenticationManager().authenticate(authRequest);

PS:交由AuthenticationManager接口的ProviderManager實現類處理。

2、ProviderManager.authenticate(Authentication authentication);

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class toTest = authentication.getClass();
        Object lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        // 拿到全部的provider
        Iterator e = this.getProviders().iterator();
        // 遍歷provider
        while(e.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)e.next();
            // 挨著個的校驗是否支持當前token
            if(provider.supports(toTest)) {
                if(debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    // 找到后直接break,并由當前provider來進行校驗工作
                    result = provider.authenticate(authentication);
                    if(result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }
        // 若沒有一個支持,則嘗試交給父類來執行
        if(result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }
    ..........................
    }

**3、此方法遍歷所有的Providers,然后依次執行驗證方法看是否支持UsernamepasswordAuthenticationToken**

// 拿到全部的provider
Iterator e = this.getProviders().iterator();
// 遍歷provider
while(e.hasNext()) {
    AuthenticationProvider provider = (AuthenticationProvider)e.next();
    // 挨著個的校驗是否支持當前token
    if(provider.supports(toTest)) {
        if(debug) {
            logger.debug("Authentication attempt using " + provider.getClass().getName());
        }
    }
}

4、若有一個能夠支持當前token,則直接交由此provider處理并break。

// 找到后直接break,并由當前provider來進行校驗工作
result = provider.authenticate(authentication);
if(result != null) {
    this.copyDetails(authentication, result);
    break;
}

5、若沒一個provider驗證成功,則交由父類來嘗試處理

// 若沒有一個支持,則嘗試交給父類來執行
if(result == null && this.parent != null) {
    try {
        result = this.parent.authenticate(authentication);
    } catch (ProviderNotFoundException var9) {
        ;
    } catch (AuthenticationException var10) {
        lastException = var10;
    }
}

(四)、AuthenticationProvider處理流程

1、怎么觸發的?

// 由上一步的ProviderManager的authenticate方法來觸發
result = provider.authenticate(authentication);

PS:這里交由AuthenticationProvider接口的實現類DaoAuthenticationProvider來處理。

2、DaoAuthenticationProvider

// 繼承了AbstractUserDetailsAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
            /*
             * 調用UserDetailsService接口的loadUserByUsername方法,
             * 此方法就是我們自己定義的類去實現接口重寫的方法,處理我們自己的業務邏輯。
             */
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException var6) {
            if(authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
            }

            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }

        if(loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    }
}

3、繼承了AbstractUserDetailsAuthenticationProvider

// 實現了AuthenticationProvider接口
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
  
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null?"NONE_PROVIDED":authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if(user == null) {
            cacheWasUsed = false;

            try {
                // 調用自類retrieveUser
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User \'" + username + "\' not found");
                if(this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            /*
             * 前檢查由DefaultPreAuthenticationChecks類實現(主要判斷當前用戶是否鎖定,過期,凍結
             * User接口)
             */
            this.preAuthenticationChecks.check(user);
            // 子類具體實現
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if(!cacheWasUsed) {
                throw var7;
            }
            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }
        // 檢測用戶密碼是否過期
        this.postAuthenticationChecks.check(user);
        if(!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if(this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }
}

4、AbstractUserDetailsAuthenticationProvider.authenticate()首先調用了user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);

PS:調用的是DaoAuthenticationProvider.retrieveUser()

5、調用我們自己的業務處理類

 /*
 * 調用UserDetailsService接口的loadUserByUsername方法,
 * 此方法就是我們自己定義的類去實現接口重寫的方法,處理我們自己的業務邏輯。
 */
 loadedUser = this.getUserDetailsService().loadUserByUsername(username);

比如:

/**
 * @author chentongwei@bshf360.com 2018-03-26 13:15
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表單登錄用戶名:" + username);
        return buildUser(username);
    }

    private UserDetails buildUser(String username) {
        /**
         * passwordEncoder.encode這步驟應該放到注冊接口去做,而這里只需要傳一個從db查出來的pwd即可。
         *
         * passwordEncoder.encode("123456")每次打印出來都是不同的,雖然是同一個(123456)密碼,
         * 但是他會隨機生成一個鹽(salt),他會把隨機生成的鹽混到加密的密碼里。Springsecurity驗證(matches方法)的時候會將利用此鹽解析出pwd,進行匹配。
         * 這樣的好處是:如果數據庫里面有10個123456密碼。但是被破解了1個,那么另外九個是安全的,因為db里存的串是不一樣的。
         */
        String password = passwordEncoder.encode("123456");
        logger.info("數據庫密碼是:" + password);
        // 這個User不一定必須用SpringSecurity的,可以寫一個自定義實現UserDetails接口的類,然后把是否鎖定等判斷邏輯寫進去。
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

PS:注意:實現UserDetailsService接口。可返回我們自己定義的User類,但User類要實現UserDetails接口

6、調用完retrieveUser方法繼續回到抽象類的authenticate方法

7、首先做一些檢查

/*
* 前檢查由DefaultPreAuthenticationChecks類實現(主要判斷當前用戶是否鎖定,過期,凍結
* User接口)
*/
this.preAuthenticationChecks.check(user);
// 檢測用戶密碼是否過期
this.postAuthenticationChecks.check(user);

8、調用createSuccessAuthentication方法進行授權成功

return this.createSuccessAuthentication(principalToReturn, authentication, user);
// 成功授權
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    // 回調UsernamePasswordAuthenticationToken的構造器,這里調用的是授權成功的構造器
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    // 將認證信息的一塊內容放到details
    result.setDetails(authentication.getDetails());
    return result;
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
    // 不在是null,而是傳來的權限,這個權限就是我們自己定義的detailsService類所返回的,可以從db查
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    // 這里是true,不在是false。
    super.setAuthenticated(true);
}

9、回到起點

AbstractAuthenticationProcessingFilter.doFilter()

進行session存儲和成功后的處理器的調用等


三、總結

只是簡單說下類之間的調用順序。

UsernamePasswordAuthenticationFilter
Authentication
AuthenticationManager
AuthenticationProvider
UserDetailsService
// 回到起點進行后續操作,比如緩存認證信息到session和調用成功后的處理器等等
UsernamePasswordAuthenticationFilter 

四、Demo

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
</head>
<body>
<h2>標準登錄頁面</h2>
<h3>表單登錄</h3>
<form action="login" 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"><button type="submit">登錄</button></td>
        </tr>
    </table>
</form>
</body>
</html>
http.formLogin()
    // 默認表單登錄頁
    .loginPage(SecurityConstant.DEFAULT_UNAUTHENTICATION_URL)
    // 登錄接口
    .loginProcessingUrl(SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_FORM)
/**
 * 常量
 *
 * @author chentongwei@bshf360.com 2018-03-26 11:40
 */
public interface SecurityConstant {

    /**
     * 默認登錄頁
     */
    String DEFAULT_LOGIN_PAGE_URL = "/default-login.html";

    /**
     * 默認的登錄接口
     */
    String DEFAULT_LOGIN_PROCESSING_URL_FORM = "/login";
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
 * @author chentongwei@bshf360.com 2018-03-26 13:15
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表單登錄用戶名:" + username);
        return buildUser(username);
    }

    private UserDetails buildUser(String username) {
        /**
         * passwordEncoder.encode這步驟應該放到注冊接口去做,而這里只需要傳一個從db查出來的pwd即可。
         *
         * passwordEncoder.encode("123456")每次打印出來都是不同的,雖然是同一個(123456)密碼,
         * 但是他會隨機生成一個鹽(salt),他會把隨機生成的鹽混到加密的密碼里。Springsecurity驗證(matches方法)的時候會將利用此鹽解析出pwd,進行匹配。
         * 這樣的好處是:如果數據庫里面有10個123456密碼。但是被破解了1個,那么另外九個是安全的,因為db里存的串是不一樣的。
         */
        String password = passwordEncoder.encode("123456");
        logger.info("數據庫密碼是:" + password);
        // 這個User不一定必須用SpringSecurity的,可以寫一個自定義實現UserDetails接口的類,然后把是否鎖定等判斷邏輯寫進去。
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

大功告成!

只需要一個html,一段配置,一個Service自己的業務類即可。

疑問:

1、接口login在哪定義的?

2、用戶名username和密碼password在哪接收的?

3、沒有控制器怎么進入我們的MyUserDetailsService的方法?

解答:

1、SpringSecurity內置的,并且只能為POST

public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}

2、名稱不能變,必須是usernamepassword

public class UsernamePasswordAuthenticationFilter extends
      AbstractAuthenticationProcessingFilter {
   // ~ Static fields/initializers
   // =====================================================================================

   public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
   public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
}

3、自己看我上面的源碼分析


五、廣告

  • Demo源碼已上傳到碼云,文章會定期更新。下面鏈接是我對Spring-Security進行的二次封裝。使之變得零配置,高擴展。如果覺得對您有幫助,希望給個star,沒幫助也可以看看框架思想。

    https://gitee.com/geekerdream/common-security

  • QQ群【Java初學者學習交流群】:458430385

  • 微信公眾號【Java碼農社區】

    img
  • 今日頭條號:編程界的小學生

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容