springboot整合springsecurity從Hello World到源碼解析(五):springsecurity+jwt整合restful服務(wù)

cover


上一章我們?cè)敿?xì)介紹了springsecurity的基礎(chǔ)結(jié)構(gòu),并且在第三章我們已經(jīng)知道了springsecurity中的
基礎(chǔ)配置,但是那是基于all in one模式的,前端頁(yè)面和后端集成在一起,然而事實(shí)上現(xiàn)在越來(lái)越多的開(kāi)發(fā)模式是前后端分離,后端提供的restful接口,那我們本章就來(lái)學(xué)習(xí)下
springsecurity+jwt整合restful服務(wù)。

springboot整合springsecurity系列文章:
springboot整合springsecurity從Hello World到源碼解析(一):hello world程序入門
springboot整合springsecurity從Hello World到源碼解析(二):springsecurity配置加載解析
springboot整合springsecurity從Hello World到源碼解析(三):基礎(chǔ)配置詳解
springboot整合springsecurity從Hello World到源碼解析(四):springsecurity基礎(chǔ)架構(gòu)解析
springboot整合springsecurity從Hello World到源碼解析(五):springsecurity+jwt整合restful服務(wù)

jwt

首先我們先了解下什么是jwt,jwt全稱是json web tokens,它是基于RFC 7519開(kāi)放標(biāo)準(zhǔn)用于雙方安全展示信息的一種方式。通俗說(shuō)就是是用于服務(wù)端和客戶端相互交換信息的一種憑證。
這個(gè)憑證我們就叫token,在傳統(tǒng)的認(rèn)證模式中,我們普遍的做法是這樣的:
用戶登錄-》服務(wù)端生成session-》寫入sessionId到cookie-》瀏覽器攜帶該session訪問(wèn)服務(wù)端-》服務(wù)器基于該sessionId查找信息-》認(rèn)證通過(guò)(找到了)
這種做法存在如下問(wèn)題:

  1. 服務(wù)端需一定資源保存session信息,用戶多時(shí)資源消耗較大
  2. 擴(kuò)展性不好,當(dāng)我們的服務(wù)端需要集群時(shí),因session保存在服務(wù)端,此時(shí)無(wú)法定位session,造成登錄失效(傳統(tǒng)解決辦法:iphash,session寫入redis等)
  3. 跨域問(wèn)題,當(dāng)我們?cè)L問(wèn)A網(wǎng)站時(shí),此時(shí)不想再登錄就能夠訪問(wèn)關(guān)聯(lián)網(wǎng)站B。(傳統(tǒng)解決辦法:寫入持久層,A,B同時(shí)訪問(wèn))
    雖然上面面對(duì)的問(wèn)題我們普遍都有解決辦法,但是顯然都不怎么 ”友好“,所以這個(gè)時(shí)候就有一種干脆的解決辦法了,服務(wù)端不再保存session,這樣就輕松解決了上面所有問(wèn)題。
    服務(wù)端只頒發(fā)token,那么現(xiàn)在的流程變成了這樣:
    用戶登錄-》服務(wù)端頒發(fā)token-》客戶端保存token(放入cooken或者h(yuǎn)eader)-》攜帶token訪問(wèn)服務(wù)端-》服務(wù)端驗(yàn)證token(通過(guò))-》調(diào)用api-》獲取信息
    那么既然是服務(wù)端頒發(fā)的token,那肯定要保證該token的安全(只有該服務(wù)端頒發(fā)的token才認(rèn)),唯一(不能偽造),而jwt則是用來(lái)生成這個(gè)安全的token的。 jwt的組成如下:
  • header(頭),保存算法,類型
  • payload(負(fù)載),用戶的信息,如id,用戶名等等
  • signature(簽名),將生成的token編碼(加密)
    他們之間用 "."號(hào)隔開(kāi),例如:xxxxx.yyyyy.zzzzz


    token

整合rest服務(wù)

我們知道,在restful服務(wù)中,服務(wù)端不再直接生成頁(yè)面了,而是只返回?cái)?shù)據(jù)(json),客戶端渲染,而我們前面的例子已經(jīng)知道了springsecurity默認(rèn)也是直接生成整個(gè)頁(yè)面的,所以這里的關(guān)鍵
就成了我們需要自己定義返回?cái)?shù)據(jù)形式了,主要是錯(cuò)誤處理,那我們接下來(lái)就開(kāi)始實(shí)踐。

實(shí)踐

pom

新建一個(gè)springboot項(xiàng)目,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-security-demos</artifactId>
        <groupId>cn.jsbintask</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>security-jwt-restful</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

    </dependencies>
</project>

數(shù)據(jù)源

定義數(shù)據(jù)源,application.yml文件以及sql腳本如下:

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: jason
    url:  jdbc:mysql://localhost:3306/springsecurity_demos?useSSL=false

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    org.springframework.*: debug

sql腳本如下,本例我們了簡(jiǎn)單,密碼使用明文:

CREATE DATABASE springsecurity_demos;
USE springsecurity_demos;


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
                       `id` int(11) NOT NULL AUTO_INCREMENT,
                       `role_name` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
                       `description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
                       PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'user', 'this is a user role.');
INSERT INTO `role` VALUES (2, 'admin', 'this is a admin role.');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
                       `id` int(11) NOT NULL AUTO_INCREMENT,
                       `username` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
                       `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
                       `age` int(11) NULL DEFAULT NULL,
                       `address` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
                       `role_id` int(11) NOT NULL,
                       PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'jsbintask', '123456', 22, 'China, Wuhan', 1);

SET FOREIGN_KEY_CHECKS = 1;

接著定義實(shí)體類domain,repository等,參考之前的博客,值得注意的是,此處我們的AuthUser稍有不同:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AuthUser implements UserDetails {
    private String username;
    private String password;
    private List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.isEmpty() ? Collections.EMPTY_LIST :
                // ROLE_ 是springsecurity對(duì)于角色的默認(rèn)前綴,如果不加,驗(yàn)證會(huì)失敗
                (roles.parallelStream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName())).collect(Collectors.toList()));
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

配置securityConfig

我們和前面章節(jié)一樣,定義一個(gè)SecurityConfig類,繼承WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

接下來(lái)就是開(kāi)始詳細(xì)配置了,前面我們學(xué)習(xí)基礎(chǔ)配置的時(shí)候已經(jīng)知道,配置的重點(diǎn)是自定義
UserDetailsService,并且我們通過(guò)源碼的方式知道了是UsernamePasswordAuthenticationFilter幫我們認(rèn)證了用戶,并且生成了頁(yè)面,轉(zhuǎn)發(fā)等等。
但是!在本例中,因?yàn)槲覀兪钦蟫estful服務(wù),返回的都是json數(shù)據(jù),所以我們不再需要這些。而既然要返回json數(shù)據(jù),那我們先定義好一個(gè)通用的數(shù)據(jù)類,ResultVO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVO<T> {
    // code請(qǐng)自己定義,例如  10成功,20需要登陸  30無(wú)權(quán)限等等
    private Integer code;
    private String msg;
    private T data;
}

而既然我們的服務(wù)端是生成jwt,那現(xiàn)在登錄的接口就成了生成jwt了,我們首先編寫JwtUtil工具類:

public class JwtUtil {
    private static final String secret = "jsbintask@gmail.com";

    public static String generateToken(String username, List<Role> roles) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", roles.parallelStream().map(Role::getRoleName).collect(Collectors.joining(",")));

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                //創(chuàng)建時(shí)間
                .setIssuedAt(new Date())
                //過(guò)期時(shí)間,我們?cè)O(shè)置為 五分鐘
                .setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
                //簽名,通過(guò)密鑰保證安全性
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public static AuthUser parseToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        String username = claims.getSubject();
        String roles = (String) claims.get("roles");

        //因?yàn)樯傻臅r(shí)候沒(méi)有放入密碼,所以不需要密碼
        return new AuthUser(username, null, Arrays.stream(roles.split(",")).map(name -> {
            Role role = new Role();
            role.setRoleName(name);
            return role;
        }).collect(Collectors.toList()));
    }
}

編寫生成token的controller,UserController:

@RestController
public class UserController {
    @Resource
    private UserRepository userRepository;
    @Resource
    private RoleRepository roleRepository;

    @GetMapping("/token")
    public ResultVO login(String username, String password) {
        User user = userRepository.findByUsername(username);

        if (user == null || !user.getPassword().equals(password)) {
            ResultVO<Object> result = new ResultVO<>();
            result.setCode(10);
            result.setMsg("用戶名或密碼錯(cuò)誤");
            return result;
        }

        ResultVO<Object> success = new ResultVO<>();
        //用戶名密碼正確,生成token給客戶端
        success.setCode(0);
        List<Role> roles = Collections.singletonList(roleRepository.findById(user.getId()).get());
        success.setData(JwtUtil.generateToken(username, roles));

        return success;
    }
}

我們?cè)诙x一個(gè)如果異常處理,用于處理請(qǐng)求沒(méi)有攜帶token以及一個(gè)禁止訪問(wèn)處理器,用于返回沒(méi)有權(quán)限的用戶并且全部作為bean
TokenExceptionHandler:

@Component
public class TokenExceptionHandler implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 直接返回 json錯(cuò)誤
        ResultVO<Object> result = new ResultVO<>();
        //20,標(biāo)識(shí)沒(méi)有token
        result.setCode(20);
        result.setMsg("請(qǐng)求無(wú)效,沒(méi)有有效token");

        ObjectMapper objectMapper = new ObjectMapper();
        
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

AccessDeniedHandler:

@Component
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 返回我們的自定義json
        ObjectMapper objectMapper = new ObjectMapper();
        ResultVO<Object> result = new ResultVO<>();
        //50,標(biāo)識(shí)有token,但是該用戶沒(méi)有權(quán)限
        result.setCode(50);
        result.setMsg("請(qǐng)求無(wú)效,沒(méi)有有效token");
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

除此之外,前面我們已經(jīng)通過(guò)debug(第四章)得知,在UsernamePasswordAuthenticationFilter中,springsecurity加入了SecurityContext,既然我們現(xiàn)在不用了,那我們要自己定義攔截器并且加入
securityContext以便springsecurity作權(quán)限處理,所以我們自定義攔截器:

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");

        //獲取token,并且解析token,如果解析成功,則放入 SecurityContext
        if (token != null) {
            try {
                AuthUser authUser = JwtUtil.parseToken(token);
                //todo: 如果此處不放心解析出來(lái)的 authuser,可以再?gòu)臄?shù)據(jù)庫(kù)查一次,驗(yàn)證用戶身份:

                //解析成功
                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    //我們依然使用原來(lái)filter中的token對(duì)象
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());

                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            } catch (Exception e) {
                logger.info("解析失敗,可能是偽造的或者該token已經(jīng)失效了(我們?cè)O(shè)置失效5分鐘)。");
            }
        }

        filterChain.doFilter(request, response);
    }
}

前面已經(jīng)說(shuō)了,我們不再需要UserDetailsService和UsernamePasswordAuthenticationFilter,當(dāng)然密碼加密器也不再需要,那現(xiàn)在我們的websecurity如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private TokenExceptionHandler tokenExceptionHandler;
    @Resource
    private AccessDeniedHandler accessDeniedHandler;
    @Resource
    private JwtTokenFilter jwtTokenFilter;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 因?yàn)槲覀兊膖oken是無(wú)狀態(tài)的,不需要跨站保護(hù)
                .csrf().disable()
                // 添加異常處理,以及訪問(wèn)禁止(無(wú)權(quán)限)處理
                .exceptionHandling().authenticationEntryPoint(tokenExceptionHandler).accessDeniedHandler(accessDeniedHandler).and()

                // 我們不再需要session了
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

                //定義攔截頁(yè)面,所有api全部需要認(rèn)證
                .authorizeRequests()

                .anyRequest().authenticated();

        //最后,我們定義 filter,用來(lái)替換原來(lái)的UsernamePasswordAuthenticationFilter
        httpSecurity.addFilterAt(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                // 讓我們獲取 token的api不走springsecurity的過(guò)濾器,大道開(kāi)放
                .antMatchers(HttpMethod.GET, "/token");
    }
}

值得注意的是,我們把 /token(登錄)這個(gè)api完全開(kāi)放了,因?yàn)樗静恍枰哌^(guò)濾器鏈,接下來(lái)我們?cè)賹懸粋€(gè)Controller用于測(cè)試,叫PermissionController:

@RestController
@RequestMapping
public class PermissionController {

    @GetMapping("/permission")
    public ResultVO loginTest(@AuthenticationPrincipal AuthUser authUser) {
        ResultVO<String> resultVO = new ResultVO<>();
        resultVO.setCode(0);

        resultVO.setData("你成功訪問(wèn)了該api,這代表你已經(jīng)登錄,你是: " + authUser);
        return resultVO;
    }

    @GetMapping("/permission")
    @PreAuthorize("hasRole('user')")
    public ResultVO loginTest() {
        ResultVO<String> resultVO = new ResultVO<>();
        resultVO.setCode(0);

        resultVO.setData("你成功訪問(wèn)了需要有 user 角色的api。");
        return resultVO;
    }
}

該controller一共兩個(gè)api,一個(gè)為登錄可訪問(wèn),一個(gè)為登陸后還需要有 user角色的api。

測(cè)試

接下來(lái)就是見(jiàn)證奇跡的時(shí)候了,首先我們直接訪問(wèn) /8080,我們猜想應(yīng)該是錯(cuò)誤code=20,因?yàn)樗衋pi都需要token,結(jié)果如下:

/8080

果然和我們想的一樣(亂碼原因是我們手動(dòng)new的jackson對(duì)象,沒(méi)有設(shè)置編碼),同時(shí)我們debug JwtTokenFilter,果然在過(guò)濾器鏈中找到了我們的filter,并且替換了原來(lái)的UsernamePasswordAuthenticationFilter
不見(jiàn)了:
/filter

接下來(lái)我們登錄,訪問(wèn) http://localhost:8080/token?username=jsbintask&password=12345,用戶名
密碼根據(jù)自己數(shù)據(jù)庫(kù)調(diào)整,結(jié)果如下:用戶名密碼錯(cuò)誤:
/token?

然后我們輸入正確的用戶名密碼,如下:
/token?username=jsbintask&password=123456

果然,我們得到了code=0,代表成功了,并且拿到了token,接下來(lái)我們用該token去訪問(wèn)我們的PermissionController:
訪問(wèn)不需要身份的 /normal:
/normal

成功!并且看到了我們的信息。
接下來(lái)訪問(wèn) /role,需要user身份,我們創(chuàng)建一個(gè)沒(méi)有該身份的用戶:

INSERT INTO `springsecurity_demos`.`user`(`id`, `address`, `age`, `password`, `role_id`, `username`) VALUES (2, 'Wuhan, China', 22, '123456', 22, 'jsbintask2');

同樣登陸后,訪問(wèn):


/denied

果然,我們得到了我們自定義的json數(shù)據(jù) coe=50,接下來(lái)我們切換有該身份的用戶:


/role

果然成功了!,這樣,我們的目的就全部達(dá)到了。 restful服務(wù)也完全整合好了

總結(jié)

本章,我們首先介紹了jwt是什么,以及和傳統(tǒng)session的不同,然后我們通過(guò)一個(gè)完整的例子展示了前面章節(jié)所學(xué)以及在實(shí)際中該如何保護(hù)我們的 api。并且成功的達(dá)到了我們的目的。
本章源碼:https://github.com/jsbintask22/spring-security-demos.git
到此,我們的spingsecurity系列的文章就寫完了,同系列文章地址:https://jsbintask.cn/tags/springsecurity/
本文原創(chuàng)地址:https://jsbintask.cn/2019/01/21/springsecurity-restful/#more,未經(jīng)允許,禁止轉(zhuǎn)載。

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

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