Spring 源碼分析(五)Sercurity

Spring 源碼分析(五)Sercurity

sschrodienger

2019/03/04


Filter 代理組件分析


DelegatingFilterProxy

DelegatingFilterProxy 是標準 servlet 過濾器的一個代理類推沸,它可以代理 Spring 容器中實現(xiàn)了 Filter 接口的 Bean,以方便該過濾器獲得 Spring 依賴注入以及生命周期的支持夺刑。

DelegatingFilterProxy 維護了一個代理類 private volatile Filter delegatedelegate是最重要的執(zhí)行過濾器。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        synchronized (this.delegateMonitor) {
            delegateToUse = this.delegate;
            if (delegateToUse == null) {
                WebApplicationContext wac = findWebApplicationContext();
                if (wac == null) {
                    throw new IllegalStateException("No WebApplicationContext found: " +
                            "no ContextLoaderListener or DispatcherServlet registered?");
                }
                delegateToUse = initDelegate(wac);
            }
            this.delegate = delegateToUse;
        }
    }

    // Let the delegate perform the actual doFilter operation.
    invokeDelegate(delegateToUse, request, response, filterChain);
}

protected void invokeDelegate(
        Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    delegate.doFilter(request, response, filterChain);
}

可以看到墓臭,當 DelegatingFilterProxy 注冊到 tomcat 時,doFilter() 方法主要是調(diào)用 invokeDelegate() 方法 來執(zhí)行代理的 doFilter() 方法妖谴。

FilterChainProxy

DelegatingFilterProxy 中窿锉,delegate 變量的實際類型是 FilterChainProxy。關(guān)鍵代碼如下:

public class FilterChainProxy extends GenericFilterBean {
    // ~ Instance fields
    // ================================================================================================

    private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
            ".APPLIED");

    private List<SecurityFilterChain> filterChains;

    private HttpFirewall firewall = new StrictHttpFirewall();

    // ~ Methods
    // ========================================================================================================

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (clearContext) {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                doFilterInternal(request, response, chain);
            }
            finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        }
        else {
            doFilterInternal(request, response, chain);
        }
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);

        List<Filter> filters = getFilters(fwRequest);

        //如果得到的過濾器的數(shù)量為零窖维,則直接跳過
        if (filters == null || filters.size() == 0) {
            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);

            return;
        }

        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }

    /**
     * Returns the first filter chain matching the supplied URL.
     *
     * @param request the request to match
     * @return an ordered array of Filters defining the filter chain
     */
    private List<Filter> getFilters(HttpServletRequest request) {
        for (SecurityFilterChain chain : filterChains) {
            if (chain.matches(request)) {
                return chain.getFilters();
            }
        }

        return null;
    }

    /**
     * Convenience method, mainly for testing.
     *
     * @param url the URL
     * @return matching filter list
     */
    public List<Filter> getFilters(String url) {
        return getFilters(firewall.getFirewalledRequest((new FilterInvocation(url, "GET")
                .getRequest())));
    }

    /**
     * @return the list of {@code SecurityFilterChain}s which will be matched against and
     * applied to incoming requests.
     */
    public List<SecurityFilterChain> getFilterChains() {
        return Collections.unmodifiableList(filterChains);
    }
    
    private static class VirtualFilterChain implements FilterChain {
        //鏈條中的節(jié)點全部執(zhí)行完后榆综,處理request請求的對象
        private final FilterChain originalChain;
        private final List<Filter> additionalFilters;
        private final FirewalledRequest firewalledRequest;
        private final int size;
        private int currentPosition = 0;

        private VirtualFilterChain(FirewalledRequest firewalledRequest,
                FilterChain chain, List<Filter> additionalFilters) {
            this.originalChain = chain;
            this.additionalFilters = additionalFilters;
            this.size = additionalFilters.size();
            this.firewalledRequest = firewalledRequest;
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " reached end of additional filter chain; proceeding with original chain");
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                currentPosition++;

                Filter nextFilter = additionalFilters.get(currentPosition - 1);

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

}

比較關(guān)鍵的變量是 FILTER_APPLIEDfilterChains妙痹,前者在 doFilter 中防止2次處理铸史,后者存儲了需要代理的所有 Filter 并在 doFilterInternal 中選擇符合 url 條件的 Filter 運行。

內(nèi)部類 VirtualFilterChain 繼承自 FilterChain怯伊,使用的是責任鏈模式琳轿。如下圖所示:

責任鏈執(zhí)行模式

看源代碼如何實現(xiàn)責任鏈模式。

private static class VirtualFilterChain implements FilterChain {
    //鏈條中的節(jié)點全部執(zhí)行完后,處理request請求的對象
    private final FilterChain originalChain;
    private final List<Filter> additionalFilters;
    private final FirewalledRequest firewalledRequest;
    private final int size;
    private int currentPosition = 0;

    private VirtualFilterChain(FirewalledRequest firewalledRequest,
            FilterChain chain, List<Filter> additionalFilters) {
        this.originalChain = chain;
        this.additionalFilters = additionalFilters;
        this.size = additionalFilters.size();
        this.firewalledRequest = firewalledRequest;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
        if (currentPosition == size) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                        + " reached end of additional filter chain; proceeding with original chain");
            }

            // Deactivate path stripping as we exit the security filter chain
            this.firewalledRequest.reset();

            originalChain.doFilter(request, response);
        } else {
            currentPosition++;

            Filter nextFilter = additionalFilters.get(currentPosition - 1);

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

在調(diào)用 nextFilter.doFilter(request, response, this) 時崭篡,會把自身當作 FilterChain 傳入 nextFilter 中挪哄,這樣,只要 nextFilter 調(diào)用 filterChain.doFilter琉闪,就會重新回到當前 VirtualFilterChain迹炼,并選擇下一個 Filter 執(zhí)行。執(zhí)行圖如下所示:

執(zhí)行圖

spring security core filter 組件分析


在 Spring web Security 中颠毙,spring security core filter 以責任鏈的模式注冊在 FilterChainProxy 中斯入,按照順序依次是:

1. WebAsyncManagerIntegrationFilter
2. SecurityContextPersistenceFilter
3. HeaderWriterFilter
4. CsrfFilter
5. LogoutFilter
6. UsernamePasswordAuthenticationFilter
7. RequestCacheAwareFilter
8. SecurityContextHolderAwareRequestFilter
9. AnonymousAuthenticationFilter
10.SessionManagementFilter
11.ExceptionTranslationFilter
12.FilterSecurityInterceptor

和登陸息息相關(guān)的依次是 5,6,9,11,12,下面依次分析這些組件蛀蜜。

LogoutFilter

LogoutFilter 實現(xiàn)的功能比較簡單刻两,主要是當遇到 logoutUrl 的時候進行退出的操作,并且跳轉(zhuǎn)到規(guī)定界面滴某。

觀察源碼磅摹,如下:

public class LogoutFilter extends GenericFilterBean {

    // ~ Instance fields
    // ================================================================================================

    private RequestMatcher logoutRequestMatcher;

    private final LogoutHandler handler;
    private final LogoutSuccessHandler logoutSuccessHandler;

    // ~ Constructors
    // ===================================================================================================

    /**
     * Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
     * target destination after logging out. The list of <tt>LogoutHandler</tt>s are
     * intended to perform the actual logout functionality (such as clearing the security
     * context, invalidating the session, etc.).
     */
    public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler,
            LogoutHandler... handlers) {
        this.handler = new CompositeLogoutHandler(handlers);
        Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
        this.logoutSuccessHandler = logoutSuccessHandler;
        setFilterProcessesUrl("/logout");
    }

    public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
        this.handler = new CompositeLogoutHandler(handlers);
        Assert.isTrue(
                !StringUtils.hasLength(logoutSuccessUrl)
                        || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
                () -> logoutSuccessUrl + " isn't a valid redirect URL");
        SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
        if (StringUtils.hasText(logoutSuccessUrl)) {
            urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
        }
        logoutSuccessHandler = urlLogoutSuccessHandler;
        setFilterProcessesUrl("/logout");
    }

    // ~ Methods
    // ========================================================================================================

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

        if (requiresLogout(request, response)) {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();

            if (logger.isDebugEnabled()) {
                logger.debug("Logging out user '" + auth
                        + "' and transferring to logout destination");
            }

            this.handler.logout(request, response, auth);

            logoutSuccessHandler.onLogoutSuccess(request, response, auth);

            return;
        }

        chain.doFilter(request, response);
    }

    /**
     * Allow subclasses to modify when a logout should take place.
     *
     * @param request the request
     * @param response the response
     *
     * @return <code>true</code> if logout should occur, <code>false</code> otherwise
     */
    protected boolean requiresLogout(HttpServletRequest request,
            HttpServletResponse response) {
        return logoutRequestMatcher.matches(request);
    }

    public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
        Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
        this.logoutRequestMatcher = logoutRequestMatcher;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
    }
}

首先看 doFilter() 函數(shù),主要邏輯偽代碼是:

//step 1.1
if (requiresLogout(request)) {
    //step 1.2
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    this.handler.logout(request, response, auth);
    //step 1.3
    logoutSuccessHandler.onLogoutSuccess(request, response, auth);
    return;
} else {
    //step 2.1 
    chain.doFilter(request, response);
}

主要分為 4 步:

  • step 1.1:匹配 request 是否為 logout url
  • step 1.2:利用 handler 實現(xiàn)退出邏輯
  • step 1.3:執(zhí)行 logoutSuccessHanleronLogoutSuccess 函數(shù)霎奢,直接返回
  • step 2.1:為匹配到 logout url户誓,進入下一個責任鏈的 Filter

變量 logoutRequestMatcher 用于匹配 url。
變量 handler 用于處理登出邏輯椰憋。在標準 LogoutHandler 實現(xiàn)中厅克,使用了 CompositeLogoutHandler,定義如下:

public final class CompositeLogoutHandler implements LogoutHandler {

    private final List<LogoutHandler> logoutHandlers;

    public CompositeLogoutHandler(LogoutHandler... logoutHandlers) {
        Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
        this.logoutHandlers = Arrays.asList(logoutHandlers);
    }

    public CompositeLogoutHandler(List<LogoutHandler> logoutHandlers) {
        Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
        this.logoutHandlers = logoutHandlers;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        for (LogoutHandler handler : this.logoutHandlers) {
            handler.logout(request, response, authentication);
        }
    }
}

CompositeLogoutHandler 實現(xiàn)了 LogoutHandler橙依,并且用責任鏈的形式執(zhí)行退出邏輯证舟。在標準實現(xiàn)中,CsrfLogoutHandlerSecurityContextLogoutHandler 被用在了責任鏈中窗骑。主要看 SecurityContextLogoutHandler 的形式女责。

public class SecurityContextLogoutHandler implements LogoutHandler {
    private boolean invalidateHttpSession = true;
    private boolean clearAuthentication = true;

    // ~ Methods
    // ========================================================================================================

    /**
     * Requires the request to be passed in.
     *
     * @param request from which to obtain a HTTP session (cannot be null)
     * @param response not used (can be <code>null</code>)
     * @param authentication not used (can be <code>null</code>)
     */
    public void logout(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {
        Assert.notNull(request, "HttpServletRequest required");
        if (invalidateHttpSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                logger.debug("Invalidating session: " + session.getId());
                session.invalidate();
            }
        }

        if (clearAuthentication) {
            SecurityContext context = SecurityContextHolder.getContext();
            context.setAuthentication(null);
        }

        SecurityContextHolder.clearContext();
    }

    public boolean isInvalidateHttpSession() {
        return invalidateHttpSession;
    }

    /**
     * Causes the HttpSession to be invalidated when this  LogoutHandler is invoked. Defaults to true.
     */
    public void setInvalidateHttpSession(boolean invalidateHttpSession) {
        this.invalidateHttpSession = invalidateHttpSession;
    }

    /**
     * If true, removes the Authentication from the SecurityContext to prevent issues with concurrent requests.
     */
    public void setClearAuthentication(boolean clearAuthentication) {
        this.clearAuthentication = clearAuthentication;
    }
}

由此可見,主要是清空 session 和 SecurityContextHolder创译。

logoutSuccessHandler 變量實現(xiàn)了在登陸成功后實現(xiàn)的邏輯抵知。默認的變量使用 SimpleUrlLogoutSuccessHandler,可以實現(xiàn)路徑的跳轉(zhuǎn)软族。定義如下:

public class SimpleUrlLogoutSuccessHandler extends
        AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {

    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        super.handle(request, response, authentication);
    }

}

AbstractAuthenticationTargetUrlRequestHandler 實現(xiàn)了跳轉(zhuǎn)的功能刷喜。

UsernamePasswordAuthenticationFilter

復(fù)雜的一個類,主要實現(xiàn)了登陸邏輯立砸。UsernamePasswordAuthenticationFilter 繼承自 AbstractAuthenticationProcessingFilter掖疮,AbstractAuthenticationProcessingFilter 實現(xiàn)了 doFilter() 方法,UsernamePasswordAuthenticationFilter 是使用的 AbstractAuthenticationProcessingFilterdoFilter() 方法颗祝。首先看 doFilter() 方法浊闪。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

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

    //step 1
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }
    
    Authentication authResult;

    try {
        //step 2
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    //step 3
    catch (InternalAuthenticationServiceException failed) {
        unsuccessfulAuthentication(request, response, failed);

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

    // Authentication success
    //step 5
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }

    //step 6
    successfulAuthentication(request, response, chain, authResult);
}

AbstractAuthenticationProcessingFilter 主要實現(xiàn)了 6 個步驟恼布,如下:

  • step 1:匹配是否為登陸界面并且為POST
  • step 2:嘗試進行驗證并執(zhí)行與會話相關(guān)的操作
  • step 3:當拋出 InternalAuthenticationServiceException 錯誤時,執(zhí)行 unsuccessfulAuthentication 函數(shù)并返回
  • step 4:當拋出 AuthenticationException 錯誤搁宾,即驗證失敗時折汞,執(zhí)行 unsuccessfulAuthentication 函數(shù)并返回
  • step 5:當 ontinueChainBeforeSuccessfulAuthenticationtrue 時,執(zhí)行下一個 Filter 的函數(shù)
  • step 6:執(zhí)行 successfulAuthentication() 函數(shù)盖腿。

step 2 執(zhí)行 attemptAuthentication() 函數(shù)爽待,是一個抽象函數(shù),具體的實現(xiàn)在 UsernamePasswordAuthenicationFilter 中翩腐。

note

  • attemptAuthentication() 必須返回一個已驗證用戶填充的 Authentication堕伪,表明驗證通過。
  • 或者返回一個 null 表明表示身份驗證過程仍在進行中栗菜。在返回之前欠雌,實現(xiàn)應(yīng)該執(zhí)行完成流程所需的任何額外工作。
  • 如果身份驗證過程失敗疙筹,拋出 AuthenticationException 異常富俄。

UsernamePasswordAuthenicationFilterattemptAuthentication 實現(xiàn)如下:

public Authentication attemptAuthentication(HttpServletRequest request,
        HttpServletResponse response) throws AuthenticationException {
    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);
}

obtainXXX() 函數(shù)通過使用 request.getParameter("XXX")獲得參數(shù),最重要的邏輯在 this.getAuthenticationManager().authenticate(authRequest) 中而咆。

在介紹具體的驗證邏輯之前霍比,介紹幾個基本的概念和類。

首先是 Autnenication 接口暴备,這個接口就是我們所說的令牌悠瞬,保存了該用戶的 principal、credential涯捻、details浅妆、authorities。就用戶名密碼登陸來說障癌,principal 就是用戶名凌外,credential 就是密碼,details 就是 IP 之類的詳細信息涛浙,authorities 代表的就是被授權(quán)的權(quán)利康辑。部分如下:

public interface Authentication extends Principal, Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Set by an AuthenticationManager to indicate the authorities that the
     * principal has been granted. Implementations should ensure that modifications to the returned collection array do not affect the state of the Authentication object, or use an unmodifiable instance.
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * The credentials that prove the principal is correct. This is usually a password, but could be anything relevant to the AuthenticationManager. Callers are expected to populate the credentials.
     */
    Object getCredentials();

    Object getDetails();

    /**
     * The identity of the principal being authenticated. In the case of an authentication request with username and password, this would be the username. Callers are expected to populate the principal for an authentication request.
     * Many of the authentication providers will create a code UserDetails object as the principal.
     */
    Object getPrincipal();

    /**
     * Used to indicate to AbstractSecurityInterceptor whether it should present
     * the authentication token to the AuthenticationManager. Typically an
     * AuthenticationManage(or, more often, one of its
     * AuthenticationProviders) will return an immutable authentication token
     * after successful authentication, in which case that token can safely return
     * true to this method. Returning <code>true</code> will improve
     * performance, as calling the <code>AuthenticationManager</code> for every request
     * will no longer be necessary.
     */
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication 的抽象實現(xiàn) AbstractAuthenticationToken 中,賦予了 principal 更多的意義轿亮,一般使用 userDetails 類作為 principal疮薇,已存儲更多的信息,如被授予的權(quán)限我注、用戶名按咒、密碼、是否賬號過期仓手、是否賬號被鎖胖齐、賬號是否可用等信息。

UsernamePasswordAuthenticationToken 實現(xiàn)了 AbstractAuthenticationToken嗽冒。

AuthenticationManager呀伙,是主要的驗證方法。
定義如下:

public interface AuthenticationManager {
    // ~ Methods
    // ========================================================================================================

    /**
     * Attempts to authenticate the passed Authentication object, returning a
     * fully populated Authentication object (including granted authorities)
     * if successful.
     *
     * @param authentication the authentication request object
     *
     * @return a fully authenticated object including credentials
     *
     * @throws AuthenticationException if authentication fails
     */
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

AuthenticationManager 的實現(xiàn)必須遵循以下的原則:

  • 當賬戶不能使用并且 AuthenticationManager 可以檢測到這個狀態(tài)時必須拋出 DisabledException(A DisabledException must be thrown if an account is disabled and the AuthenticationManager can test for this state)
  • 當賬戶被鎖定并且 AuthenticationManager 可以檢測到這個狀態(tài)時必須拋出 LockedException 錯誤添坊。(A LockedException must be thrown if an account is locked and the AuthenticationManager can test for account locking)
  • 如果出現(xiàn)不正確的憑證剿另,則必須拋出 BadCredentialsException(A BadCredentialsException must be thrown if incorrect credentials arepresented. Whilst the above exceptions are optional, an AuthenticationManager must always test credentials)
  • 異常應(yīng)該按照上述順序進行測試(例如,如果帳戶被禁用或鎖定贬蛙,則立即拒絕身份驗證請求雨女,且不執(zhí)行憑據(jù)測試過程),并在適用的情況下拋出異常阳准。此證書將針對禁用或鎖定帳戶進行測試(Exceptions should be tested for and if applicable thrown in the order expressedabove (i.e. if an account is disabled or locked, the authentication request isimmediately rejected and the credentials testing process is not performed). Thisprevents credentials being tested against disabled or locked accounts)

在 spring security 中氛堕,AuthenticationManager 只有一個實現(xiàn),即 ProviderManager野蝇。在 ProviderManager 中讼稚,最重要的是維持了一個 AuthenticationProvider 列表。
AuthenticationProvider 接口定義如下:

public interface AuthenticationProvider {
    
    //和AuthenticationManager相同
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    //如果此AuthenticationProvider支持指定的身份驗證對象绕沈,則返回true锐想。
    //返回true并不保證AuthenticationProvider能夠?qū)ι矸蒡炞C類的呈現(xiàn)實例進行身份驗證。它只是表明它可以支持對其進行更密切的評估乍狐。AuthenticationProvider仍然可以從authenticate(Authentication)方法返回null赠摇,以指示應(yīng)該嘗試另一個AuthenticationProvider。
    //選擇能夠執(zhí)行身份驗證的AuthenticationProvider是在ProviderManager運行時進行的浅蚪。
    boolean supports(Class<?> authentication);
}

ProviderManagerauthenticate 方法藕帜,如下:

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;

    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }

        try {
            result = provider.authenticate(authentication);

            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        } catch (AccountStatusException e) {
            prepareException(e, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw e;
        } catch (InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            throw e;
        }
        catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result == null && parent != null) {
        // Allow the parent to try.
        try {
            result = parentResult = parent.authenticate(authentication);
        }
        catch (ProviderNotFoundException e) {
            // ignore as we will throw below if no other exception occurred prior to
            // calling parent and the parent
            // may throw ProviderNotFound even though a provider in the child already
            // handled the request
        }
        catch (AuthenticationException e) {
            lastException = parentException = e;
        }
    }

    if (result != null) {
        if (eraseCredentialsAfterAuthentication
                && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
        }

        // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
        // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
        if (parentResult == null) {
            eventPublisher.publishAuthenticationSuccess(result);
        }
        return result;
    }

    // Parent was null, or didn't authenticate (or throw an exception).

    if (lastException == null) {
        lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
    }

    // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
    // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
    if (parentException == null) {
        prepareException(lastException, authentication);
    }
    throw lastException;
}

具體的實現(xiàn)邏輯是調(diào)用支持該 AuthenticationAuthenticationProvider進行驗證,如果遇到賬號被鎖定或者被禁用(即拋出 AccountStatusException 異常惜傲,這是 DisabledExceptionLockedException 的父類)耘戚,如果驗證不正確,即遇到 AuthenticationException 異常操漠,則記錄最新異常到 lastException收津,并執(zhí)行下一個 provider。所有 provider 執(zhí)行完成之后浊伙,如果 result 為空撞秋,則說明沒有驗證通過,如果存在 AuthenticationManager parent嚣鄙,則嘗試執(zhí)行 parent 的驗證函數(shù)吻贿。如果這一步執(zhí)行完之后 result 不為空,則返回 result哑子,否則拋出 lastException舅列。

Spring 默認使用 DaoThenticationProvider 來實現(xiàn) AuthenticationProvider肌割。DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProviderAuthenticate() 方法如下(只支持 UsernamePasswordAuthenticationToken):

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            () -> messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.onlySupports",
                    "Only UsernamePasswordAuthenticationToken is supported"));

    // Determine username
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

        try {
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
            logger.debug("User '" + username + "' not found");

            if (hideUserNotFoundExceptions) {
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
            else {
                throw notFound;
            }
        }

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

    try {
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
        if (cacheWasUsed) {
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        else {
            throw exception;
        }
    }

    postAuthenticationChecks.check(user);

    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

處理邏輯是首先根據(jù) Token authentication 獲得用戶名帐要,然后判斷是否有 UserDetail 緩存把敞,如果沒有,通過 retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication) 檢索得到 userDetail榨惠,如果沒有找到奋早,拋出 UsernameNotFoundException 異常或者 BadCredentialsException 異常赠橙。當有 userDetail 之后耽装,調(diào)用 DefaultPreAuthenticationCheckscheck() 函數(shù),即 preAuthenticationChecks.check(user) 用來測試 userDetail 賬號是否被鎖期揪,賬號是否不可用掉奄,賬號是否過期,并拋出相應(yīng)錯誤凤薛。完成之后挥萌,執(zhí)行 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication),這個函數(shù)是一個抽象函數(shù)枉侧,要求子類實現(xiàn)引瀑,增加更多的檢測。如果拋出 AuthenticationException 異常榨馁,并且是從緩存中獲得 userDetail 的話憨栽,會重新調(diào)用 retrieveUser 重新檢測,如果都不通過翼虫,才徹底拋出異常屑柔。如果沒有拋出,會執(zhí)行 postAuthenticationChecks.checks(user)珍剑,默認實現(xiàn)是 DefaultPostAuthenticationChecks 主要是檢查 密鑰是否過期掸宛,如過期,拋出 CredentialsExpiredException 異常招拙。最后調(diào)用 createSuccessAuthentication 創(chuàng)建 UsernamePasswordAuthenticationToken 并返回唧瘾。

createSuccessAuthentication() 函數(shù)如下:

    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

注意 CredentialsExpiredException棕硫、LockedException岸裙、DisabledExceptionAccountExpiredException都是 AccountStatusException 的子類贰拿,都會被 ProviderManager 捕獲并且直接拋出錯誤规哪。

DaoAuthenticationProvider 實現(xiàn)了 additionalAuthenticationChecks()求豫、 retrieveUser() 函數(shù),并且改寫了 createSuccessAuthentication() 函數(shù)。

additionalAuthenticationChecks() 如下:

protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");

        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }

    String presentedPassword = authentication.getCredentials().toString();

    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");

        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
}

如上蝠嘉,additionalAuthenticationChecks() 主要是增加了密碼驗證的邏輯最疆,如果驗證不通過,拋出 BadCredentialsException 錯誤蚤告。

retrieveUser() 函數(shù)主要實現(xiàn) userDetail 的獲取努酸。

protected final UserDetails retrieveUser(String username,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
    catch (UsernameNotFoundException ex) {
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    }
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    }
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

如上所示,主要是通過 this.getUserDetailsService().loadUserByUsername(username) 函數(shù)獲取罩缴,this.getUserDetailsService() 返回一個 UserDetailsService 接口實現(xiàn) UserDetail 的查詢。

createSuccessAuthentication() 主要增加了可否使用增強密鑰的判斷层扶,增加了安全性箫章。

protected Authentication createSuccessAuthentication(Object principal,
        Authentication authentication, UserDetails user) {
    boolean upgradeEncoding = this.userDetailsPasswordService != null
            && this.passwordEncoder.upgradeEncoding(user.getPassword());
    if (upgradeEncoding) {
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = this.passwordEncoder.encode(presentedPassword);
        user = this.userDetailsPasswordService.updatePassword(user, newPassword);
    }
    return super.createSuccessAuthentication(principal, authentication, user);
}

UserDetailsService 接口最常見的實現(xiàn)為 InMemoryUserDetailsManagerJdbcUserDetailsManager,前者在內(nèi)存中維護一個 <<String -> UserDetail>> 映射镜会,后者直接從數(shù)據(jù)庫中讀取數(shù)據(jù)檬寂,前者的 loadUserByUsername 實現(xiàn)如下:

public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
    UserDetails user = users.get(username.toLowerCase());

    if (user == null) {
        throw new UsernameNotFoundException(username);
    }

    return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
            user.isAccountNonExpired(), user.isCredentialsNonExpired(),
            user.isAccountNonLocked(), user.getAuthorities());
}

用于驗證的接口及類圖如下所示:

用于驗證的接口與類url圖

驗證流程如下:

驗證流程

回到 UsernamePasswordAuthenticationFilter,我們知道戳表,當驗證失敗時桶至,會拋出三種錯誤,第一種為 AccountStatusException匾旭,第二種為 InternalAuthenticationServiceException镣屹,第三種為 AuthenticationException。在 UsernamePasswordAuthenticationFilter 的第三步价涝,第四步女蜈,分別用兩個 catch 語句塊進行捕捉,進行錯誤處理然后直接返回色瘩,如下:

catch (InternalAuthenticationServiceException failed) {
        unsuccessfulAuthentication(request, response, failed);

    return;
}
//step 4
// AccountStatusException 為 AuthenticationException 子類伪窖,這個捕捉函數(shù)可以捕捉到 AccountStatusException 和 AuthenticationException 兩種異常
catch (AuthenticationException failed) {
    // Authentication failed
    unsuccessfulAuthentication(request, response, failed);
    return;
}

unsucessfulAuthentication() 函數(shù)的功能較簡單,即調(diào)用 rememberMeServices.loginFailfailureHandler.onAuthenticationFailure 設(shè)置失敗的操作居兆。

protected void unsuccessfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException failed)
        throws IOException, ServletException {
        SecurityContextHolder.clearContext();

    rememberMeServices.loginFail(request, response);

    failureHandler.onAuthenticationFailure(request, response, failed);
}

重點看 failureHandler.onAuthenticationFailure覆山。
failureHander 是實現(xiàn)了 AuthenticationFailureHandler 的類,默認實現(xiàn)是 SimpleUrlAuthenticationFailureHandler泥栖,onAuthenticationFailure 函數(shù)如下簇宽,可以看出其主要目的是實現(xiàn)跳轉(zhuǎn)或者重定向。

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

    if (defaultFailureUrl == null) {
        logger.debug("No failure URL set, sending 401 Unauthorized error");

        response.sendError(HttpStatus.UNAUTHORIZED.value(),
            HttpStatus.UNAUTHORIZED.getReasonPhrase());
    } else {
        saveException(request, exception);

        if (forwardToDestination) {
            logger.debug("Forwarding to " + defaultFailureUrl);

            request.getRequestDispatcher(defaultFailureUrl)
                    .forward(request, response);
        } else {
            logger.debug("Redirecting to " + defaultFailureUrl);
            redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
        }
    }
}

UsernamePasswordAuthenticationFilter 的第六步即認證成功后的護理吧享,主要是調(diào)用 successfulAuthentication() 函數(shù)進行處理晦毙。函數(shù)如下:

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

    SecurityContextHolder.getContext().setAuthentication(authResult);

    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }

    successHandler.onAuthenticationSuccess(request, response, authResult);
}

主要工作就是把密鑰存儲在 SecurityContextHolder 上,并調(diào)用 successHandler.onAuthenticationSuccess() 實現(xiàn)相關(guān)的操作耙蔑。與錯誤情況類似见妒,登陸成功也主要是進行一些跳轉(zhuǎn)。

AnonymousAuthenticationFilter

AnonymousAuthenticationFilter 的邏輯很簡單,當在 SecurityContextHolder 中沒有值時须揣,就創(chuàng)建一個匿名的 Token盐股,傳遞到下一個 Filter,代碼如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        SecurityContextHolder.getContext().setAuthentication(
                createAuthentication((HttpServletRequest) req));
    } else {}
    chain.doFilter(req, res);
}

protected Authentication createAuthentication(HttpServletRequest request) {
    AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
            principal, authorities);
    auth.setDetails(authenticationDetailsSource.buildDetails(request));

    return auth;
}

ExceptionTranslationFilter

FilterSecurityInterceptor

FilterSecurityInterceptor 繼承自 AbstractSecurityInterceptor耻卡。主要作用是通過 Filter 接口實現(xiàn)對 http 資源的控制疯汁。
FilterSecurityInterceptordoFilter 函數(shù)如下:

public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    invoke(fi);
}

其中,FilterInvocation 的主要作用是對 request, response, chain 的封裝卵酪。重點函數(shù)還是在 invoke() 上幌蚊。 invoke()函數(shù)如下所示:

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    // step 1
    if ((fi.getRequest() != null)
            && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
            && observeOncePerRequest) {
        // filter already applied to this request and user wants us to observe
        // once-per-request handling, so don't re-do security checking
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    } else {
        // step 2
        // first time this request being called, so perform security checking
        if (fi.getRequest() != null && observeOncePerRequest) {
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }

        // step 3
        InterceptorStatusToken token = super.beforeInvocation(fi);

        // step 4
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.finallyInvocation(token);
        }

        // step 5
        super.afterInvocation(token, null);
    }
}

其中,step 1,2 的作用主要是判斷是否是一次應(yīng)用并且已經(jīng)應(yīng)用的 request溃卡,如果是則直接進入下一個 Filter溢豆,如果不是并且還沒有應(yīng)用,則設(shè)應(yīng)用標志為 true 進行處理瘸羡。step 3 對 fi 做驗證漩仙,step 4,5 對 fi做一些其他的處理。

beforeInvocation() 的實現(xiàn)在父類 AbstractSecurityIntercepter 中犹赖,如下:

protected InterceptorStatusToken beforeInvocation(Object object) {

    if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException(
                "Security invocation attempted for object "
                        + object.getClass().getName()
                        + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                        + getSecureObjectClass());
    }

    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
            .getAttributes(object);

    if (attributes == null || attributes.isEmpty()) {
        if (rejectPublicInvocations) {
            throw new IllegalArgumentException(
                    "Secure object invocation "
                            + object
                            + " was denied as public invocations are not allowed via this interceptor. "
                            + "This indicates a configuration error because the "
                            + "rejectPublicInvocations property is set to 'true'");
        }

        if (debug) {
            logger.debug("Public object - authentication not attempted");
        }

        publishEvent(new PublicInvocationEvent(object));

        return null; // no further work post-invocation
    }

    if (debug) {
        logger.debug("Secure object: " + object + "; Attributes: " + attributes);
    }

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        credentialsNotFound(messages.getMessage(
                "AbstractSecurityInterceptor.authenticationNotFound",
                "An Authentication object was not found in the SecurityContext"),
                object, attributes);
    }

    Authentication authenticated = authenticateIfRequired();

    // Attempt authorization
    try {
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException accessDeniedException) {
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                accessDeniedException));

        throw accessDeniedException;
    }

    if (debug) {
        logger.debug("Authorization successful");
    }

    if (publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }

    // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
            attributes);

    if (runAs == null) {
        if (debug) {
            logger.debug("RunAsManager did not change Authentication object");
        }

        // no further work post-invocation
        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                attributes, object);
    } else {
        if (debug) {
            logger.debug("Switching to RunAs Authentication: " + runAs);
        }

        SecurityContext origCtx = SecurityContextHolder.getContext();
        SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
        SecurityContextHolder.getContext().setAuthentication(runAs);

        // need to revert to token.Authenticated post-invocation
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
}

主要是調(diào)用 this.accessDecisionManager.decide(authenticated, object, attributes) 進行授權(quán)队他。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市峻村,隨后出現(xiàn)的幾起案子麸折,更是在濱河造成了極大的恐慌,老刑警劉巖粘昨,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件磕谅,死亡現(xiàn)場離奇詭異,居然都是意外死亡雾棺,警方通過查閱死者的電腦和手機膊夹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捌浩,“玉大人放刨,你說我怎么就攤上這事∈龋” “怎么了进统?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長浪听。 經(jīng)常有香客問我螟碎,道長,這世上最難降的妖魔是什么迹栓? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任掉分,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酥郭。我一直安慰自己华坦,他們只是感情好,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布不从。 她就那樣靜靜地躺著惜姐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪椿息。 梳的紋絲不亂的頭發(fā)上歹袁,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音寝优,去河邊找鬼条舔。 笑死,一個胖子當著我的面吹牛倡勇,可吹牛的內(nèi)容都是我干的逞刷。 我是一名探鬼主播嘉涌,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼妻熊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了仑最?” 一聲冷哼從身側(cè)響起扔役,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎警医,沒想到半個月后亿胸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡预皇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年侈玄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吟温。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡序仙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鲁豪,到底是詐尸還是另有隱情潘悼,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布爬橡,位于F島的核電站治唤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏糙申。R本人自食惡果不足惜宾添,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辞槐,春花似錦掷漱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鹿榜,卻和暖如春海雪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舱殿。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工奥裸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沪袭。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓湾宙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親冈绊。 傳聞我的和親對象是個殘疾皇子侠鳄,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

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