SpringSecurity實現(xiàn)多種登錄方式,郵件驗證碼塘偎、電話號碼登錄

不知道疗涉, 你在用Spring Security的時候拿霉,有沒有想過,用它實現(xiàn)多種登錄方式勒咱扣,這次我的小伙伴就給我提了一些登錄方面的需求绽淘,需要在原有賬號密碼登錄的基礎上,另外實現(xiàn)電話驗證碼以及郵件驗證碼登錄闹伪,以及在實現(xiàn)之后沪铭,讓我能夠做到實現(xiàn)第三方登錄,如gitee偏瓤、github等杀怠。

本文主要是講解Security在實現(xiàn)賬號密碼的基礎上,并且不改變原有業(yè)務情況下厅克,實現(xiàn)郵件赔退、電話驗證碼登錄。

前言:
這對于已經學過一段時間证舟,并且對Security已經有了解的小伙伴來說硕旗,還是比較合適的,但是對于我以及其他一些急于解決當下問題的小白女责,并不是那么友善漆枚。

一、理論知識
我們先思考一下這個流程大致是如何的抵知?

填寫郵件號碼墙基,獲取驗證碼
輸入獲取到的驗證碼進行登錄(登錄的接口:/email/login,這里不能使用默認的/login刷喜,因為我們是擴展)
在自定義的過濾器EmailCodeAuthenticationFilter中獲取發(fā)送過來的郵件號碼及驗證碼碘橘,判斷驗證碼是否正確,郵件賬號是否為空等
封裝成一個需要認證的Authentication吱肌,此處我們自定義實現(xiàn)為EmailCodeAuthenticationToken。
將 Authentiction 傳給 AuthenticationManager接口中authenticate 方法進行認證處理
AuthenticationManager 默認是實現(xiàn)類為 ProviderManager 仰禽,ProviderManager 又委托給 AuthenticationProvider 進行處理
我們自定義一個EmailCodeAuthenticationProvider實現(xiàn)AuthenticationProvider,實現(xiàn)身份驗證氮墨。
自定義的EmailCodeAuthenticationFilter繼承了 AbstractAuthenticationProcessingFilter 抽象類,AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中對登錄成功進行了處理吐葵,通過 SecurityContextHolder.getContext().setAuthentication() 方法將 Authentication 認證信息對象綁定到 SecurityContext即安全上下文中规揪。
其實對于身份驗證通過后的處理,有兩種方案温峭,一種是直接在過濾器重寫successfulAuthentication猛铅,另外一種就是實現(xiàn)AuthenticationSuccessHandler來處理身份驗證通過。
身份驗證失敗也是一樣凤藏,可重寫unsuccessfulAuthentication方法奸忽,也可以實現(xiàn) AuthenticationFailureHandler來對身份驗證失敗進行處理堕伪。
大致流程就是如此。從這個流程中我們可以知道栗菜,需要重寫的組件有以下幾個:

EmailCodeAuthenticationFilter:郵件驗證登錄過濾器
EmailCodeAuthenticationToken:身份驗證令牌
EmailCodeAuthenticationProvider:郵件身份認證處理
AuthenticationSuccessHandler:處理登錄成功操作
AuthenticationFailureHandler:處理登錄失敗操作
接下來欠雌,我是模仿著源碼寫出我的代碼,建議大家可以在使用的時候疙筹,多去看看富俄,我這里去除了一些不是和這個相關的代碼。

來吧6亍霍比!

二、EmailCodeAuthenticationFilter
我們需要重寫的
EmailCodeAuthenticationFilter暴备,實際繼承了AbstractAuthenticationProcessingFilter抽象類悠瞬,我們不會寫,可以先看看它的默認實現(xiàn)UsernamePasswordAuthenticationFilter是怎么樣的嗎馍驯,抄作業(yè)這是大家的強項的哈阁危。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
        "POST");
//從前臺傳過來的參數(shù)
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

private boolean postOnly = true;

//  初始化一個用戶密碼 認證過濾器  默認的登錄uri 是 /login 請求方式是POST
public UsernamePasswordAuthenticationFilter() {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}

public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}

/**
執(zhí)行實際身份驗證。實現(xiàn)應執(zhí)行以下操作之一:
1汰瘫、為經過身份驗證的用戶返回填充的身份驗證令牌狂打,表示身份驗證成功
2、返回null混弥,表示認證過程還在進行中趴乡。 在返回之前,實現(xiàn)應該執(zhí)行完成流程所需的任何額外工作蝗拿。
3晾捏、如果身份驗證過程失敗,則拋出AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    String username = obtainUsername(request);
    username = (username != null) ? username : "";
    username = username.trim();
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    //生成 UsernamePasswordAuthenticationToken 稍后交由AuthenticationManager中的authenticate進行認證
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    // 可以放一些其他信息進去
    setDetails(request, authRequest);
    return this.getAuthenticationManager().authenticate(authRequest);
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
    return request.getParameter(this.passwordParameter);
}

@Nullable
protected String obtainUsername(HttpServletRequest request) {
    return request.getParameter(this.usernameParameter);
}

protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
    authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

//set哀托、get方法

}
接下來我們就抄個作業(yè)哈:

package com.crush.security.auth.email_code;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;

/**

  • @Author: crush

  • @Date: 2021-09-08 21:13

  • version 1.0
    /
    public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /
    *

    • 前端傳來的 參數(shù)名 - 用于request.getParameter 獲取
      */
      private final String DEFAULT_EMAIL_NAME="email";

    private final String DEFAULT_EMAIL_CODE="e_code";

    @Autowired
    @Override
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
    super.setAuthenticationManager(authenticationManager);
    }
    /**

    • 是否 僅僅post方式
      */
      private boolean postOnly = true;

    /**

    • 通過 傳入的 參數(shù) 創(chuàng)建 匹配器
    • 即 Filter過濾的url
      */
      public EmailCodeAuthenticationFilter() {
      super(new AntPathRequestMatcher("/email/login","POST"));
      }
/**
 * filter 獲得 用戶名(郵箱) 和 密碼(驗證碼) 裝配到 token 上 惦辛,
 * 然后把token 交給 provider 進行授權
 */
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    if(postOnly && !request.getMethod().equals("POST") ){
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }else{
        String email = getEmail(request);
        if(email == null){
            email = "";
        }
        email = email.trim();
        //如果 驗證碼不相等 故意讓token出錯 然后走springsecurity 錯誤的流程
        boolean flag = checkCode(request);
        //封裝 token
        EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());
        this.setDetails(request,token);
        //交給 manager 發(fā)證
        return this.getAuthenticationManager().authenticate(token);
    }
}

/**
 * 獲取 頭部信息 讓合適的provider 來驗證他
 */
public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
    token.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

/**
 * 獲取 傳來 的Email信息
 */
public String getEmail(HttpServletRequest request ){
    String result=  request.getParameter(DEFAULT_EMAIL_NAME);
    return result;
}

/**
 * 判斷 傳來的 驗證碼信息 以及 session 中的驗證碼信息
 */
public boolean checkCode(HttpServletRequest request ){
    String code1 = request.getParameter(DEFAULT_EMAIL_CODE);
    System.out.println("code1**********"+code1);
    // TODO 另外再寫一個鏈接 生成 驗證碼 那個驗證碼 在生成的時候  存進redis 中去
    //TODO  這里的驗證碼 寫在Redis中, 到時候取出來判斷即可 驗證之后 刪除驗證碼
    if(code1.equals("123456")){
        return true;
    }
    return false;
}
// set仓手、get方法...

}
三胖齐、EmailCodeAuthenticationToken
我們
EmailCodeAuthenticationToken是繼承AbstractAuthenticationToken的,按照同樣的方式嗽冒,我們接著去看看AbstractAuthenticationToken的默認實現(xiàn)是什么樣的就行了呀伙。

/**

*/
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

// 這里指的賬號密碼哈
private final Object principal;

private Object credentials;

/**
沒經過身份驗證時,初始化權限為空添坊,setAuthenticated(false)設置為不可信令牌
 */
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super(null);
    this.principal = principal;
    this.credentials = credentials;
    setAuthenticated(false);
}

/**
經過身份驗證后剿另,將權限放進去,setAuthenticated(true)設置為可信令牌
 */
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
        Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}

@Override
public Object getCredentials() {
    return this.credentials;
}

@Override
public Object getPrincipal() {
    return this.principal;
}

@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    Assert.isTrue(!isAuthenticated,
            "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
    super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
    super.eraseCredentials();
    this.credentials = null;
}

}
日常抄作業(yè)哈:

/**

  • @Author: crush
  • @Date: 2021-09-08 21:13
  • version 1.0
    */
    public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {
/**
 * 這里的 principal 指的是 email 地址(未認證的時候)
 */
private final Object principal;

public EmailCodeAuthenticationToken(Object principal) {
    super((Collection) null);
    this.principal = principal;
    setAuthenticated(false);
}

public EmailCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    super.setAuthenticated(true);
}

@Override
public Object getCredentials() {
    return null;
}

@Override
public Object getPrincipal() {
    return this.principal;
}

@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    if (isAuthenticated) {
        throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
    } else {
        super.setAuthenticated(false);
    }
}

}
這個很簡單的哈。

四雨女、EmailCodeAuthenticationProvider
自定義的
EmailCodeAuthenticationProvider是實現(xiàn)了AuthenticationProvider接口谚攒,抄作業(yè)就得學會看看源碼。我們接著來戚篙。

4.1五鲫、先看看AbstractUserDetailsAuthenticationProvider,我們再來模仿
AuthenticationProvider接口有很多實現(xiàn)類岔擂,不一一說明了位喂,直接看我們需要看的
AbstractUserDetailsAuthenticationProvider, 該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求。但是它是一個抽象類乱灵,但其實就一個步驟在它的實現(xiàn)類中實現(xiàn)的塑崖,很簡單,稍后會講到痛倚。

在這個源碼中我把和檢查相關的一些操作都給刪除规婆,只留下幾個重點,我們一起來看一看哈蝉稳。

//該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求抒蚜。
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {

protected final Log logger = LogFactory.getLog(getClass());

private UserCache userCache = new NullUserCache();

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                    "Only UsernamePasswordAuthenticationToken is supported"));
    //獲取用戶名
    String username = determineUsername(authentication);
    //判斷緩存中是否存在
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;
        try {
            // 緩存中沒有 通過字類實現(xiàn)的retrieveUser 從數(shù)據(jù)庫進行檢索,返回一個 UserDetails 對象
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException ex) {
            this.logger.debug("Failed to find user '" + username + "'");
            if (!this.hideUserNotFoundExceptions) {
                throw ex;
            }
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }
    try {
        //進行相關檢查  因為可能是從緩存中取出來的 并非是最新的
        this.preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException ex) {
        if (!cacheWasUsed) {
            throw ex;
        }
        // 沒有通過檢查耘戚, 重新檢索最新的數(shù)據(jù)
        cacheWasUsed = false;
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        this.preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    // 再次進行檢查
    this.postAuthenticationChecks.check(user);
    // 存進緩存中去
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    //創(chuàng)建一個可信的身份令牌返回
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

private String determineUsername(Authentication authentication) {
    return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}

/**

簡而言之就是創(chuàng)建了一個通過身份驗證的UsernamePasswordAuthenticationToken
*/
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}

/**

允許子類從特定于實現(xiàn)的位置實際檢索UserDetails 嗡髓,如果提供的憑據(jù)不正確,則可以選擇立即拋出AuthenticationException (如果需要以用戶身份綁定到資源以獲得或生成一個UserDetails )
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
//...

//簡而言之:當然有時候我們有多個不同的 `AuthenticationProvider`收津,它們分別支持不同的 `Authentication`對象饿这,那么當一個具體的 `AuthenticationProvier`傳進入 `ProviderManager`的內部時,就會在 `AuthenticationProvider`列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證
@Override
public boolean supports(Class<?> authentication) {
    return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}

}

關于protected abstract UserDetails retrieveUser的實現(xiàn)撞秋,
AbstractUserDetailsAuthenticationProvider實現(xiàn)是DaoAuthenticationProvider.

DaoAuthenticationProvider主要操作是兩個长捧,第一個是從數(shù)據(jù)庫中檢索出相關信息,第二個是給檢索出的用戶信息進行密碼的加密操作吻贿。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

private UserDetailsService userDetailsService;

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        // 檢索用戶串结,一般我們都會實現(xiàn) UserDetailsService接口,改為從數(shù)據(jù)庫中檢索用戶信息 返回安全核心類 UserDetails
        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);
    }
}

@Override
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);
}

}

4.2舅列、抄作業(yè)啦
看完源碼奉芦,其實我們如果要重寫的話,主要要做到以下幾個事情:

重寫public boolean supports(Class<?> authentication)方法剧蹂。 有時候我們有多個不同的 AuthenticationProvider,它們分別支持不同的 Authentication對象烦却,那么當一個具體的 AuthenticationProvier傳進入 ProviderManager的內部時宠叼,就會在 AuthenticationProvider列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證 簡單說就是指定AuthenticationProvider驗證哪個Authentication對象。如指定DaoAuthenticationProvider認證UsernamePasswordAuthenticationToken, 所以我們指定EmailCodeAuthenticationProvider認證EmailCodeAuthenticationToken冒冬。
檢索數(shù)據(jù)庫伸蚯,返回一個安全核心類UserDetail。
創(chuàng)建一個經過身份驗證的Authentication對象
了解要做什么事情了简烤,我們就可以動手看看代碼啦剂邮。

/**

  • @Author: crush

  • @Date: 2021-09-08 21:14

  • version 1.0
    */
    @Slf4j
    public class EmailCodeAuthenticationProvider implements AuthenticationProvider {

    ITbUserService userService;

    public EmailCodeAuthenticationProvider(ITbUserService userService) {
    this.userService = userService;
    }

/**
 * 認證
 */
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    if (!supports(authentication.getClass())) {
        return null;
    }
    log.info("EmailCodeAuthentication authentication request: %s", authentication);
    EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;

    UserDetails user = userService.getByEmail((String) token.getPrincipal());

    System.out.println(token.getPrincipal());
    if (user == null) {
        throw new InternalAuthenticationServiceException("無法獲取用戶信息");
    }
    System.out.println(user.getAuthorities());
    EmailCodeAuthenticationToken result =
            new EmailCodeAuthenticationToken(user, user.getAuthorities());
            /*
            Details 中包含了 ip地址、 sessionId 等等屬性 也可以存儲一些自己想要放進去的內容
            */
    result.setDetails(token.getDetails());
    return result;
}

@Override
public boolean supports(Class<?> aClass) {
    return EmailCodeAuthenticationToken.class.isAssignableFrom(aClass);
}

}
五横侦、在配置類中進行配置
主要就是做下面幾件事:

將過濾器挥萌、認證器注入到spring中
將登錄成功處理、登錄失敗處理器注入到Spring中报辱,或者在自定義過濾器中對登錄成功和失敗進行處理畸颅。
添加到過濾鏈中
@Bean
public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter() {
EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter();
emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
return emailCodeAuthenticationFilter;
}

@Bean
public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider() {
    return new EmailCodeAuthenticationProvider(userService);
}

/**
 * 因為使用了BCryptPasswordEncoder來進行密碼的加密舰罚,所以身份驗證的時候也的用他來判斷哈、憨栽,
 *
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    //authenticationProvider 根據(jù)傳入的自定義AuthenticationProvider添加身份AuthenticationProvider 。
    auth.authenticationProvider(emailCodeAuthenticationProvider())翼虫;
}

.and()
.authenticationProvider(emailCodeAuthenticationProvider())
.addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

.authenticationProvider(mobileCodeAuthenticationProvider())
.addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

六屑柔、測試及源代碼
項目具體的配置、啟動方式珍剑、環(huán)境等掸宛、都在github及gitee的文檔上有詳細說明。

源代碼中包含sql文件次慢、配置文件以及相關博客鏈接旁涤,源代碼中也加了很多注釋,盡最大程度讓大家能夠看明白迫像。

在最大程度上保證大家都能正確的運行及測試劈愚。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市闻妓,隨后出現(xiàn)的幾起案子菌羽,更是在濱河造成了極大的恐慌,老刑警劉巖由缆,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件注祖,死亡現(xiàn)場離奇詭異,居然都是意外死亡均唉,警方通過查閱死者的電腦和手機是晨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舔箭,“玉大人罩缴,你說我怎么就攤上這事蚊逢。” “怎么了箫章?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵烙荷,是天一觀的道長。 經常有香客問我檬寂,道長终抽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任桶至,我火速辦了婚禮昼伴,結果婚禮上,老公的妹妹穿的比我還像新娘塞茅。我一直安慰自己亩码,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布野瘦。 她就那樣靜靜地躺著描沟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鞭光。 梳的紋絲不亂的頭發(fā)上吏廉,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音惰许,去河邊找鬼席覆。 笑死,一個胖子當著我的面吹牛汹买,可吹牛的內容都是我干的佩伤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼晦毙,長吁一口氣:“原來是場噩夢啊……” “哼生巡!你這毒婦竟也來了?” 一聲冷哼從身側響起见妒,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤孤荣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后须揣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盐股,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年耻卡,在試婚紗的時候發(fā)現(xiàn)自己被綠了疯汁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡卵酪,死狀恐怖涛目,靈堂內的尸體忽然破棺而出秸谢,到底是詐尸還是另有隱情,我是刑警寧澤霹肝,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站塑煎,受9級特大地震影響沫换,放射性物質發(fā)生泄漏。R本人自食惡果不足惜最铁,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一讯赏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冷尉,春花似錦漱挎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雾棺,卻和暖如春膊夹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捌浩。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工放刨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尸饺。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓进统,卻偏偏與公主長得像,于是被迫代替她去往敵國和親浪听。 傳聞我的和親對象是個殘疾皇子螟碎,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容