spring gateway集成spring security

spring gateway

分布式開發(fā)時(shí)储耐,微服務(wù)會(huì)有很多揍诽,但是網(wǎng)關(guān)是請(qǐng)求的第一入口籽慢,所以一般會(huì)把客戶端請(qǐng)求的權(quán)限驗(yàn)證統(tǒng)一放在網(wǎng)關(guān)進(jìn)行認(rèn)證與鑒權(quán)酸役。SpringCloud Gateway 作為 Spring Cloud 生態(tài)系統(tǒng)中的網(wǎng)關(guān)住诸,目標(biāo)是替代 Zuul,為了提升網(wǎng)關(guān)的性能涣澡,SpringCloud Gateway是基于WebFlux框架實(shí)現(xiàn)的贱呐,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。

注意:

由于web容器不同入桂,在gateway項(xiàng)目中使用的webflux奄薇,是不能和spring-web混合使用的。

Spring MVC和WebFlux的區(qū)別
11772383-b70d80a3893f3a04.png

依賴:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

配置spring security

spring security設(shè)置要采用響應(yīng)式配置事格,基于WebFlux中WebFilter實(shí)現(xiàn)惕艳,與Spring MVC的Security是通過(guò)Servlet的Filter實(shí)現(xiàn)類似搞隐,也是一系列filter組成的過(guò)濾鏈。

  1. 部分概念是對(duì)應(yīng)的:
Reactive Web
@EnableWebFluxSecurity @EnableWebSecurity
ReactiveSecurityContextHolder SecurityContextHolder
AuthenticationWebFilter FilterSecurityInterceptor
ReactiveAuthenticationManager AuthenticationManager
ReactiveUserDetailsService UserDetailsService
ReactiveAuthorizationManager AccessDecisionManager
  1. 首先需要配置@EnableWebFluxSecurity注解远搪,開啟Spring WebFlux Security的支持
import java.util.LinkedList;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;

/**
* @Author: pilsy
* @Date: 2020/6/29 0029 16:54
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

   @Autowired
   private AuthenticationConverter authenticationConverter;

   @Autowired
   private AuthorizeConfigManager authorizeConfigManager;

   @Autowired
   private AuthEntryPointException serverAuthenticationEntryPoint;

   @Autowired
   private JsonServerAuthenticationSuccessHandler jsonServerAuthenticationSuccessHandler;

   @Autowired
   private JsonServerAuthenticationFailureHandler jsonServerAuthenticationFailureHandler;

   @Autowired
   private JsonServerLogoutSuccessHandler jsonServerLogoutSuccessHandler;

   @Autowired
   private AuthenticationManager authenticationManager;

   private static final String[] AUTH_WHITELIST = new String[]{"/login", "/logout"};

   @Bean
   public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
       SecurityWebFilterChain chain = http.formLogin()
               .loginPage("/login")
               // 登錄成功handler
               .authenticationSuccessHandler(jsonServerAuthenticationSuccessHandler)
               // 登陸失敗handler
               .authenticationFailureHandler(jsonServerAuthenticationFailureHandler)
               // 無(wú)訪問(wèn)權(quán)限handler
               .authenticationEntryPoint(serverAuthenticationEntryPoint)
               .and()
               .logout()
               // 登出成功handler
               .logoutSuccessHandler(jsonServerLogoutSuccessHandler)
               .and()
               .csrf().disable()
               .httpBasic().disable()
               .authorizeExchange()
               // 白名單放行
               .pathMatchers(AUTH_WHITELIST).permitAll()
               // 訪問(wèn)權(quán)限控制
               .anyExchange().access(authorizeConfigManager)
               .and().build();
       // 設(shè)置自定義登錄參數(shù)轉(zhuǎn)換器
       chain.getWebFilters()
               .filter(webFilter -> webFilter instanceof AuthenticationWebFilter)
               .subscribe(webFilter -> {
                   AuthenticationWebFilter filter = (AuthenticationWebFilter) webFilter;
                   filter.setServerAuthenticationConverter(authenticationConverter);
               });
       return chain;
   }

   /**
    * 注冊(cè)用戶信息驗(yàn)證管理器劣纲,可按需求添加多個(gè)按順序執(zhí)行
    * @return 
    */
   @Bean
   ReactiveAuthenticationManager reactiveAuthenticationManager() {
       LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
       managers.add(authenticationManager);
       return new DelegatingReactiveAuthenticationManager(managers);
   }


   /**
    * BCrypt密碼編碼
    * @return 
    */
   @Bean
   public BCryptPasswordEncoder bcryptPasswordEncoder() {
       return new BCryptPasswordEncoder();
   }

}
  1. 特殊handler的實(shí)現(xiàn)
  • JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import io.netty.util.CharsetUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * @Author: pilsy
 * @Date: 2020/6/29 0029 17:39
 */
@Component
public class JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        // 登錄成功后可以放入一些參數(shù)到session中
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        String body = JSONObject.toJSONString(AjaxResult.ok("登錄成功!"));
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

  • JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * @Author: pilsy
 * @Date: 2020/6/29 0029 17:44
 */
@Component
public class JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

    private static final String USER_NOT_EXISTS = "用戶不存在谁鳍!";

    private static final String USERNAME_PASSWORD_ERROR = "用戶密碼錯(cuò)誤癞季!";

    private static final String USER_LOCKED = "用戶鎖定!";

    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        if (exception instanceof UsernameNotFoundException) {
            return writeErrorMessage(response, USER_NOT_EXISTS);
        } else if (exception instanceof BadCredentialsException) {
            return writeErrorMessage(response, USERNAME_PASSWORD_ERROR);
        } else if (exception instanceof LockedException) {
            return writeErrorMessage(response, USER_LOCKED);
        }
        return writeErrorMessage(response, exception.getMessage());
    }

    private Mono<Void> writeErrorMessage(ServerHttpResponse response, String message) {
        String result = JSONObject.toJSONString(AjaxResult.restResult(message, ApiErrorCode.FAILED));
        DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

  • JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;

import io.netty.util.CharsetUtil;
import reactor.core.publisher.Mono;

/**
 * @Author: pilsy
 * @Date: 2020/7/10 0010 15:05
 */
@Component
public class JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {
    @Override
    public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
        ServerHttpResponse response = exchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        String result = JSONObject.toJSONString(AjaxResult.restResult("注銷成功", ApiErrorCode.SUCCESS));
        DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
  • AuthEntryPointException implements ServerAuthenticationEntryPoint
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * 無(wú)訪問(wèn)權(quán)限的返回結(jié)果
 *
 * @author pilsy
 */
@Component
public class AuthEntryPointException implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
        AjaxResult<String> ajaxResult = AjaxResult.restResult(e.getMessage(), ApiErrorCode.FAILED);
        String body = JSONObject.toJSONString(ajaxResult);
        DataBuffer wrap = exchange.getResponse().bufferFactory().wrap(body.getBytes(CharsetUtil.UTF_8));
        return exchange.getResponse().writeWith(Flux.just(wrap));
    }
}
  1. 表單登陸時(shí)security默認(rèn)只會(huì)獲取了username倘潜,password參數(shù)绷柒,但有時(shí)候需要一些特殊屬性,所以需要覆蓋默認(rèn)獲取的表單參數(shù)的Converter
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 將表單參數(shù)轉(zhuǎn)換為AuthenticationToken
 *
 * @Author: pilsy
 * @Date: 2020/7/15 0015 15:41
 */
@Component
public class AuthenticationConverter extends ServerFormLoginAuthenticationConverter {

    private String usernameParameter = "username";

    private String passwordParameter = "password";

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        String tenant = headers.getFirst("_tenant");
        String host = headers.getHost().getHostName();
        return exchange.getFormData()
                .map(data -> {
                    String username = data.getFirst(this.usernameParameter);
                    String password = data.getFirst(this.passwordParameter);
                    return new AuthenticationToken(username, password, tenant, host);
                });
    }

}
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 存儲(chǔ)用戶信息的token
 *
 * @Author: pilsy
 * @Date: 2020/7/15 0015 16:08
 */
@SuppressWarnings("serial")
@Getter
@Setter
public class AuthenticationToken extends UsernamePasswordAuthenticationToken {

    private String tenant;

    private String host;

    public AuthenticationToken(Object principal, Object credentials, String tenant, String host) {
        super(principal, credentials);
        this.tenant = tenant;
        this.host = host;
    }

    public AuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public AuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

  1. 驗(yàn)證用戶身份
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * 驗(yàn)證用戶
 *
 * @Author: pilsy
 * @Date: 2020/7/15 0015 16:43
 */
@Component
public class AuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {

    private Scheduler scheduler = Schedulers.boundedElastic();

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Autowired
    private MySqlReactiveUserDetailsServiceImpl mySqlReactiveUserDetailsService;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        AuthenticationToken token = (AuthenticationToken) authentication;
        final String username = authentication.getName();
        final String presentedPassword = (String) authentication.getCredentials();
        final String tenant = token.getTenant();
        final String host = token.getHost();
        return retrieveUser(username)
                .publishOn(scheduler)
                .filter(u -> passwordEncoder.matches(presentedPassword, u.getPassword()))
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
                .flatMap(u -> {
                    boolean upgradeEncoding = mySqlReactiveUserDetailsService != null
                            && passwordEncoder.upgradeEncoding(u.getPassword());
                    if (upgradeEncoding) {
                        String newPassword = passwordEncoder.encode(presentedPassword);
                        return mySqlReactiveUserDetailsService.updatePassword(u, newPassword);
                    }
                    return Mono.just(u);
                })
                .flatMap(userDetails -> {
                    // 省略業(yè)務(wù)代碼
                    return Mono.just(userDetails);
                })
                .map(u -> new AuthenticationToken(u, u.getPassword(), u.getAuthorities()));
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return mySqlReactiveUserDetailsService.findByUsername(username);
    }
}
import com.gsoft.foa.gateway.repository.AccountInfoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * 身份認(rèn)證類
 *
 * @Author: pilsy
 * @Date: 2020/6/29 0029 18:01
 */
@Slf4j
@Component
public class MySqlReactiveUserDetailsServiceImpl implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {

    private static final String USER_NOT_EXISTS = "用戶不存在涮因!";

    private final AccountInfoRepository accountInfoRepository;

    public MySqlReactiveUserDetailsServiceImpl(AccountInfoRepository accountInfoRepository) {
        this.accountInfoRepository = accountInfoRepository;
    }

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        return accountInfoRepository.findByUsername(username)
                .switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
                .doOnNext(u -> log.info(
                        String.format("查詢賬號(hào)成功  user:%s password:%s", u.getUsername(), u.getPassword())))
                .cast(UserDetails.class);
    }

    @Override
    public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
        return accountInfoRepository.findByUsername(user.getUsername())
                .switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
                .map(foundedUser -> {
                    foundedUser.setPassword(bCryptPasswordEncoder.encode(newPassword));
                    return foundedUser;
                })
                .flatMap(updatedUser -> accountInfoRepository.save(updatedUser))
                .cast(UserDetails.class);
    }
}
  1. 鑒權(quán)

import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collection;

/**
 * API請(qǐng)求權(quán)限校驗(yàn)配置類
 *
 * @Author: pilsy
 * @Date: 2020/7/1 0001 18:27
 */
@Slf4j
@Component
public class AuthorizeConfigManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication,
                                             AuthorizationContext authorizationContext) {
        return authentication.map(auth -> {
            ServerWebExchange exchange = authorizationContext.getExchange();
            ServerHttpRequest request = exchange.getRequest();

            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                String authorityAuthority = authority.getAuthority();
                String path = request.getURI().getPath();
                if (antPathMatcher.match(authorityAuthority, path)) {
                    log.info(String.format("用戶請(qǐng)求API校驗(yàn)通過(guò)废睦,GrantedAuthority:{%s}  Path:{%s} ", authorityAuthority, path));
                    return new AuthorizationDecision(true);
                }
            }
            return new AuthorizationDecision(false);
        }).defaultIfEmpty(new AuthorizationDecision(false));
    }

    @Override
    public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
        return check(authentication, object)
                .filter(d -> d.isGranted())
                .switchIfEmpty(Mono.defer(() -> {
                    AjaxResult<String> ajaxResult = AjaxResult.restResult("當(dāng)前用戶沒(méi)有訪問(wèn)權(quán)限! ", ApiErrorCode.FAILED);
                    String body = JSONObject.toJSONString(ajaxResult);
                    return Mono.error(new AccessDeniedException(body));
                }))
                .flatMap(d -> Mono.empty());
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市养泡,隨后出現(xiàn)的幾起案子嗜湃,更是在濱河造成了極大的恐慌,老刑警劉巖澜掩,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件购披,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡肩榕,警方通過(guò)查閱死者的電腦和手機(jī)刚陡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)株汉,“玉大人筐乳,你說(shuō)我怎么就攤上這事±商樱” “怎么了哥童?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)褒翰。 經(jīng)常有香客問(wèn)我贮懈,道長(zhǎng),這世上最難降的妖魔是什么优训? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任朵你,我火速辦了婚禮,結(jié)果婚禮上揣非,老公的妹妹穿的比我還像新娘抡医。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布忌傻。 她就那樣靜靜地躺著大脉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪水孩。 梳的紋絲不亂的頭發(fā)上镰矿,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音俘种,去河邊找鬼秤标。 笑死,一個(gè)胖子當(dāng)著我的面吹牛宙刘,可吹牛的內(nèi)容都是我干的苍姜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼悬包,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼衙猪!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起玉罐,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤屈嗤,失蹤者是張志新(化名)和其女友劉穎潘拨,沒(méi)想到半個(gè)月后吊输,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铁追,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年季蚂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琅束。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扭屁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涩禀,到底是詐尸還是另有隱情料滥,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布艾船,位于F島的核電站葵腹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏屿岂。R本人自食惡果不足惜践宴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望爷怀。 院中可真熱鬧阻肩,春花似錦、人聲如沸运授。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至柒室,卻和暖如春瘤缩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伦泥。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工剥啤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人不脯。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓府怯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親防楷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子牺丙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344