spring oauth2實(shí)現(xiàn)單點(diǎn)登錄败砂,Vue+spring boot+oauth2前后端分離

相關(guān)文章

1们妥、spring boot oauth2單點(diǎn)登錄(一)-實(shí)現(xiàn)例子
2、spring boot oauth2單點(diǎn)登錄(二)-客戶端信息存儲(chǔ)
3郎笆、spring boot oauth2單點(diǎn)登錄(三)-token存儲(chǔ)方式

源碼地址

后端:https://gitee.com/fengchangxin/sso
前端:https://gitee.com/fengchangxin/sso-page
前后端分離單點(diǎn)登錄薄声,后端返回json數(shù)據(jù),不涉及頁(yè)面渲染题画。最近在學(xué)習(xí)如何用spring oauth2來做單點(diǎn)登錄時(shí),發(fā)現(xiàn)網(wǎng)上的例子基本上都是不分離的德频,或者只講原理而沒有代碼苍息。通過對(duì)spring oauth2的debug跟蹤,大概了解它的執(zhí)行流程壹置,然后才做出這個(gè)例子竞思,但由于前端了解不多,以及對(duì)spring oauth2源碼了解不夠深钞护,與標(biāo)準(zhǔn)的oauth2流程有些差異盖喷,如果大家有更好的想法可以留言,但不一定回难咕。下面進(jìn)入正題:

一课梳、環(huán)境準(zhǔn)備

此篇文章涉及的項(xiàng)目基于windows系統(tǒng)
后端:jdk1.8距辆、三個(gè)spring boot服務(wù)(授權(quán)中心服務(wù):auth、客戶端服務(wù)1:client1暮刃、客戶端服務(wù)2:client2)
前端:node.js跨算、vue.js,三個(gè)Vue項(xiàng)目(授權(quán)中心前端:auth椭懊、客戶端1前端:client1诸蚕、客戶端2前端:client2)
三個(gè)域名:oauth.com(授權(quán)中心)、client1.com(客戶端1)氧猬、client2.com(客戶端2)
準(zhǔn)備好nginx

后端項(xiàng)目模塊.png

前端項(xiàng)目模塊.png

二背犯、后端項(xiàng)目

1、授權(quán)中心服務(wù):auth

1.1 自定義未登錄盅抚、登錄成功漠魏、登錄失敗的返回處理

未登錄處理
在這里做了兩個(gè)邏輯處理,根據(jù)參數(shù)isRedirect是否是true泉哈,如果是true則重定向到授權(quán)中心auth的前端登錄頁(yè)蛉幸,若為空或false,則返回授權(quán)中心的后端授權(quán)接口丛晦,并帶上isRedirect=true奕纫,定義Result對(duì)象的code為800則為未登錄。

@Component("unauthorizedEntryPoint")
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        Map<String, String[]> paramMap = request.getParameterMap();
        StringBuilder param = new StringBuilder();
        paramMap.forEach((k, v) -> {
            param.append("&").append(k).append("=").append(v[0]);
        });
        param.deleteCharAt(0);
        String isRedirectValue = request.getParameter("isRedirect");
        if (!StringUtils.isEmpty(isRedirectValue) && Boolean.valueOf(isRedirectValue)) {
            response.sendRedirect("http://oauth.com/authPage/login?"+param.toString());
            return;
        }
        String authUrl = "http://oauth.com/auth/oauth/authorize?"+param.toString()+"&isRedirect=true";
        Result result = new Result();
        result.setCode(800);
        result.setData(authUrl);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        PrintWriter writer = response.getWriter();
        ObjectMapper mapper = new ObjectMapper();
        writer.print(mapper.writeValueAsString(result));
        writer.flush();
        writer.close();
    }
}

登錄成功處理
這比較簡(jiǎn)單烫沙,就返回一個(gè)json對(duì)象匹层,Result對(duì)象的code為0則是成功,其他失敗锌蓄。

@Component("successAuthentication")
public class SuccessAuthentication extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        PrintWriter writer = response.getWriter();
        Result result = new Result();
        result.setCode(0);
        result.setMsg("成功");
        ObjectMapper mapper = new ObjectMapper();
        writer.println(mapper.writeValueAsString(result));
        writer.flush();
        writer.close();
    }
}

登錄失敗處理
和登錄成功差不多的處理

@Component("failureAuthentication")
public class FailureAuthentication extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        PrintWriter writer = response.getWriter();
        Result result = new Result();
        result.setCode(1000);
        result.setMsg("登錄失敗");
        ObjectMapper mapper = new ObjectMapper();
        writer.println(mapper.writeValueAsString(result));
        writer.flush();
        writer.close();
    }
}
1.2 資源配置和security配置

資源配置
定義了兩個(gè)客戶端升筏,可以通過數(shù)據(jù)庫(kù)方式來加載,至于如何實(shí)現(xiàn)網(wǎng)上有教程瘸爽,我這里圖方便用硬編碼兩個(gè)客戶端信息您访,這里有個(gè)問題需要注意,就是客戶端的回調(diào)地址只能寫/login剪决,這是因?yàn)锧EnableOAuth2Sso的客戶端默認(rèn)傳的授權(quán)回調(diào)地址就是login灵汪,這應(yīng)該可以修改旁振,但我不知道如何操作算柳。

@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(inMemoryClientDetailsService());
    }


    @Bean
    public ClientDetailsService inMemoryClientDetailsService() throws Exception {
        return new InMemoryClientDetailsServiceBuilder()
                // client oa application
                .withClient("client1")
                .secret(passwordEncoder.encode("client1_secret"))
                .scopes("all")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://client1.com/client1/login")
                .accessTokenValiditySeconds(7200)
                .autoApprove(true)

                .and()

                // client crm application
                .withClient("client2")
                .secret(passwordEncoder.encode("client2_secret"))
                .scopes("all")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://client2.com/client2/login")
                .accessTokenValiditySeconds(7200)
                .autoApprove(true)

                .and()
                .build();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter())
                .tokenStore(jwtTokenStore());
    }

    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("123456");
        return jwtAccessTokenConverter;
    }

}

security 配置
這里把上面自定義的未登錄呼奢、登錄成功和失敗的處理加載進(jìn)來研铆,同時(shí)設(shè)了兩個(gè)用戶賬號(hào)admin和user1秋泄,密碼都是123456躬充,用于頁(yè)面登錄醋寝。

@EnableWebSecurity
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private SuccessAuthentication successAuthentication;
    @Autowired
    private FailureAuthentication failureAuthentication;
    @Autowired
    private UnauthorizedEntryPoint unauthorizedEntryPoint;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.formLogin()
//                .loginPage("/login")
//                .and()
//                .authorizeRequests()
//                .antMatchers("/login").permitAll()
//                .anyRequest()
//                .authenticated()
//                .and().csrf().disable().cors();

        http.cors().and().csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().successHandler(successAuthentication).failureHandler(failureAuthentication);
    }

    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() {
        Collection<UserDetails> users = buildUsers();

        return new InMemoryUserDetailsManager(users);
    }

    private Collection<UserDetails> buildUsers() {
        String password = passwordEncoder().encode("123456");

        List<UserDetails> users = new ArrayList<>();

        UserDetails user_admin = User.withUsername("admin").password(password).authorities("ADMIN", "USER").build();
        UserDetails user_user1 = User.withUsername("user1").password(password).authorities("USER").build();

        users.add(user_admin);
        users.add(user_user1);

        return users;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}
1.3 設(shè)置允許跨域

當(dāng)前端調(diào)用客戶端接口時(shí)丽惶,如果未登錄客戶端就會(huì)重定向到授權(quán)中心服務(wù)auth請(qǐng)求授權(quán)譬胎,這就涉及到跨域了差牛,如果不加這個(gè)配置命锄,sso流程無法走通。在這里設(shè)置了所有域都可以訪問多糠,這是不安全的累舷,可以結(jié)合動(dòng)態(tài)配置中心或者數(shù)據(jù)庫(kù)來動(dòng)態(tài)加載允許訪問的域名。

@Order(Ordered.HIGHEST_PRECEDENCE)
@Configuration
public class CORSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        //允許所有的域訪問夹孔,可以設(shè)置只允許自己的域訪問
        response.setHeader("Access-Control-Allow-Origin", "*");
        //允許所有方式的請(qǐng)求
        response.setHeader("Access-Control-Allow-Methods", "*");
        //頭信息緩存有效時(shí)長(zhǎng)(如果不設(shè) Chromium 同時(shí)規(guī)定了一個(gè)默認(rèn)值 5 秒)被盈,沒有緩存將已OPTIONS進(jìn)行預(yù)請(qǐng)求
        response.setHeader("Access-Control-Max-Age", "3600");
        //允許的頭信息
        response.setHeader("Access-Control-Allow-Headers", "Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE,x-requested-with,Authorization");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }
}
1.4 yml配置
server:
  port: 8080
  servlet:
    context-path: /auth
    session:
      cookie:
        name: SSO-SESSION

2、客戶端服務(wù)

因?yàn)閮蓚€(gè)客戶端的是幾乎相同的搭伤,所以這里只展示client1的只怎,詳細(xì)代碼可以到文章開頭那里下載。

2.1 security配置

使用@EnableOAuth2Sso注解怜俐,使用單點(diǎn)登錄身堡,所有的接口都需要登錄之后才可訪問。

@EnableOAuth2Sso
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout()
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }
}
2.2 yml配置

這里配置了oauth2流程的必要配置拍鲤。

server:
  port: 8081
  servlet:
    context-path: /client1

security:
  oauth2:
    client:
      client-id: client1
      client-secret: client1_secret
      access-token-uri: http://oauth.com/auth/oauth/token
      user-authorization-uri: http://oauth.com/auth/oauth/authorize
    resource:
      jwt:
        key-uri: http://oauth.com/auth/oauth/token_key
2.3 定義兩個(gè)接口

定義了一個(gè)測(cè)試接口/test贴谎,至于第二個(gè)接口是回調(diào)接口,當(dāng)客戶端授權(quán)成功后最后一步調(diào)用季稳,這里重定向返回到對(duì)應(yīng)客戶端的前端地址擅这。

@RestController
public class Controller {

    @GetMapping("/test")
    public Result test() {
        System.out.println("11111");
        Result result = new Result();
        result.setCode(0);
        result.setData("hello client1");
        return result;
    }

    @GetMapping("/")
    public void callback(HttpServletResponse response) throws IOException {
        response.sendRedirect("http://client1.com/client1Page/home");
    }
}

三、前端項(xiàng)目

1景鼠、授權(quán)中心前端:auth

授權(quán)中心的前端頁(yè)面寫了一個(gè)簡(jiǎn)單的登錄頁(yè)仲翎,當(dāng)點(diǎn)擊登錄按鈕時(shí)調(diào)用login()方法,方法調(diào)用授權(quán)中心后端接口铛漓,如果返回的json的code為0溯香,則登錄成功,然后跳轉(zhuǎn)到授權(quán)中心后端授權(quán)接口浓恶,這里要用window.location.href跳轉(zhuǎn)玫坛,而不能用js調(diào)用,否則無法跳轉(zhuǎn)到客戶端包晰。

<template>
  <div>
    <p>賬號(hào):</p>
    <input type="text" v-model="loginForm.username">
    <p>密碼:</p>
    <input type="password" v-model="loginForm.password">
    <p></p>
    <button v-on:click="login">登錄</button>
  </div>
</template>

<script>
  import {postRequest} from "../utils/api";

  export default {
    name: 'Login',
    data() {
      return {
        loginForm: {
          username: '',
          password: ''
        }
      }
    },
    methods: {
      login() {
        postRequest('/auth/login', this.loginForm).then(resp => {
          if (resp.data.code === 0) {
            var pageUrl = window.location.href
            var param = pageUrl.split('?')[1]
            window.location.href = '/auth/oauth/authorize?'+param
          } else {
            console.log('登錄失敯和骸:'+resp.data.msg)
          }
        })
      }
    }
  }
</script>
<style scoped>
</style>

2、客戶端client1前端

客戶端client2的代碼基本一樣杜窄,在test()方法中調(diào)用客戶端后端接口,如果返回的code為0則顯示數(shù)據(jù)算途,如果返回800塞耕,是未登錄然后跳轉(zhuǎn)到授權(quán)中心的授權(quán)接口,這里的800返回是在授權(quán)中心后端的自定義未登錄 處理UnauthorizedEntryPoint返回的嘴瓤,與標(biāo)準(zhǔn)oauth2流程相比扫外,這里多了一次跳轉(zhuǎn)到授權(quán)接口莉钙,在UnauthorizedEntryPoint然后重定向到授權(quán)中心的登錄頁(yè)。

<template>
  <div>
    <button v-on:click="test">顯示</button>
    <p>client1顯示結(jié)果:{{msg}}</p>
  </div>
</template>

<script>
  import {getRequest} from "../utils/api";

  export default {
  name: 'Home',
  data () {
    return {
      msg: ''
    }
  },
  methods: {
    test() {
      getRequest('/client1/test').then(resp=>{
        if (resp.data.code === 0) {
          this.msg = resp.data.data
        }else if (resp.data.code === 800) {
          window.location.href = resp.data.data
        } else {
          console.log('失斏秆琛:'+resp.data)
        }
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

代碼已經(jīng)準(zhǔn)備好磁玉,一些細(xì)節(jié)的代碼需要從碼云下載了解,在文章就不展示了驾讲,接下來就是測(cè)試了蚊伞。

四、測(cè)試

1吮铭、環(huán)境配置準(zhǔn)備

1.1 配置hosts

在hosts中添加下面三個(gè)域名配置时迫,如果都用localhost來測(cè)試的話,測(cè)試無法知道單點(diǎn)登錄流程是否正常谓晌,因?yàn)槿齻€(gè)項(xiàng)目的域名相同的話cookie可能會(huì)造成干擾掠拳。

127.0.0.1 oauth.com
127.0.0.1 client1.com
127.0.0.1 client2.com
1.2 nginx配置
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    
    server {
        listen          80;
        server_name     oauth.com;

        location /auth/ {
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://localhost:8080/auth/;
        }
        
        location ^~ /authPage {
            try_files $uri $uri/ /authPage/index.html;
        }
    }
    
    server {
        listen          80;
        server_name     client1.com;
    
        location /client1/ {
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://localhost:8081/client1/;
        }
        
        location ^~ /client1Page {
            try_files $uri $uri/ /client1Page/index.html;
        }
    }

    server {
        listen       80;
        server_name  client2.com;
        
        location /client2/ {
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://localhost:8082/client2/;
        }
    
        location ^~ /client2Page {
            try_files $uri $uri/ /client2Page/index.html;
        }
        
    
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

}

注意:配置后端接口時(shí)要加上下面兩句,不然后端重定向時(shí)域名會(huì)變成localhost纸肉,導(dǎo)致流程失敗溺欧。

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1.3 前端打包部署

如何在nginx下打包部署Vue項(xiàng)目,可以看我的這篇文章柏肪。

1.4 啟動(dòng)后端服務(wù)

依次啟動(dòng)nginx姐刁、auth、client1预吆、client2后端服務(wù)龙填。

2、測(cè)試

在瀏覽器輸入http://client1.com/client1Page/home拐叉,訪問客戶端1的前端地址岩遗,點(diǎn)擊顯示按鈕會(huì)跳轉(zhuǎn)到授權(quán)中心的登錄頁(yè),輸入賬號(hào)admin凤瘦,密碼123456宿礁,登錄成功后會(huì)重定向到客戶端1的頁(yè)面,此頁(yè)面地址就是client1后端的的callback接口里設(shè)置的重定向地址蔬芥,然后再點(diǎn)擊按鈕下面會(huì)顯示client1后端接口返回的數(shù)據(jù)梆靖。
然后瀏覽器再開一個(gè)標(biāo)簽頁(yè),輸入http://client2.com/client2Page/home笔诵,訪問客戶端2的前端地址返吻,點(diǎn)擊顯示按鈕然后請(qǐng)求授權(quán)中心授權(quán),然后不需要登錄就授權(quán)成功并重定向到客戶端2的頁(yè)面乎婿,此頁(yè)面地址就是client2后端的callback接口里設(shè)置的重定向地址测僵,這里設(shè)置了相同的頁(yè)面,所以不要錯(cuò)誤認(rèn)為沒有登錄成功,然后點(diǎn)擊顯示按鈕下面會(huì)顯示client2后端返回的數(shù)據(jù)捍靠。

第一步.png

第二步.png
第三步.png
第四步.png
第五步.png

五沐旨、流程解析

1、UML圖

1.1 client1流程

此流程與標(biāo)準(zhǔn)的oauth2流程相比榨婆,多了兩次授權(quán)請(qǐng)求磁携,按照正常oauth2流程,在第一次請(qǐng)求授權(quán)時(shí)如果未登錄就重定向到登錄頁(yè)良风,但用前后端分離后谊迄,返回了授權(quán)接口在前端跳轉(zhuǎn),此時(shí)多了一次授權(quán)請(qǐng)求拖吼,在登錄成功后又再次請(qǐng)求授權(quán)接口鳞上,這樣做的原因是登錄成功后,client2再請(qǐng)求時(shí)無法獲取到登錄成功后的SSO-SESSION這個(gè)cookie吊档,從而導(dǎo)致需要再登錄篙议,我認(rèn)為拿不到cookie的原因是在不同域名下請(qǐng)求另一個(gè)域名的接口是無法取到cookie的,所以只能在瀏覽器上跳轉(zhuǎn)怠硼,授權(quán)中心根據(jù)isRedirect這個(gè)參數(shù)來判斷是重定向到登錄頁(yè)還是返回json未登錄鬼贱。


image.png
1.2 client2流程
image.png

2、源碼跟蹤

2.1 有時(shí)間再寫吧
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末香璃,一起剝皮案震驚了整個(gè)濱河市这难,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌葡秒,老刑警劉巖姻乓,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異眯牧,居然都是意外死亡蹋岩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門学少,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剪个,“玉大人,你說我怎么就攤上這事版确】勰遥” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵绒疗,是天一觀的道長(zhǎng)侵歇。 經(jīng)常有香客問我,道長(zhǎng)吓蘑,這世上最難降的妖魔是什么盒至? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上枷遂,老公的妹妹穿的比我還像新娘。我一直安慰自己棋嘲,他們只是感情好酒唉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沸移,像睡著了一般痪伦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上雹锣,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天网沾,我揣著相機(jī)與錄音,去河邊找鬼蕊爵。 笑死辉哥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的攒射。 我是一名探鬼主播醋旦,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼会放!你這毒婦竟也來了饲齐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤咧最,失蹤者是張志新(化名)和其女友劉穎捂人,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矢沿,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滥搭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咨察。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片论熙。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖摄狱,靈堂內(nèi)的尸體忽然破棺而出脓诡,到底是詐尸還是另有隱情,我是刑警寧澤媒役,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布祝谚,位于F島的核電站,受9級(jí)特大地震影響酣衷,放射性物質(zhì)發(fā)生泄漏交惯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望席爽。 院中可真熱鬧意荤,春花似錦、人聲如沸只锻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)齐饮。三九已至捐寥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間祖驱,已是汗流浹背握恳。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捺僻,地道東北人乡洼。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像陵像,于是被迫代替她去往敵國(guó)和親就珠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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