Vue前后端整合

vue-monitor作為前端應(yīng)用庶骄,vue-admin作為后臺提供標(biāo)準(zhǔn)接口毁渗,這是標(biāo)準(zhǔn)的前后端分離解決方案。本文主要講解兩個應(yīng)用整合的過程单刁。按理來說灸异,也不存在什么整合的過程,這本就是兩個獨立的應(yīng)用羔飞,只需要將兩個應(yīng)用分別啟動就可以正常訪問了肺樟,事實也確實如此,但在此之前需要先解決一個問題:跨域問題逻淌。

啟動vue-monitor

進入vue-monitor跟目錄么伯,執(zhí)行以下命令:

npm run dev

這樣前端應(yīng)用就啟動了,這時候前端時完全依賴于node的卡儒。如果想不依賴于node也可以田柔,可以執(zhí)行

 npm run build 

這樣就可以將前端應(yīng)用打包成靜態(tài)資源俐巴,然后將這些靜態(tài)資源部署到nginx服務(wù)器,同樣可以正常訪問硬爆,這里用的時第一種方法欣舵。

啟動vue-admin

其實就是啟動一個spring-boot應(yīng)用,啟動方法很多種缀磕,這里是在IDEA中直接 運行的

至此缘圈,兩個應(yīng)用已經(jīng)完全啟動了。

前端調(diào)用后端接口

在我們的前端應(yīng)用中袜蚕,有一個登錄界面糟把,效果如下

注意,隨著應(yīng)用的開發(fā)廷没,可能之后這些代碼會被刪除或者覆蓋糊饱,這里只是 為了說明前端調(diào)用后端應(yīng)用的一個示例。

當(dāng)點擊的登錄按鈕的時候颠黎,調(diào)用后臺的登錄接口另锋,后臺對應(yīng) 的接口信息如下

我們期望的結(jié)果是:點擊登錄的時候,可以訪問到后臺這個接口狭归。但是當(dāng)我們點擊登錄按鈕的時候夭坪,發(fā)現(xiàn)界面沒有任何響應(yīng),后臺也沒有任何日志輸出过椎,這已經(jīng)可以說明接口沒有調(diào)用成功了室梅。打開瀏覽器調(diào)試工具,發(fā)現(xiàn)有如下錯誤:

具體內(nèi)容如下:

Failed to load http://localhost:8081/api/system/login: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8082' is therefore not allowed access. The response had HTTP status code 403. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

這是一個跨域問題疚宇,我們前端應(yīng)用的端口是 8002亡鼠,后臺應(yīng)用的端口是8001,存在跨域問題敷待。怎么解決间涵?有很多方法:

  • nginx方向代理
  • CORS

這里用了CORS 解決跨域問題。第二種方法沒有用過榜揖,之后補上吧勾哩。

CORS解決跨域

使用cors解決跨域問題,需要在后臺將 前端域名 設(shè)置成可以訪問举哟。
先在后臺添加一個 過濾器:

package com.hand.sxy.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 不加 @Configuration 注解不生效
 *
 * @author spilledyear
 * @date 2018/4/21 18:42
 */
@Configuration
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
    private Logger logger = LoggerFactory.getLogger(CorsFilter.class);

    @Override
    public void init(FilterConfig arg0) throws ServletException {
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        logger.debug("跨域攔截");

        HttpServletResponse response = (HttpServletResponse) res;

        // 指定允許其他域名訪問
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 響應(yīng)類型
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE");
        // 響應(yīng)頭設(shè)置
        response.setHeader("Access-Control-Allow-Headers", "token,Content-Type,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Max-Age,authorization");
        response.setHeader("Access-Control-Max-Age", "3600");

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}

這里有一個需要注意的地方思劳,需要添加 @Configuration注解,要不然這個filter 是不生效了妨猩。添加過濾器后潜叛,重新啟動后臺應(yīng)用,再次點擊登錄按鈕册赛,發(fā)現(xiàn)還是報錯了钠导,報錯信息如下:

Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. Origin 'http://localhost:8082' is therefore not allowed access.

大概意思是說震嫉, 后臺中的響應(yīng)頭不能設(shè)置 Access-Control-Allow-Origin 的值為 通配符 *森瘪。這時候?qū)笈_應(yīng)用該稍作修改

// 指定允許其他域名訪問牡属,因為前端應(yīng)用域名是 http://localhost:8082
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082");

再次重新啟動該后臺應(yīng)用,在前端再次訪問扼睬,發(fā)現(xiàn)還是失敗了逮栅,報錯信息如下:

Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. Origin 'http://localhost:8082' is therefore not allowed access

提示當(dāng) request's credentials mode 的值是  'include' 的時候, Access-Control-Allow-Credentials 在響應(yīng)頭中必輸設(shè)置成 true窗宇。

這讓我想到一個問題措伐,那就是在前端應(yīng)用中 request 請求頭設(shè)置問題,內(nèi)容如下:

let request = {
  credentials: 'include',
  method: type,
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  mode: "cors",
  cache: "force-cache"
}

果然設(shè)置了 credentials: 'include'军俊。
可是這個參數(shù)我并不太清楚是干嘛的侥加,解決方法有兩個:
1、在后臺添加一行代碼粪躬,設(shè)置 Access-Control-Allow-Credentials 的值為 true

response.setHeader("Access-Control-Allow-Credentials", "true");

2担败、在前端的請求頭中,去除 credentials: 'include' 這個設(shè)置

let request = {
  method: type,
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  mode: "cors",
  cache: "force-cache"
}

至此镰官,跨域問題得到解決提前。再次點擊登錄按鈕,查看后臺日志:

從上圖中的日志中可以看出泳唠,成功的調(diào)用了 登錄接口狈网。

接收參數(shù)

跨域問題是成功解決了,但是參數(shù)還沒有傳過來笨腥。為了將用戶名和密碼傳到后臺測試拓哺,對后臺代碼稍作修改。

//對查詢方法稍作修改
<select id="query" resultMap="BaseResultMap" parameterType="com.hand.sxy.account.dto.User">
    SELECT * FROM USER WHERE USERNAME = #{username} AND PASSWORD = #{password}
</select>

然后其它地方也稍作修改脖母,具體的請看源碼士鸥,controller 中的代碼如下

package com.hand.sxy.system.controller;

@RestController
public class LoginController {
    private Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private ILoginService loginService;

    @RequestMapping(value = "/api/system/login", method = RequestMethod.POST)
    public Result login(HttpServletRequest request, User user) {

        List<User> userList = loginService.login(user);
        Result result = new Result(userList);

        if (userList == null || userList.isEmpty()) {
            logger.info("登錄失敗,用戶名或密碼錯誤");
            result.setSuccess(false);
            result.setMessage("用戶名或密碼錯誤");
        }
        logger.info("登錄成功");
        return result;
    }

}

修改之后镶奉,重啟后臺系統(tǒng)础淤,在前端界面輸入用戶名和密碼,點擊登錄按鈕哨苛,用瀏覽器調(diào)試工具發(fā)現(xiàn)前端調(diào)用了請求鸽凶,參數(shù)也傳了,但是在后臺打斷點發(fā)現(xiàn)參數(shù)沒有穿過來建峭。
解決方法:方法加上 @RequestBody 注解

package com.hand.sxy.system.controller;

@RestController
public class LoginController {
    private Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private ILoginService loginService;

    @RequestMapping(value = "/api/system/login", method = RequestMethod.POST)
    public Result login(HttpServletRequest request, @RequestBody User user) {

        List<User> userList = loginService.login(user);
        Result result = new Result(userList);

        if (userList == null || userList.isEmpty()) {
            logger.info("登錄失敗玻侥,用戶名或密碼錯誤");
            result.setSuccess(false);
            result.setMessage("用戶名或密碼錯誤");
        }
        logger.info("登錄成功");
        return result;
    }

}

@RequestBody用于讀取Request請求的body部分?jǐn)?shù)據(jù),使用系統(tǒng)默認配置的HttpMessageConverter進行解析亿蒸,然后把相應(yīng)的數(shù)據(jù)綁定到要返回的對象上凑兰,然后再把HttpMessageConverter返回的對象數(shù)據(jù)綁定到 controller中方法的參數(shù)上掌桩。

@RequestBody注解是否必須要,根據(jù)request header Content-Type的值來判斷
GET姑食、POST方式提時

  • application/x-www-form-urlencoded波岛, 可選(即非必須,因為這種情況的數(shù)據(jù)@RequestParam, @ModelAttribute也可以處理音半,當(dāng)然@RequestBody也能處理)
  • multipart/form-data, 不能處理(即使用@RequestBody不能處理這種格式的數(shù)據(jù))
  • 其他格式则拷, 必須(其他格式包括application/json, application/xml等。這些格式的數(shù)據(jù)曹鸠,必須使用@RequestBody來處理)

PUT方式提交時

  • application/x-www-form-urlencoded煌茬, 必須
  • multipart/form-data, 不能處理
  • 其他格式, 必須

說明:request的body部分的數(shù)據(jù)編碼格式由header部分的Content-Type指定彻桃;

@ResponseBody用于將Controller的方法返回的對象坛善,通過適當(dāng)?shù)腍ttpMessageConverter轉(zhuǎn)換為指定格式后,寫入到Response對象的body數(shù)據(jù)區(qū)邻眷。 在Controller中返回的數(shù)據(jù)不是html標(biāo)簽的頁面眠屎,而是其他某種格式的數(shù)據(jù)時(如json、xml等)使用耗溜。

整合echarts

echarts是百度的组力,現(xiàn)在已經(jīng)到了版本4了,很強大的一個圖表框架抖拴。而且上手特別簡單燎字,打開官網(wǎng)看看就知道怎么用, 至于具體的細節(jié)阿宅,那就是靠查API了候衍,下面解釋怎么在vue應(yīng)用中使用 echarts。

安裝echarts

npm install echarts --save

這樣就已經(jīng)安裝好了洒放。

引用echarts

引用方式有兩種:一種是全局引用蛉鹿;一種根據(jù)需求引用你要用到的部分。這里使用的是全局引用往湿。
在 main.js 文件中妖异,添加以下內(nèi)容

import echarts from 'echarts'
Vue.prototype.$echarts = echarts 

然后使用的時候

<template>
  <div class="line1">
    <div id="line1" class="" style="width: 100%;height:680px;"></div>
  </div>
</template>

<script>
  export default {
    mounted() {
      this.myChart = this.$echarts.init(document.getElementById('line1'));
      this.initData();
    },
    methods: {
      initData() {
          const  option = xxx
          this.myChart.setOption(option);
    }
</script>

不要好奇那個 const option = xxx是什么意思,你可以把它當(dāng)作是配置领追,實際上它就是個配置他膳,你需要顯示什么圖形,需要什么數(shù)據(jù)绒窑,都是在這里面配置棕孙,它是一個json對象,也就說,echarts的對象就是一個echarts對象蟀俊。如果你實在不知道這個option是什么钦铺,有一個最簡單的辦法,你去官網(wǎng)上找一個例子肢预,然后它代碼拷貝出來就好了矛洞。至于具體顯示什么圖形,就得去查API了误甚。

JWT交互

vue-admin后臺已經(jīng)整合了JWT這一方面的內(nèi)容缚甩,那么前后端怎么通過JWT交互呢谱净?

安裝Js-Cookie

js-cookie是干嘛的窑邦,看一下這個

A simple, lightweight JavaScript API for handling cookies

If you're viewing this at https://github.com/js-cookie/js-cookie, you're reading the documentation for the master branch. View documentation for the latest release.

沒錯,就是幫助我們操作cookie的壕探,npm上的鏈接 js-cookie
基礎(chǔ)用法冈钦,簡單看看,不說了李请,不清楚的時候看看文檔把瞧筛。

image.png

安裝 js-cookie
//auth.js

npm install js-cookie --save

封裝一個工具類

import Cookies from 'js-cookie'

export const TOKEN_KEY = 'Authorization';

export function getToken() {
  return Cookies.get(TOKEN_KEY)
}

export function setToken(token) {
  return Cookies.set(TOKEN_KEY, token)
}

export function removeToken() {
  return Cookies.remove(TOKEN_KEY)
}

axios工具類

整體上比較簡單,就是在請求和響應(yīng)上設(shè)置一個攔截器导盅。在請求的適合较幌,判斷前端是否已經(jīng)獲取過token,如果有token白翻,就將它設(shè)置在請求頭中乍炉。子啊響應(yīng)的時,判斷一下響應(yīng)狀態(tài)滤馍,做一些處理岛琼。

import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { TOKEN_KEY, getToken } from '@/utils/auth'

/**
 * 創(chuàng)建一個 axios 實例
 */
const service = axios.create({
  baseURL: process.env.BASE_API,
  timeout: 50000
})

// 請求 攔截器,在請求之前執(zhí)行一些邏輯
service.interceptors.request.use(request => {
  if (store.getters.token) {
    // 讓每個請求攜帶{TOKEN_KEY: xxx} TOKEN_KEY為自定義key巢株,在 @/utils/auth 中定義槐瑞, 請根據(jù)實際情況自行修改
    request.headers[TOKEN_KEY] = getToken();
  }
  return request
}, error => {
  Promise.reject(error)
});

// 響應(yīng) 攔截器,返回結(jié)果之后處理一些親求
service.interceptors.response.use(
  response => {
    /**
     * 下面的注釋為通過response自定義code來標(biāo)示請求狀態(tài)阁苞,當(dāng)code返回如下情況為權(quán)限有問題困檩,登出并返回到登錄頁
     * 如通過xmlhttprequest 狀態(tài)碼標(biāo)識 邏輯可寫在下面error中 
     */
    const res = response.data;
    if (response.status !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000
      });
      // 50008:非法的token; 50012:其他客戶端登錄了; 50014:Token 過期了;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        MessageBox.confirm('你已被登出,可以取消繼續(xù)留在該頁面那槽,或者重新登錄', '確定登出', {
          confirmButtonText: '重新登錄',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('FedLogOut').then(() => {
            // 為了重新實例化vue-router對象 避免bug
            location.reload(); 
          });
        })
      }
      return Promise.reject('error');
    } else {
      return response.data;
    }
  },
  error => {
    console.log('error' + error);
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
});

export default service

測試

啟動后臺應(yīng)用: localhost:8081
啟動前端應(yīng)用: localhost:8082

打開前端的的登錄界面悼沿,輸入用戶名密碼,點擊登錄倦炒。通過chrom調(diào)試工具觀察network請求显沈;同時觀察后臺的自定義過濾器。

這個接口返回的信息, 可以看到拉讯,已經(jīng)返回的token涤浇。

同時,利用vue開發(fā)者調(diào)試工具觀察狀態(tài)情況


可以看到魔慷,已經(jīng)把值存進去了只锭。

登錄成功之后,跳到了首頁院尔。

這時候再請求一個用戶列表蜻展,觀察前后端情況

可以看到,后臺已經(jīng)拿到了 token


同時邀摆,前端也返回了我們想要的用戶信息


整合Quartz

大概流程如下:在 vue-admin 中提供一個可視化管理Job的界面纵顾,可以查看Job信息,添加Job栋盹、停止或啟動Job施逾。同時這里引入另外一個東西,查看Job的執(zhí)行記錄例获,在后臺引入一張 sys_job_record表汉额,用于記錄Job的執(zhí)行里歷史記錄。有關(guān)于執(zhí)行歷史記錄在 vue-monitor 這一塊的實現(xiàn)榨汤,大概上就是添加了一個 JobListern蠕搜,監(jiān)聽Job,在Job執(zhí)行的時候?qū)?執(zhí)行記錄插入到 sys_job_record表中收壕。然后對應(yīng)的妓灌,前端也提供一個界面用于展示Job執(zhí)行記錄。

i

關(guān)鍵代碼

JobRecordListener

package com.hand.sxy.job.listener;

import com.hand.sxy.job.dto.JobRecord;
import com.hand.sxy.job.service.IJobRecordService;
import org.quartz.*;
import org.quartz.listeners.JobListenerSupport;
import org.springframework.context.ApplicationContext;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;

public class JobRecordListener extends JobListenerSupport {

    private static final String VETOED = "Vetoed";

    private static final String FINISH = "Finish";

    private static final String FAILED = "Failed";


    private ApplicationContext applicationContext;

    public JobRecordListener(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }


    @Override
    public String getName() {
        return "JobRecordListener";
    }


    /**
     * @param context
     * @param jobException
     */
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        JobRecord dto = getRecord(context);
        if (jobException != null) {
            String errMsg = jobException.getMessage();
            dto.setJobStatusMessage(errMsg);
            dto.setJobStatus(FAILED);
        } else {
            dto.setJobStatus(FINISH);
        }
        this.insert(dto);
        Job job = context.getJobInstance();
        if (job instanceof JobListener) {
            context.put("JOB_RUNNING_INFO_ID", dto.getJobRecordId());
            ((JobListener) job).jobWasExecuted(context, jobException);
        }
    }
......
}

RcordSchedulerPlugin

package com.hand.sxy.job.plugin;

import com.hand.sxy.job.listener.JobRecordListener;
import com.hand.sxy.job.listener.SchedulerRecordListener;
import org.quartz.ListenerManager;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.matchers.EverythingMatcher;
import org.quartz.spi.ClassLoadHelper;
import org.quartz.spi.SchedulerPlugin;
import org.springframework.context.ApplicationContext;

/**
 * @author spilledyear
 */
public class RcordSchedulerPlugin implements SchedulerPlugin {

    private ApplicationContext applicationContext;

    private Scheduler scheduler;


    @Override
    public void initialize(String name, Scheduler scheduler, ClassLoadHelper loadHelper) throws SchedulerException {
        this.scheduler = scheduler;
    }


    @Override
    public void start() {
        try {
            applicationContext = (ApplicationContext) scheduler.getContext().get("applicationContext");
            ListenerManager listenerManager = scheduler.getListenerManager();
            listenerManager.addJobListener(new JobRecordListener(applicationContext), EverythingMatcher.allJobs());
            listenerManager.addSchedulerListener(new SchedulerRecordListener(applicationContext));

        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public void shutdown() {

    }

}

后臺每執(zhí)行一次job啼器,都會將 執(zhí)行記錄寫入到 sys_job_record表中旬渠,并在前端展示。

有關(guān)于后端JWT的處理

整合Spring Security

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

Springboot2的變化還是很多的端壳,其中有一個就是密碼加密方式問題告丢。
要想更靈活的使用spring-securiy,你必須理解它的工作流程损谦,我覺得理解它的過濾器鏈?zhǔn)亲钪匾尼猓还沧詭Я?1個過濾器鏈,但是關(guān)鍵的只有那么幾個照捡,如果你知道了 各個過濾器的作用颅湘,就可以根據(jù)自己的需求在合適的位置插入自己的過濾器。其實主要來看栗精,spring-security基礎(chǔ)就分兩個部分:一個是用戶認證闯参;一個是判斷請求的資源是否有權(quán)限訪問瞻鹏,也可以認為是權(quán)限和資源的關(guān)系,這兩部分可以分開來看鹿寨。在我們實際項目中新博,一般客戶還的是前半部分,也就是用戶的認證過程脚草。比如最常用的通過數(shù)據(jù)庫的方式實現(xiàn)認證:實現(xiàn) UserDetailsService 接口赫悄,在loadUserByUsername 方法中從數(shù)據(jù)庫加載用戶信息,然后封裝成UserDetails對象交接給spring-securitys馏慨」』矗可以發(fā)現(xiàn),我們要做的就是實現(xiàn)一個loadUserByUsername 方法写隶,簡單到不能再簡單倔撞。但如果你不理解spring-security的工作流程,你寫完這個代碼之后其實是沒底的樟澜。又比如:你想在修改認證攔截误窖,可以在UsernamePasswordAuthenticationFilter 過濾器前加自己的過濾器,你想自定義資源與權(quán)限的關(guān)系秩贰,可以繼承 FilterSecurityInterceptor 攔截器等。

添加一個 配置類

package com.hand.sxy.config;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.route.authentication.path}")
    private String authenticationPath;

    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;


    /**
     * 通過這種方式注入 authenticationManagerBean 柔吼,然后在別的地方也可以用
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 注冊 UserDetailsService 的 Bean
     *
     * @return
     */
    @Bean
    UserDetailsService customUserService() {
        return new CustomUserService();
    }

    /**
     * Sring5 中密碼加密新方式
     *
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
//        httpSecurity.rememberMe().rememberMeServices(rememberMeServices());
//        httpSecurity.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);

        httpSecurity
                .cors().and()
                .csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)

                /** 不創(chuàng)建 session **/
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                .antMatchers("/*.html", "/**/*.html", "/**/*.js", "/**/*.css").permitAll()
                .antMatchers("/login", "/register", "/auth", "/oauth/*").permitAll()
                .antMatchers("/api/role/query").hasRole("ADMIN")
                .anyRequest().authenticated()

                .and()
                .formLogin().loginPage("/login").loginProcessingUrl("/api/system/login").usernameParameter("username").passwordParameter("password").permitAll()

                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/api/system/logout").permitAll();


        /**
         * spring security過濾器鏈中毒费,真正的用戶信息校驗是 UsernamePasswordAuthenticationFilter 過濾器,然后才是權(quán)限校驗愈魏。
         * 這里在 UsernamePasswordAuthenticationFilter過濾器之前 自定義一個過濾器觅玻,這樣就可以提前根據(jù)token將authenticate信息
         * 維護進speing security上下文,然后在 UsernamePasswordAuthenticationFilter 得到的就已經(jīng)是通過校驗的用戶了培漏。
         */
        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(customUserService(), jwtTokenUtil, tokenHeader);
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        /**
         * disable page caching
         *
         * 下面這行代碼巨玄乎溪厘,加了這個之后,前端應(yīng)用就無法正常訪問了(也就是說需要開發(fā)/api/**權(quán)限才能正常 訪問)
         */
//        httpSecurity.headers().frameOptions().sameOrigin().cacheControl();

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity
                .ignoring().antMatchers(HttpMethod.POST, "/login", "/auth")

                .and()
                .ignoring().antMatchers("/**/*.html", "/**/*.js", "/**/*.css");
    }
}

那個密碼加密方式是sringboot2中的改變牌柄。使用這種方式加密之后畸悬,在后臺存的不僅僅是 密碼的hash指,還包括hash加密方法的名字(用{}包起來)珊佣,如下:

{bcrypt}$2a$10$PurQWIEtutmzpPXFQS9G6eRMSj5kAtTQY58APfrY1CJD.grxqA6DK

CustomUserService 源碼如下蹋宦,其實就是根據(jù)用戶名從數(shù)據(jù)庫哪用戶信息

package com.hand.sxy.security;

@Service
public class CustomUserService implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(CustomUserService.class);

    @Autowired
    UserMapper userMapper;

    @Autowired
    RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            logger.error("username is empty");
            throw new UsernameNotFoundException("username is empty");
        }

        User user = userMapper.selectByUserName(username);
        if (null == user) {
            logger.error("get user is null, userName:{}", username);
            throw new UsernameNotFoundException("username is empty");
        }


        List<Role> roleList = roleMapper.queryByUser(user);
        Set<GrantedAuthority> authorities = new HashSet<>();
        if (!CollectionUtils.isEmpty(roleList)) {
            for (Role role : roleList) {
//                GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getUrl(), permission.getMethod());
                GrantedAuthority auth = new SimpleGrantedAuthority(role.getRoleCode());
                authorities.add(auth);
            }
        }

        UserDetails userDetails = new CustomUser(user.getUserId(), user.getUsername(), user.getPassword(),
                true, true, true, true, authorities, null);
        return userDetails;
    }

}

整合JWT

JWT是JSON Web Token的縮寫,即JSON Web令牌咒锻。JSON Web令牌(JWT)是一種緊湊的冷冗、URL安全的方式,用來表示要在雙方之間傳遞的“聲明”惑艇。JWT中的聲明被編碼為JSON對象蒿辙,用作JSON Web簽名(JWS)結(jié)構(gòu)的有效內(nèi)容或JSON Web加密(JWE)結(jié)構(gòu)的明文,使得聲明能夠被:數(shù)字簽名、或利用消息認證碼(MAC)保護完整性思灌、加密碰镜。

JWT構(gòu)成

一個JWT實際上就是一個字符串,它由三部分組成习瑰,頭部绪颖、載荷與 簽名依順序用點號(".")鏈接而成:headerpayload甜奄,signature

Header

頭部(Header)里面說明類型和使用的算法柠横,比如:

{
  "alg": "HS256",
  "typ": "JWT"
}

說明是JWT(JSON web token)類型,使用了HMAC SHA 算法课兄。然后將頭部進行base64加密(該加密是可以對稱解密的),構(gòu)成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload

載荷(Payload)載荷就是存放有效信息的地方,含三個部分:
1牍氛、標(biāo)準(zhǔn)中注冊的聲明
2、公共的聲明
3烟阐、私有的聲明

  • 標(biāo)準(zhǔn)中注冊的聲明搬俊,建議但不強制使用
    iss: jwt簽發(fā)者
    sub: jwt所面向的用戶
    aud: 接收jwt的一方
    exp: jwt的過期時間,這個過期時間必須要大于簽發(fā)時間
    nbf: 定義在什么時間之前蜒茄,該jwt都是不可用的
    iat: jwt的簽發(fā)時間
    jti: jwt的唯一身份標(biāo)識唉擂,主要用來作為一次性token,從而回避重放攻擊

  • 公共的聲明
    公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息.但不建議添加敏感信息檀葛,因為該部分在客戶端可解密.

  • 私有的聲明
    私有聲明是提供者和消費者所共同定義的聲明玩祟,一般不建議存放敏感信息,因為base64是對稱解密的屿聋,意味著該部分信息可以歸類為明文信息空扎。
    定義一個payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

然后將其進行base64加密,得到Jwt的第二部分润讥。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

把場景2的操作描述成一個json對象转锈。其中添加了一些其他的信息,幫助今后收到這個JWT的服務(wù)器理解這個JWT楚殿。 當(dāng)然撮慨,你還可以往載荷放非敏感的用戶信息,比如uid

signature

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串勒魔,然后通過header中聲明的加密方式進行加鹽secret組合加密甫煞,然后就構(gòu)成了jwt的第三部分。

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret');
//TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意secret是保存在服務(wù)器端的冠绢,jwt的簽發(fā)生成也是在服務(wù)器端的抚吠,secret就是用來進行jwt的簽發(fā)和jwt的驗證,所以弟胀,它就是你服務(wù)端的私鑰楷力,在任何場景都不應(yīng)該流露出去喊式。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發(fā)jwt了。

將這三部分用.連接成一個完整的字符串,構(gòu)成了最終的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

應(yīng)用場景

應(yīng)用場景:
1萧朝、瀏覽器將用戶名和密碼以post請求的方式發(fā)送給服務(wù)器岔留。

2蚯瞧、服務(wù)器接受后驗證通過是己,用一個密鑰生成一個JWT。

3颠焦、服務(wù)器將這個生成的JWT返回給瀏覽器何址。

4里逆、瀏覽器存儲JWT并在使用時將JWT包含在authorization header里面,然后發(fā)送請求給服務(wù)器用爪。

5原押、服務(wù)器可以在JWT中提取用戶相關(guān)信息。進行驗證偎血。

6诸衔、服務(wù)器驗證完成后,發(fā)送響應(yīng)結(jié)果給瀏覽器颇玷。

好吹就是無狀態(tài)笨农,在前后端分離的應(yīng)用中,后臺不需要存儲狀態(tài)亚隙,減輕服務(wù)器的壓力磁餐。

整合

這個又是借鑒了github上一位大神的代碼,其實我在好幾個地方看大了那個代碼了阿弃,需要引入一個jar,用于處理JWT的一些操作

<!-- JWT支持 -->
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

整體思路如下:
在 UsernamePasswordAuthenticationFilter 過濾器前添加一個過濾器羞延,該過濾器主要用是針對 JWT 的認證渣淳?就是說,當(dāng)前端的請求頭中伴箩,包含了token信息入愧,就通過自定義過濾器邏輯走認證過程,認證 通過之后嗤谚,把認證信息交給spring-security上下文棺蛛,然后繼續(xù)走spring-security的流程,就相當(dāng)于如果前端請求頭總有token信息巩步,并且后臺校驗這個token信息是沒有問題的旁赊,就讓spring-security認為這個請求時已經(jīng)通過校驗的,很巧妙椅野。如果沒有包含token终畅,就執(zhí)行 spring-security標(biāo)準(zhǔn)的認證流程籍胯。然后開放一個用于前端請求token的接口,這個接口不需要認證离福,認證通過之后杖狼,返回前端一個token,前端可以保存到 localstorage里面妖爷,也可以保存到cookie里面蝶涩。然后下次請求的時候,在請求頭中帶上這個token信息絮识。

security配置類

package com.hand.sxy.config;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.route.authentication.path}")
    private String authenticationPath;

    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;


    /**
     * 通過這種方式注入 authenticationManagerBean 绿聘,然后在別的地方也可以用
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 注冊 UserDetailsService 的 Bean
     *
     * @return
     */
    @Bean
    UserDetailsService customUserService() {
        return new CustomUserService();
    }

    /**
     * Sring5 中密碼加密新方式
     *
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
//        httpSecurity.rememberMe().rememberMeServices(rememberMeServices());
//        httpSecurity.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);

        httpSecurity
                .cors().and()
                .csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)

                /** 不創(chuàng)建 session **/
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                .antMatchers("/*.html", "/**/*.html", "/**/*.js", "/**/*.css").permitAll()
                .antMatchers("/login", "/register", "/auth", "/oauth/*").permitAll()
                .antMatchers("/api/role/query").hasRole("ADMIN")
                .anyRequest().authenticated()

                .and()
                .formLogin().loginPage("/login").loginProcessingUrl("/api/system/login").usernameParameter("username").passwordParameter("password").permitAll()

                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/api/system/logout").permitAll();


        /**
         * spring security過濾器鏈中,真正的用戶信息校驗是 UsernamePasswordAuthenticationFilter 過濾器笋除,然后才是權(quán)限校驗斜友。
         * 這里在 UsernamePasswordAuthenticationFilter過濾器之前 自定義一個過濾器,這樣就可以提前根據(jù)token將authenticate信息
         * 維護進speing security上下文垃它,然后在 UsernamePasswordAuthenticationFilter 得到的就已經(jīng)是通過校驗的用戶了鲜屏。
         */
        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(customUserService(), jwtTokenUtil, tokenHeader);
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        /**
         * disable page caching
         *
         * 下面這行代碼巨玄乎,加了這個之后国拇,前端應(yīng)用就無法正常訪問了(也就是說需要開發(fā)/api/**權(quán)限才能正常 訪問)
         */
//        httpSecurity.headers().frameOptions().sameOrigin().cacheControl();

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserService()).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity
                .ignoring().antMatchers(HttpMethod.POST, "/login", "/auth")

                .and()
                .ignoring().antMatchers("/**/*.html", "/**/*.js", "/**/*.css");
    }
}

注意上面的

.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
package com.hand.sxy.jwt;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

這表示當(dāng)你權(quán)限認證失敗的時候洛史,執(zhí)行 JwtAuthenticationEntryPoint 里面的 commence方法,返回給前端酱吝,用于自定義適合客戶閱讀的提示也殖。

這個就是開放的接口,用于前端請求token信息务热。

    /**
     * 認證接口忆嗜,用于前端獲取 JWT 的接口
     *
     * @param user
     * @return
     * @throws AuthenticationException
     */
    @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
    @ResponseBody
    public ResultResponse obtainToken(@RequestBody User user) throws AuthenticationException {

        /**
         * 通過調(diào)用 spring security 中的 authenticationManager 對用戶進行驗證
         */
        Objects.requireNonNull(user.getUsername());
        Objects.requireNonNull(user.getPassword());
        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
        } catch (DisabledException e) {
            throw new AuthenticationException("該已被被禁用,請檢查", e);
        } catch (BadCredentialsException e) {
            throw new AuthenticationException("無效的密碼崎岂,請檢查", e);
        }

        /**
         * 根據(jù)用戶名從數(shù)據(jù)庫獲取用戶信息捆毫,然后生成 token
         */
        final UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
        final String token = jwtTokenUtil.generateToken(userDetails);

        List<User> userList = userService.query(user);
        ResultResponse resultSet = new ResultResponse(true, token);
        resultSet.setRows(userList);
        return resultSet;
    }

這個是jwt工具類

package com.hand.sxy.jwt;

@Component
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = -3301605591108950415L;

    private Clock clock = DefaultClock.INSTANCE;

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(clock.now());
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    private Boolean ignoreTokenExpiration(String token) {
        // here you specify tokens, for that the expiration is ignored
        return false;
    }

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        final Date created = getIssuedAtDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token) || ignoreTokenExpiration(token));
    }

    public String refreshToken(String token) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        final Claims claims = getAllClaimsFromToken(token);
        claims.setIssuedAt(createdDate);
        claims.setExpiration(expirationDate);

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        CustomUser customUser = (CustomUser) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getIssuedAtDateFromToken(token);
        //final Date expiration = getExpirationDateFromToken(token);
        return (
                username.equals(customUser.getUsername()) && !isTokenExpired(token) && !isCreatedBeforeLastPasswordReset(created, customUser.getLastPasswordResetDate())
        );
    }

    private Date calculateExpirationDate(Date createdDate) {
        return new Date(createdDate.getTime() + expiration * 1000);
    }
}

這個是關(guān)鍵的過濾器

package com.hand.sxy.jwt;

public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;
    private String tokenHeader;

    public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.tokenHeader = tokenHeader;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        logger.debug("processing authentication for '{}'", request.getRequestURL());

        final String token = request.getHeader(this.tokenHeader);

        String username = null;
        if (token != null && !"".equals(token)) {
            try {
                username = jwtTokenUtil.getUsernameFromToken(token);
            } catch (IllegalArgumentException e) {
                logger.error("從Token中獲取用戶名失敗", e);
            } catch (ExpiredJwtException e) {
                logger.warn("這個Token已經(jīng)失效了", e);
            }
        } else {
            logger.warn("請求頭中未發(fā)現(xiàn) Token, 將執(zhí)行Spring Security正常的驗證流程");
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            logger.debug("security context was null, so authorizating user");

            // 也可以將用戶信息保存在token中,這時候就可以不用查數(shù)據(jù)庫
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // 校驗前端傳過來的Token是否有問題
            if (jwtTokenUtil.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                logger.info("用戶 '{}' 授權(quán)成功, 賦值給 SecurityContextHolder 上下文", username);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(request, response);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冲甘,一起剝皮案震驚了整個濱河市绩卤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌江醇,老刑警劉巖濒憋,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異陶夜,居然都是意外死亡凛驮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門律适,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辐烂,“玉大人遏插,你說我怎么就攤上這事【佬蓿” “怎么了胳嘲?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扣草。 經(jīng)常有香客問我了牛,道長,這世上最難降的妖魔是什么辰妙? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任鹰祸,我火速辦了婚禮,結(jié)果婚禮上密浑,老公的妹妹穿的比我還像新娘蛙婴。我一直安慰自己,他們只是感情好尔破,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布街图。 她就那樣靜靜地躺著,像睡著了一般懒构。 火紅的嫁衣襯著肌膚如雪餐济。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天胆剧,我揣著相機與錄音絮姆,去河邊找鬼。 笑死秩霍,一個胖子當(dāng)著我的面吹牛篙悯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铃绒,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼辕近,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了匿垄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤归粉,失蹤者是張志新(化名)和其女友劉穎椿疗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糠悼,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡届榄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了倔喂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铝条。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡靖苇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出班缰,到底是詐尸還是另有隱情贤壁,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布埠忘,位于F島的核電站脾拆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏莹妒。R本人自食惡果不足惜名船,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旨怠。 院中可真熱鬧渠驼,春花似錦、人聲如沸鉴腻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拘哨。三九已至谋梭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倦青,已是汗流浹背瓮床。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留产镐,地道東北人隘庄。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像癣亚,于是被迫代替她去往敵國和親丑掺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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