【JWT】初識與集成,及優(yōu)缺點

個人學習筆記分享挟纱,當前能力有限羞酗,請勿貶低,菜鳥互學紊服,大佬繞道

如有勘誤檀轨,歡迎指出和討論,本文后期也會進行修正和補充


前言

隨著分布式的普及围苫,session的成本正變得越來越高裤园,因而一種不需要session,而直接將身份信息放在token中的方案應運而生--JWT


請留意剂府,本文主要整理相關思路拧揽,為方便理解,示例代碼并不完整腺占,更談不上嚴謹

若需要實際使用的demo淤袜,請直接查看整理后的代碼,完整demo會傳到github或碼云


1.介紹

1.1.什么是JWT

JWT全程為Json web token衰伯,是一種開放標準(RFC 7519)铡羡,定義了一種緊湊的之景、自包含的方式腰奋,用于作為JSON對象在各方之間安全地傳輸信息贩幻。

這種數(shù)字簽名的設計,緊密且安全潦嘶,特別適用與分布式登錄和單點登錄的場景床佳。

JWT一般用于驗證身份怕品,也可以根據(jù)業(yè)務峡钓,添加其他業(yè)務邏輯所需要的的信息,如權限夭委。

JWT可以直接用于認證幅狮,也可以進行加密。


1.2.架構

image-20200826100521370.png

1.3.與傳統(tǒng)session認證的區(qū)別

打個比方:一個用戶在權限森嚴的地區(qū)活動

  • session認證:登記時告知用戶一個編號株灸,用戶到一個地方就報出自己的編號崇摄,然后工作人員去查詢這個編號能去哪不能去哪,以此來判斷是否放行
  • JWT認證:登記時發(fā)給用戶一個證件慌烧,用戶到一個地方就出示證件逐抑,工作人員確認證件是自己家的,然后直接看證件上寫了能去哪杏死,以此判斷是否放行

兩者的利弊顯而易見泵肄,

  • 傳統(tǒng)session認證的方案:傳統(tǒng)session認證,一般僅在前后端傳遞cookie淑翼,作為session的關鍵詞腐巢,后端再根據(jù)cookie查詢對應的session,從而確認登陸者的身份和權限等信息玄括。session通常存于緩存冯丙、數(shù)據(jù)庫或者redis等中間件,redis最為常見遭京。

  • 傳統(tǒng)session認證的弊端:無論將session存于何處胃惜,用戶登錄的時候都必須存儲認證信息,且大部分請求都執(zhí)行一次session查詢哪雕,因而

    • 隨著用戶越來越多船殉,服務器的開銷必然越來越大,認證速度也必然受到影響斯嚎。
    • 每次查詢session都必須請求其存儲的服務器利虫,無疑限制了分布式中負載均衡的能力
  • JWT的方案:JWT不需要將認證信息進行保存,直接將其加密后在前后端傳遞堡僻,后端進行解密即可獲取身份和其他聲明信息

  • JWT的優(yōu)勢:JWT的優(yōu)勢即解決了session認證的弊端糠惫,JWT將認證信息在前后端傳遞,而后端本身不存儲信息钉疫,因而

    • 后端不存儲信息硼讽,故服務器開銷極低,且認證速度不會受用戶數(shù)量影響
    • 后端直接解析token牲阁,無需請求其他的服務器固阁,不會給負載均衡帶來影響壤躲,易于擴展
    • 業(yè)務json的通用性,JWT也擁有了跨平臺的能力您炉,在Java柒爵、JS役电、NodeJS赚爵、PHP等語言均可使用
    • jwt一般存放于請求頭中,結構簡單且數(shù)據(jù)量很小法瑟,非常便于傳輸
  • JWT的弊端:有點多冀膝,留在文末說,不然可能會打消你繼續(xù)看下去的想法霎挟。窝剖。。綜合考慮其實我不建議JWT替代session認證


2.結構

JWT由三段信息組成:頭部(header)酥夭、載荷(payload)赐纱、簽證(signature)

https://jwt.io/可以模擬JWT的生成和解碼

2.1.header

頭部包含兩部分信息:
  • 聲明類型(typ):默認值即JWT,
  • 加密算法(alg):默認值為HS256,也可選擇其他加密算法
示例:
{
  'typ': 'JWT',
  'alg': 'HS256'
}

對應base64UrlEncode編碼為:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9


2.2.payload

載荷即存放有效數(shù)據(jù)的地方疙描,但是因為==可被解碼讶隐,不建議存放敏感信息==

包括三個部分:
  • 標準中注冊的聲明:建議但不強制使用,存放JWT相關的數(shù)據(jù)
    • iss: jwt簽發(fā)者
    • sub: jwt所面向的用戶
    • aud: 接收jwt的一方
    • exp: jwt的過期時間效五,這個過期時間必須要大于簽發(fā)時間
    • nbf: 定義在什么時間之前炉峰,該jwt都是不可用的.
    • iat: jwt的簽發(fā)時間
    • jti: jwt的唯一身份標識,主要用來作為一次性token疼阔,從而回避重放攻擊
  • 公共的聲明:可存放任何信息,一般添加用戶信息和相關業(yè)務的數(shù)據(jù)
  • 私有的聲明:提供者和消費者所共同定義的聲明
示例:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

對應base64UrlEncode編碼為:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ


2.3.signature

簽證實際上就是將header和payload進行base64編碼谱仪,再通過秘鑰加密后的密文,用于保證jwt不會被偽造或人為修改否彩,生成方式如下

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

因此秘鑰非常重要疯攒,必須保證其不會被泄露或破解,否則整個驗證系統(tǒng)將如同虛設


示例:

將前面兩個示例信息列荔,使用秘鑰echo進行加密的結果為QrtQpbkSSLyGt1qRQ4nZ3K0OcyO7CCv0HxIdsvYYSFU


3.使用流程

3.1.前端使用

前端只需在登錄成功后保存返回的token敬尺,在發(fā)起其他請求的時候枚尼,向請求頭中加入字段Authorization,并加上Bearer標注即可

fetch('api/user/info', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

發(fā)起的請求頭如下所示

image-20200826114453473


3.2.后端使用

登錄:
  • 接收登錄請求砂吞,驗證賬號密碼等信息署恍,確認其身份驗證無誤
  • 查詢其他業(yè)務相關信息,如身份權限等蜻直,組裝成payload數(shù)據(jù)
  • 通過預設規(guī)則盯质,生成JWT
  • 通過http的response返回JWT給前端,結束請求
其余請求:
  • 接收請求概而,取出頭字段Authorization呼巷,即認證信息
  • 根據(jù)預設規(guī)則,解碼獲得明文信息赎瑰,對JWT進行驗證
  • 獲取JWT中的業(yè)務相關信息王悍,并處理此請求相關業(yè)務
  • 返回業(yè)務結果給前端,結束請求


4.集成(基于Java+SpringBoot+AOP)

還有其他集成方法餐曼,有興趣的可以自行查閱

4.1.添加依賴

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

添加一個依賴就行了,其余的沒必要


# JWT
# 發(fā)行者
spring.jwt.name=echo
# 密鑰
spring.jwt.base64Secret=23333
# jwt中過期時間設置(分)
spring.jwt.jwtExpires=120

秘鑰是最核心的數(shù)據(jù)集惋,是保證令牌不被偽造的唯一防線芋膘,無論如何也不能泄露为朋,安全起見建議每個項目都生成隨機的字符串作為秘鑰


4.2.自定義注解

注解用于標記需要認證的目標厚脉,當然也可以標記不需要認證的目標霞溪,最好使用AOP自定義注解攔截中捆,來應對不同業(yè)務需求

  • 注解@JwtCheck泄伪,用于標記接口需要驗證染厅,只需要

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface JwtCheck {
        boolean required() default true;
    }
    
  • 其余注解請根據(jù)實際業(yè)務添加


4.3.JWT工具類

核心就是生成和解析token,還有驗證token

  • 生成token使用JWT.create()再組裝需要的參數(shù)即可孤页,因為方法都是限定好的行施,比較簡單悲龟,幾乎不可能出錯吧。斩芭。划乖。
  • 解析token直接解析請求頭Authorization的內容就行了琴庵,自行去除最前面的Bearer迷殿,剩余內容使用base64解碼即可
  • 驗證token是最關鍵的一步庆寺,也最好理解懦尝,重復一遍加密過程陵霉,然后把加密結果與簽名對比踊挠,兩者匹配即保證令牌不是偽造的

@Component
public class JwtUtils {

    /**
     * gson對象止毕,提前初始化
     */
    private final static Gson gson = new Gson();
    /**
     * jwt秘鑰
     */
    private static String jwtSecret;

    /**
     * jwt有效時間
     */
    private static Long jwtExpires;

    /**
     * jwt發(fā)行者名稱
     */
    private static String jwtName;

    @Value("${spring.jwt.base64Secret}")
    public void setJwtSecret(String jwtSecret) {
        this.jwtSecret = jwtSecret;
    }

    @Value("${spring.jwt.jwtExpires}")
    public void setJwtExpires(Long jwtExpires) {
        this.jwtExpires = jwtExpires;
    }

    @Value("${spring.jwt.name}")
    public void setJwtName(String jwtName) {
        this.jwtName = jwtName;
    }

    /**
     * 獲取token字符串
     *
     * @param data 對象
     * @return token字符串
     */
    public static String getToken(Object data) {
        String token = "";
        // 計算時間
        Date expiredDate = new Date(System.currentTimeMillis() + jwtExpires * 1000L);
        Date issuedDate = new Date();
        // 創(chuàng)建jwt
        token = JWT
                .create()
                .withAudience(gson.toJson(data))
                .withIssuer(jwtName)
                .withIssuedAt(issuedDate)
                .withExpiresAt(expiredDate)
                .sign(Algorithm.HMAC256(jwtSecret));
        return token;
    }

    /**
     * gson解碼
     *
     * @param encoded json字符串
     * @return 解碼后的對象
     */
    public static UserInfo decode(String encoded) {
        return gson.fromJson(encoded, UserInfo.class);
    }

    /**
     * 驗證token
     *
     * @param token token字符串
     * @throws Exception
     */
    public static void verifyToken(String token) throws Exception {
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
            throw new BaseException("身份驗證失敗");
        }
    }


    /**
     * 從http請求中解析token
     *
     * @param request http請求
     * @throws Exception 解析異常
     * @returntoken字符串
     */
    public static String getToken(HttpServletRequest request) throws Exception {
        String authorization = Strings.nullToEmpty(request.getHeader("Authorization"));
        if (!authorization.startsWith("Bearer")) {
            throw new BaseException("Token非JWT標準");
        }
        return authorization.substring(7);
    }
}

4.4.數(shù)據(jù)查詢

為方便測試僅做模擬查詢,實際應用請移步從數(shù)據(jù)庫查詢谨朝,可參考文末整理后的代碼

@Service
public class UserService {

    /**
     * 模擬數(shù)據(jù)庫的數(shù)據(jù)
     */
    private List<UserInfo> userInfoList;

    public UserService() {
        userInfoList = new ArrayList<>();
        userInfoList.add(new UserInfo(1, "user1", "pwd1", 1));
        userInfoList.add(new UserInfo(2, "user2", "pwd2", 2));
        userInfoList.add(new UserInfo(3, "user3", "pwd3", 3));

    }

    /**
     * 根據(jù)id查詢用戶
     *
     * @param id 用戶id
     * @return 用戶信息
     */
    public UserInfo getUserById(Long id) {
        for (UserInfo item : userInfoList) {
            if (Objects.equals(item.getId(), id)) {
                return item;
            }
        }
        return null;
    }

    /**
     * 校驗賬號
     *
     * @param username 賬號
     * @param password 密碼
     * @return 若查詢到賬號則返回賬號信息则披,否則返回null
     */
    public UserInfo checkUser(String username, String password) {

        for (UserInfo item : userInfoList) {
            if (Objects.equals(item.getUsername(), username) && Objects.equals(item.getPassword(), password)) {
                return item;
            }
        }
        return null;
    }

}


4.5.切面層攔截器

這里應該是最關鍵的地方士复,用于攔截注解標記的方法阱洪,在這里進行身份驗證菠镇,失敗則不再繼續(xù)利耍,成功則回到切入點

  • 使用@Pointcut設置切入點的條件
  • 在使用@Around設定切入的方法程癌,若中途驗證失敗席楚,則拋出異常烦秩,若成功則繼續(xù)執(zhí)行原有邏輯
@Aspect
@Component
@Slf4j
public class JwtInterceptAspect {

    /**
     * 切入點
     */
    @Pointcut("execution(* com.yezi_tool.demo_basic.controller..*(..))&&@annotation(com.yezi_tool.demo_basic.jwt.JwtCheck)")
    public void controllerAspect() {
    }

    /**
     * 切入方法
     */
    @Around("controllerAspect() ")
    public Object aroundMethod(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = null;
        UserInfo user = null;
        JwtCheck jwtCheck = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(JwtCheck.class);
        if (jwtCheck.required()) {
            request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
            String token = JwtUtils.getToken(request);
            //獲取token的userid
            List<String> audience = JWT.decode(token).getAudience();
           user = JwtUtils.decode(audience.get(0));
            if (user == null) {
                throw new BaseException("身份驗證失敗");
            }
            //驗證token只祠,秘鑰為用戶的密碼
            JwtUtils.verifyToken(token);
        }
        Object[] args = point.getArgs();
        UserInfo finalUser = user;
        args = Arrays.stream(args).map(arg -> {
            if (Objects.nonNull(arg) && UserInfo.class.isAssignableFrom(arg.getClass()))
                arg = finalUser;
            return arg;
        }).toArray();
        return point.proceed(args);
    }

}


4.6.控制層

簡單的寫兩個方法,一個用于登錄,一個用于測試登錄結果

@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {

    /**
     * 用戶信息業(yè)務層
     */
    private final UserService userService;

    public LoginController(UserService userService) {
        this.userService = userService;
    }


    @Data
    private static class LoginRequest {
        private String username;
        private String password;
        private Boolean rememberMe;
    }


    @PostMapping("/login")
    @ResponseBody
    public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
        ReturnMsg returnMsg = ReturnMsg.success();
        UserInfo userInfo = userService.checkUser(loginRequest.getUsername(), loginRequest.getPassword());
        if (userInfo == null) {
            throw new BaseException("賬號或密碼不正確");
        }
        Map<String, Object> data = new HashMap<>();
        data.put("id", userInfo.getId());
        data.put("username", userInfo.getUsername());
        returnMsg.setData(JwtUtils.getToken(data));
        return returnMsg;
    }

    @PostMapping("/test")
    @ResponseBody
    @JwtCheck
    public ReturnMsg test(Integer mark) throws Exception {
        ReturnMsg returnMsg = ReturnMsg.success();
        if (mark == null) {
            throw new BaseException("缺少參數(shù)");
        }
        returnMsg.setData(userInfo);
        return returnMsg;
    }
}

執(zhí)行結果

  • 執(zhí)行登錄接口,若賬號密碼正確則返回token字符串

    <img src="https://gitee.com/echo_ye/assets/raw/master/img/image-20200826181729989.png" alt="image-20200826181729989" />

  • 執(zhí)行測試接口川陆,將token放在請求頭里

    image-20200827094946884


5.JWT的弊端

以下均不考慮查詢數(shù)據(jù)庫或者緩存较沪,否則就相當于放棄自己僅有的優(yōu)勢

5.1.安全性

  • JWT令牌使用base64編碼尸曼,對于前端如同明文(http://jwt.calebb.net/不信你試試控轿?)解幽,那么敏感信息不適合放在令牌里,但如果從數(shù)據(jù)庫獲取便放棄了自己的優(yōu)勢
  • 秘鑰一旦泄露镣衡,客戶端便可以自己簽發(fā)令牌廊鸥,除非修改秘鑰辖所,那么所有令牌全部失效

5.2.不可修改

  • 如果令牌相關的內容被修改吆视,如賬號啦吧,身份授滓,姓名等般堆,只能讓用戶重新登錄
  • 令牌無法續(xù)簽淮摔,到時間即失效噩咪,除非將時間設定的極長

5.3.不可銷毀

  • 如果需要強制下線胃碾,或者報廢舊的令牌仆百,JWT完全無法做到

  • 就算下發(fā)新的令牌俄周,舊的令牌在時效內依然有效

5.4.性能

JWT的payload內容越多峦朗,令牌便越大翎朱,開銷也會越來越大拴曲,甚至超出cookie的長度限制(cookie一般限制在4k澈灼,而redis是512M叁熔。者疤。)驹马,請求帶的參數(shù)往往只有幾個糯累,本末倒置了吧效拭。缎患。挤渔。


以上問題在session認證均不會出現(xiàn)E械肌Q廴小擂红!


6.真正適合JWT的場景

6.1.一次性驗證

如用戶注冊后發(fā)送一封激活郵件,包括一個鏈接吩抓,需要用戶在時限內點擊,超時即失效雨饺,
那么需要的以下條件:

  • 能標記用戶额港,一般是id或者賬號進行標記
  • 有時效性移斩,通常只有幾個小時時效肠套,超時便失效
  • 不可被修改你稚,用于其他賬號或者用途

JWT的payload刁赖、expires宇弛、簽名都完美契合以上條件

6.2.restful api 的無狀態(tài)認證

jwt不在服務端存儲任何狀態(tài)涯肩。RESTful API的原則之一是無狀態(tài)病苗,發(fā)出請求時硫朦,總會返回帶有參數(shù)的響應,不會產(chǎn)生附加影響破婆。用戶的認證狀態(tài)引入這種附加影響祷舀,這破壞了這一原則裳扯。另外jwt的載荷中可以存儲一些常用信息饰豺,用于交換信息冤吨,有效地使用 JWT其馏,可以降低服務器查詢數(shù)據(jù)庫的次數(shù)叛复。


7.整理后代碼

7.1.整理內容

  • 優(yōu)化代碼結構

  • 可對類或者方法標記需要或跳過認證褐奥,并指定認證的身份

  • 統(tǒng)一使用AOP切面攔截請求撬码,進行身份認證呜笑,并在認證成功后將身份信息插入切入點

  • 允許不同身份的用戶登錄,并使用不同格式的令牌

  • 密碼使用鹽和MD5加密

  • 使用策略設計模式

7.2.核心源碼

自定義注解
package com.yezi_tool.demo_basic.jwt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Echo_Ye
 * @title 注解-jwt驗證
 * @description 用于標記接口jwt驗證
 * @date 2020/8/26 13:55
 * @email echo_yezi@qq.com
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    Class<?> value();
}
package com.yezi_tool.demo_basic.jwt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Echo_Ye
 * @title 注解-jwt不進行驗證
 * @description 用于標記接口jwt不進行驗證
 * @date 2020/8/26 13:55
 * @email echo_yezi@qq.com
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAuth {
}
自定義認證實體
package com.yezi_tool.demo_basic.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @title JWT認證虛擬類
 * @description
 * @author Echo_Ye
 * @date 2020/9/3 17:11
 * @email echo_yezi@qq.com
 */
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public abstract class AbstractAuthByJWT<T> {
    /**
     * 抽象方法-獲取類型
     */
    public abstract byte getUserType();
    /**
     * 抽象方法-解碼
     */
    protected abstract T decode(String encoded);

    /**
     * 抽象方法-編碼
     */
    protected abstract String encode(T bean);

    /**
     * 抽象方法-獲取bean對象
     */
    protected abstract T getBean(HttpServletRequest request) throws Exception;

    /**
     * 抽象方法-獲取默認時間
     */
    protected abstract Duration getDefaultDuration();

    /**
     * 相關鉤子-暫不使用
     */
    protected void hookOnCreate(JWTCreator.Builder builder) {
    }

    protected void hookOnAddHeader(Map<String, Object> header) {

    }

    protected void hookOnVerify(DecodedJWT jwt) {
    }

    /**
     * 加密算法
     */
    private final Algorithm algorithm;
    /**
     * jwt驗證器
     */
    private final JWTVerifier verifier;
    /**
     * 發(fā)布者,取當前class
     */
    private final String identity = this.getClass().getName();

    /**
     * 初始化
     *
     * @param secret 秘鑰
     */
    public AbstractAuthByJWT(String secret) {
        algorithm = Algorithm.HMAC256(Hashing.sha256().hashBytes(secret.getBytes()).asBytes());
        verifier = JWT.require(algorithm).build();
    }

    /**
     * 創(chuàng)建token
     */
    public String create(T auth) throws Exception {
        return create(auth, getDefaultDuration());
    }

    /**
     * 創(chuàng)建token
     */
    public String create(T auth, Duration duration) throws Exception {
        try {
            // 計算時間
            Date expiredDate = new Date(System.currentTimeMillis() + duration.getSeconds() * 1000L);
            Date issuedDate = new Date();

            // 序列化主數(shù)據(jù)
            String data = encode(auth);
            Preconditions.checkState(data.length() > 0);

            // 添加頭部信息
            HashMap<String, Object> header = Maps.newHashMap();
            hookOnAddHeader(header);
            JWTCreator.Builder builder = JWT.create()
                    .withHeader(header)
                    .withClaim(JwtConstants.JWT_REQUEST_CLAIM_KEY, data)
                    .withIssuer(identity)
                    .withIssuedAt(issuedDate)
                    .withExpiresAt(expiredDate);
            hookOnCreate(builder);
            return builder.sign(algorithm);
        } catch (Throwable e) {
            throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CREATE_TOKEN));
        }
    }

    /**
     * 驗證request
     *
     * @param request request請求
     * @return 返回泛型對象
     * @throws Exception
     */
    protected T verify(HttpServletRequest request) throws Exception {
        return verify(JwtUtils.getTokenFromHttpRequest(request));
    }

    /**
     * 驗證request
     *
     * @param token token字符串
     * @return 返回泛型對象
     */
    public T verify(String token)  {
        DecodedJWT jwt = verifier.verify(token);
        //校驗簽發(fā)者
        Preconditions.checkState(0 == jwt.getIssuer().compareTo(identity));
        //校驗數(shù)據(jù)
        Claim data = jwt.getClaim(JwtConstants.JWT_REQUEST_CLAIM_KEY);
        Preconditions.checkNotNull(data);
        //鉤子
        hookOnVerify(jwt);
        //解析
        T bean = decode(Strings.nullToEmpty(data.asString()));
        Preconditions.checkNotNull(bean);

        return bean;
    }


}
package com.yezi_tool.demo_basic.jwt;

import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;

/**
 * @author Echo_Ye
 * @title jwt學生驗證實體
 * @description jwt用于學生身份驗證的實體
 * @date 2020/8/28 13:47
 * @email echo_yezi@qq.com
 */
@Component
public class StudentAuthByJWT extends AbstractAuthByJWT<StudentAuthByJWT.Instance> {
    /**
     * 用戶類型
     */
    @Getter
    private final byte userType = CustomConstants.USER_TYPE_STUDENT;

    /**
     * 初始化
     *
     * @param secret 秘鑰
     */
    public StudentAuthByJWT(@Value("${spring.jwt.base64Secret}") String secret) {
        super(secret);
    }

    @Getter
    public static class Instance extends BaseAuthInstance {
        /**
         * 學號
         */
        private String num;

        /**
         * 年級id
         */
        private Long gradeId;

        public Instance(Integer id, List<String> permissionList, String userName, Byte type, Integer personId, String name, Byte gender, String num, Long gradeId) {
            super(id, permissionList, userName, type, personId, name, gender);
            this.num = num;
            this.gradeId = gradeId;
        }
    }


    @Bean("studentAuthByJWTInstance")
    @Scope(value = WebApplicationContext.SCOPE_REQUEST)     // 該bean僅在本次http request內有效
    @Override
    protected Instance getBean(HttpServletRequest request) throws Exception {
        return super.verify(request);
    }

    @Override
    protected Instance decode(String encoded) {
        return JwtUtils.decode(encoded, Instance.class);
    }

    @Override
    protected String encode(Instance bean) {
        return JwtUtils.encode(bean);
    }


    @Override
    protected Duration getDefaultDuration() {
        return Duration.ofDays(365);
    }


}
package com.yezi_tool.demo_basic.jwt;

import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import com.yezi_tool.demo_basic.entity.UserInfo;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;

/**
 * @author Echo_Ye
 * @title jwt教師驗證實體
 * @description jwt用于教師身份驗證的實體
 * @date 2020/8/28 13:47
 * @email echo_yezi@qq.com
 */
@Component
public class TeacherAuthByJWT extends AbstractAuthByJWT<TeacherAuthByJWT.Instance> {
    /**
     * 用戶類型
     */
    @Getter
    private final byte userType= CustomConstants.USER_TYPE_TEACHER;

    /**
     * 初始化
     *
     * @param secret 秘鑰
     */
    public TeacherAuthByJWT(@Value("${spring.jwt.base64Secret}") String secret) {
        super(secret);
    }

    @Getter
    public static class Instance extends BaseAuthInstance {
        /**
         * 學院id
         */
        private Long collegeId;

        public Instance(Integer id, List<String> permissionList, String userName, Byte type, Integer personId, String name, Byte gender, Long collegeId) {
            super(id, permissionList, userName, type, personId, name, gender);
            this.collegeId = collegeId;
        }
    }

    @Bean("teacherAuthByJWTInstance")
    @Scope(value = WebApplicationContext.SCOPE_REQUEST)
    @Override
    protected Instance getBean(HttpServletRequest request) throws Exception {
        return super.verify(request);
    }

    @Override
    protected Instance decode(String encoded) {
        return JwtUtils.decode(encoded, Instance.class);
    }

    @Override
    protected String encode(Instance bean) {
        return JwtUtils.encode(bean);
    }

    @Override
    protected Duration getDefaultDuration() {
        return Duration.ofDays(365);
    }


}
package com.yezi_tool.demo_basic.jwt;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

/**
 * @author Echo_Ye
 * @title jwt基礎instance
 * @description 基礎instance
 * @date 2020/8/28 17:54
 * @email echo_yezi@qq.com
 */
@Getter
@AllArgsConstructor
public class BaseAuthInstance {

    /**
     * id
     */
    private Integer id;
    /**
     * 權限列表
     */
    private List<String> permissionList;
    /**
     * 用戶名
     */
    private String userName;
    /**
     * 用戶類型
     */
    private Byte type;
    /**
     * 信息表id
     */
    private Integer personId;

    /**
     * 用戶姓名
     */
    private String name;

    /**
     * 用戶性別
     */
    private Byte gender;

}
自定義切面攔截器
package com.yezi_tool.demo_basic.jwt;

import com.google.common.base.Preconditions;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Objects;

/**
 * @author Echo_Ye
 * @title 身份AOP攔截器
 * @description 使用AOP攔截身份
 * @date 2020/9/3 15:28
 * @email echo_yezi@qq.com
 */
@Slf4j
@Aspect
@Component
public class AuthInterceptor {
    /**
     * AOP切入點
     */
    @Pointcut("(@annotation(com.yezi_tool.demo_basic.jwt.Auth) || @within(com.yezi_tool.demo_basic.jwt.Auth)) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
    public void authPointcut() {
    }

    /**
     * AOP方法切入點
     */
    @Pointcut("@annotation(com.yezi_tool.demo_basic.jwt.Auth) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
    public void authAnnotationPointcut() {
    }

    /**
     * AOP對象切入點
     */
    @Pointcut("@within(com.yezi_tool.demo_basic.jwt.Auth) && !@annotation(com.yezi_tool.demo_basic.jwt.NoAuth)")
    public void authWithinPointcut() {
    }

    /**
     * AOP對象切入內容
     */
    @Around("authWithinPointcut() && @within(auth)")
    public Object checkWithinAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
        return checkAuth(joinPoint, auth);
    }

    /**
     * AOP方法切入內容
     */
    @Around("authAnnotationPointcut() && @annotation(auth)")
    public Object checkAnnotationAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
        return checkAuth(joinPoint, auth);
    }


    /**
     * AOP切入內容
     *
     * @param joinPoint 切入點
     * @param auth      切入注解
     * @return
     * @throws Exception 拋出異常为牍,驗證失敗
     */
    public Object checkAuth(ProceedingJoinPoint joinPoint, Auth auth) throws Exception {
        try {
            //認證實體
            Object authBean = SpringContextHolder.getBean(auth.value());
            Preconditions.checkNotNull(authBean);

            //插入數(shù)據(jù)到切入點
            Object[] args = joinPoint.getArgs();
            args = Arrays.stream(args).map(arg -> {
                if (Objects.nonNull(arg) && arg.getClass().isAssignableFrom(auth.value()))
//                if (Objects.nonNull(arg) && arg instanceof BaseAuthInstance)//效果一樣,但上面的更利于擴展
                    arg = authBean;
                return arg;
            }).toArray();
            return joinPoint.proceed(args);
        } catch (Throwable e) {
            e.printStackTrace();
            throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CHECK_AUTH));
        }

    }
}
jwt服務
package com.yezi_tool.demo_basic.service;

import com.yezi_tool.demo_basic.jwt.AbstractAuthByJWT;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Echo_Ye
 * @title jwt服務
 * @description jwt服務蛀恩,待擴充
 * @date 2020/9/3 17:17
 * @email echo_yezi@qq.com
 */
@Service
public class JwtService {
    Map<Byte, AbstractAuthByJWT> jwtAuthMap = new HashMap<>();

    public JwtService(List<AbstractAuthByJWT> abstractAuthByJWTList) {
        for (AbstractAuthByJWT auth : abstractAuthByJWTList) {
            jwtAuthMap.put(auth.getUserType(), auth);
        }
    }

    public AbstractAuthByJWT getAuth(byte type) {
        return jwtAuthMap.get(type);
    }
}
服務層實現(xiàn)類
package com.yezi_tool.demo_basic.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yezi_tool.demo_basic.commons.constants.CommonConstants;
import com.yezi_tool.demo_basic.commons.constants.CustomConstants;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.constants.ResponseConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.commons.utils.JwtUtils;
import com.yezi_tool.demo_basic.entity.PermissionInfo;
import com.yezi_tool.demo_basic.entity.StudentInfo;
import com.yezi_tool.demo_basic.entity.TeacherInfo;
import com.yezi_tool.demo_basic.entity.UserInfo;
import com.yezi_tool.demo_basic.jwt.StudentAuthByJWT;
import com.yezi_tool.demo_basic.jwt.TeacherAuthByJWT;
import com.yezi_tool.demo_basic.mapper.PermissionInfoMapper;
import com.yezi_tool.demo_basic.mapper.StudentInfoMapper;
import com.yezi_tool.demo_basic.mapper.TeacherInfoMapper;
import com.yezi_tool.demo_basic.mapper.UserInfoMapper;
import com.yezi_tool.demo_basic.service.IUserInfoService;
import com.yezi_tool.demo_basic.service.JwtService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;

/**
 * @title 用戶信息服務層
 * @description
 * @author Echo_Ye
 * @date 2020/9/3 17:18
 * @email echo_yezi@qq.com
 */
@Slf4j
@Service("userInfoService")
public class UserInfoServiceImpl extends BaseServiceImpl<UserInfo> implements IUserInfoService {

    private final UserInfoMapper userInfoMapper;
    private final PermissionInfoMapper permissionInfoMapper;
    private final StudentInfoMapper studentInfoMapper;
    private final TeacherInfoMapper teacherInfoMapper;

    private final JwtService jwtService;

    public UserInfoServiceImpl(UserInfoMapper userInfoMapper, PermissionInfoMapper permissionInfoMapper, StudentInfoMapper studentInfoMapper, TeacherInfoMapper teacherInfoMapper, JwtService jwtService) {
        this.userInfoMapper = userInfoMapper;
        this.permissionInfoMapper = permissionInfoMapper;
        this.studentInfoMapper = studentInfoMapper;
        this.teacherInfoMapper = teacherInfoMapper;
        this.jwtService = jwtService;
    }


    @Override
    public UserInfo selectByUserName(String userName) {
        return userInfoMapper.selectOne(new QueryWrapper<UserInfo>().eq(UserInfo.COL_USERNAME, userName));
    }

    @Override
    public UserInfo selectByMobile(String mobile) {
        return userInfoMapper.selectOne(new QueryWrapper<UserInfo>().eq(UserInfo.COL_MOBILE, mobile));
    }

    @Override
    public List<String> queryPermission(String username) {
        List<Map<String, Object>> list = permissionInfoMapper.selectByUsername(username);
        List<String> permissionList = list.size() > 0 ? list.stream().
                map(m -> String.valueOf(m.get(PermissionInfo.COL_PERMISSION_NAME))).
                collect(Collectors.toList()) : new ArrayList<>();
        return permissionList;
    }

    @Override
    public UserInfo checkUser(String username, String password) {
        return userInfoMapper.selectOne(new QueryWrapper<UserInfo>()
                .eq(UserInfo.COL_USERNAME, username)
                .eq(UserInfo.COL_PASSWORD, password)
        );
    }

    @Override
    public String loginByMobileAndPassword(String mobile, String password) throws Exception {
        return loginGeneric(selectByMobile(mobile)
                , user -> JwtUtils.verifyPassword(user.getUsername(), user.getPassword(), user.getSalt(), password)
        );
    }

    @Override
    public String loginByUserNameAndPassword(String username, String password) throws Exception {
        return loginGeneric(selectByUserName(username)
                , user -> JwtUtils.verifyPassword(user.getUsername(), user.getPassword(), user.getSalt(), password)
        );
    }

    /**
     * 通用登錄邏輯
     *
     * @param user     用戶實體
     * @param callback 驗證回調
     * @return Token
     * @throws BaseException 登錄異常
     */
    private String loginGeneric(UserInfo user, Function<UserInfo, Boolean> callback) throws Exception {
        //檢查賬號
        if (null == user) {
            throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_INCORRECT_USERNAME);
        }
        //檢查密碼
        if (!callback.apply(user)) {
            throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_INCORRECT_PASSWORD);
        }
        //檢查賬號是否被禁用
        if (user.getStatus() == CommonConstants.STATE_DISABLED) {
            throw new BaseException(ResponseConstants.RETURN_MSG_LOGIN_ACCOUNT_DISABLE);
        }
        //判斷登陸者類型
        String token = makeToken(user);
        if (StringUtils.isBlank(token)) {
            throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CREATE_TOKEN));
        }
        return token;
    }

    /**
     * 根據(jù)用戶類型生成token谓厘,主要用戶判斷用戶類型
     *
     * @param userInfo 用戶信息
     * @throws Exception 可能拋出自定義異常
     * @return生成的toen字符串
     */
    public String makeToken(UserInfo userInfo) throws Exception {
        //開始生成token
        String token = "";
        switch (userInfo.getType()) {
            case CustomConstants.USER_TYPE_ADMIN:
                //管理員竟稳,暫不處理該類型人員
                break;
            case CustomConstants.USER_TYPE_STUDENT:
                //學生
                StudentInfo studentInfo = studentInfoMapper.selectById(userInfo.getPersonId());
                token = jwtService.
                        getAuth(CustomConstants.USER_TYPE_STUDENT).
                        create(new StudentAuthByJWT.Instance(
                                userInfo.getId(),
                                queryPermission(userInfo.getUsername()),
                                userInfo.getUsername(),
                                userInfo.getType(),
                                userInfo.getPersonId(),
                                studentInfo.getName(),
                                studentInfo.getGender(),
                                studentInfo.getNum(),
                                studentInfo.getGradeId()));
                break;
            case CustomConstants.USER_TYPE_TEACHER:
                //老師
                TeacherInfo teacherInfo = teacherInfoMapper.selectById(userInfo.getPersonId());
                token = jwtService.
                        getAuth(CustomConstants.USER_TYPE_TEACHER).
                        create(new TeacherAuthByJWT.Instance(
                                userInfo.getId(),
                                queryPermission(userInfo.getUsername()),
                                userInfo.getUsername(),
                                userInfo.getType(),
                                userInfo.getPersonId(),
                                teacherInfo.getName(),
                                teacherInfo.getGender(),
                                teacherInfo.getCollegeId()));
                break;
            default:
                break;
        }
        return token;
    }
}

jwt相關工具類
package com.yezi_tool.demo_basic.commons.utils;

import com.google.common.base.Strings;
import com.google.common.hash.Hashing;
import com.google.gson.Gson;
import com.yezi_tool.demo_basic.commons.constants.JwtConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author Echo_Ye
 * @title jwt工具
 * @description 封裝jwt相關方法
 * @date 2020/8/26 17:46
 * @email echo_yezi@qq.com
 */
@Component
public class JwtUtils {

    /**
     * gson對象果善,提前初始化
     */
    private final static Gson gson = new Gson();

    /**
     * gson解碼
     *
     * @param encoded json字符串
     * @param t       解碼目標類型
     * @param <T>     解碼目標類型
     * @return 解碼后的對象
     */
    public static <T> T decode(String encoded, Class<T> t) {
        return gson.fromJson(encoded, t);
    }

    /**
     * gson編碼
     *
     * @param t 需要編碼的對象
     * @return 編碼后的結果
     */
    public static String encode(Object t) {
        return gson.toJson(t);
    }

    /**
     * 從http請求中解析token
     *
     * @param request http請求
     * @return token字符串
     * @throws Exception 解析異常
     */
    public static String getTokenFromHttpRequest(HttpServletRequest request) throws Exception {
        String authorization = Strings.nullToEmpty(request.getHeader(JwtConstants.JWT_REQUEST_HEAD_KEY));
        if (!authorization.startsWith(JwtConstants.JWT_REQUEST_HEAD_PREFIX)) {
            throw new BaseException(ReturnMsg.error(JwtConstants.JWT_MSG_ERROR_CHECK_AUTH));
        }
        return authorization.substring(7);
    }
    /**
     * 生成鹽
     */
    public static String generateSalt() {
        return RandomStringUtils.randomAlphanumeric(16);
    }

    /**
     * 密碼加密
     */
    public static String encryptPassword(String password, String salt) {
        return Hashing.hmacMd5(salt.getBytes()).hashBytes(password.getBytes()).toString();
    }

    /**
     * 密碼驗證
     */
    public static boolean verifyPassword(String username,String password, String salt, String encryptedPassword) {
        return encryptPassword(Strings.nullToEmpty(encryptedPassword), salt).equals(password);
    }
}
控制層接口
package com.yezi_tool.demo_basic.controller;

import com.yezi_tool.demo_basic.commons.constants.ResponseConstants;
import com.yezi_tool.demo_basic.commons.exception.BaseException;
import com.yezi_tool.demo_basic.commons.model.ReturnMsg;
import com.yezi_tool.demo_basic.jwt.*;
import com.yezi_tool.demo_basic.service.IUserInfoService;
import lombok.Data;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * @author Echo_Ye
 * @title 登錄接口
 * @description 用于登錄相關接口
 * @date 2020/8/17 9:39
 * @email echo_yezi@qq.com
 */
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {

    /**
     * 用戶信息業(yè)務層
     */
    private final IUserInfoService userInfoService;

    public LoginController(IUserInfoService userInfoService) {
        this.userInfoService = userInfoService;
    }


    @Data
    private static class LoginRequest {
        private String username;
        private String password;
        private Boolean rememberMe;
    }


    @PostMapping("/login")
    @ResponseBody
    public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
        ReturnMsg returnMsg = ReturnMsg.success();
        String token = userInfoService.loginByUserNameAndPassword(loginRequest.getUsername(), loginRequest.getPassword());
        returnMsg.setData(token);
        return returnMsg;
    }

    @PostMapping("/testTeacher")
    @ResponseBody
    @Auth(TeacherAuthByJWT.Instance.class)
    public ReturnMsg testTeacher(BaseAuthInstance auth, Integer mark) throws Exception {
        ReturnMsg returnMsg = ReturnMsg.success();
        if (mark == null) {
            throw new BaseException(ResponseConstants.RETURN_MSG_ABNORMAL_PARAM);
        }
        returnMsg.setData(auth);
        return returnMsg;
    }

    @PostMapping("/testStudent")
    @ResponseBody
    @Auth(StudentAuthByJWT.Instance.class)
    public ReturnMsg testStudent(BaseAuthInstance auth, Integer mark) throws Exception {
        ReturnMsg returnMsg = ReturnMsg.success();
        if (mark == null) {
            throw new BaseException(ResponseConstants.RETURN_MSG_ABNORMAL_PARAM);
        }
        returnMsg.setData(auth);
        return returnMsg;
    }
}
運行截圖
  • 登錄

    image-20200903172126432
  • 驗證

    image-20200904171723793

7.3.全部代碼

demo地址:https://gitee.com/echo_ye/jwt-demo

demo已能正常運轉預期所有功能晾匠,但僅供參考混聊,請視實際業(yè)務自行刪減和修改句喜,有疑問或者建議可以留言或者聯(lián)系我~


BB兩句

其實考慮到JWT的弊端咳胃,JWT在與傳統(tǒng)session認證的比較之下展懈,并不具備太多優(yōu)勢存崖,甚至是部分地方有著無法彌補的劣勢

==權衡之下睡毒,個人不建議使用JWT取代session認證==



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

個人站點:在搭了在搭了供搀。葛虐。。(右鍵 - 新建文件夾)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赞季,隨后出現(xiàn)的幾起案子申钩,更是在濱河造成了極大的恐慌撒遣,老刑警劉巖义黎,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狐蜕,居然都是意外死亡层释,警方通過查閱死者的電腦和手機贡羔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宵统,“玉大人马澈,你說我怎么就攤上這事∨ⅲ” “怎么了痊班?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長摹量。 經(jīng)常有香客問我涤伐,道長,這世上最難降的妖魔是什么缨称? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任凝果,我火速辦了婚禮器净,結果婚禮上,老公的妹妹穿的比我還像新娘浪慌。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般夏漱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上交播,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天隧土,我揣著相機與錄音,去河邊找鬼皆愉。 笑死练链,一個胖子當著我的面吹牛,可吹牛的內容都是我干的疚沐。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼动遭,長吁一口氣:“原來是場噩夢啊……” “哼偷仿!你這毒婦竟也來了?” 一聲冷哼從身側響起全跨,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后浦徊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呢岗,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡挫酿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡轮洋,死狀恐怖汉柒,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤乓诽,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響筷畦,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一周偎、第九天 我趴在偏房一處隱蔽的房頂上張望胡嘿。 院中可真熱鬧,春花似錦罐监、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背浙芙。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拐迁,可洞房花燭夜當晚...
    茶點故事閱讀 45,876評論 2 361