二、SpringBoot 整合 JWT

SpringBoot 整合 JWT

1 什麼是 JWT

  JSON Web Token(JWT)是一個非常輕巧的規範,這個規範允許我們使用 JWT 在用戶和服務器之間傳遞安全可靠的信息,在 Java 世界中通過 JJWT 實現 JWT 創建和驗證。

2 快速上手

2.1 pom.xml

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

2.2 測試類

import io.jsonwebtoken.*;

import java.util.Date;

/**
 * @author PkyShare
 * @date 2020/1/3 0003 16:06
 */
public class JwtTest {

    private final static String key = "share"; // 祕鑰

    public static void main(String[] args) {
        String token = created();
        parse(token);
    }

    /**
     * 創建token
     */
    public static String created() {
        JwtBuilder jwtBuilder = Jwts.builder().setId("123456") // 設置ID
                .setSubject("PkyShare") // 存放的內容
                .setIssuedAt(new Date()) // 簽名簽發時間
                .signWith(SignatureAlgorithm.HS256, key) // 加密算法以及祕鑰
                .claim("school", "gcd");  // 自定義內容,key-value 形式
        String token = jwtBuilder.compact();
        System.out.println(token); // 創建 JwtBuilder 對象並打印
        return token;
    }

    /**
     * 解析 token
     * @param token
     */
    public static void parse(String token) {
        Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
        System.out.println(claims.getId());
        System.out.println(claims.getSubject());
        System.out.println(claims.getIssuedAt());
        String school = (String) claims.get("school");
        System.out.println("school----" + school);
    }
}

2.3 測試結果

在這裏插入圖片描述

3 SpringBoot 整合 JWT

  以上簡單的使用我們基本瞭解了 token 的生成,接下來完成一個簡單的登錄邏輯。

3.1 pom.xml

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

3.2 JwtUtil 工具類

import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * JWT 工具類
 * @author PkyShare
 * @date 2020/1/3 0003 16:39
 */
public class JwtUtil {
    //token 密鑰
    private static final String TOKEN_SECRET = "dfsjeo329safei22kdfeiajdeie1";
    //15分鐘超時時間
    private static final long OUT_TIME = 150 * 60 * 1000;

    /**
     * 用戶登錄成功後生成Jwt
     * 使用Hs256算法  私匙使用用戶密碼
     * @param user 登錄成功的user對象
     * @return
     */
    public static String createJWT(UserInfo user) {
        //指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成JWT的時間
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", user.getId());
        claims.put("username", user.getUsername());
        claims.put("password", user.getPassword());
        //生成簽名的時候使用的祕鑰secret,這個方法本地封裝了的,一般可以從本地配置文件中讀取,切記這個祕鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
//        String key = user.getPassword();
        //生成簽發人
        String subject = user.getUsername();
        //下面就是在爲payload添加各種標準聲明和私有聲明瞭
        //這裏其實就是new一個JwtBuilder,設置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
                .setClaims(claims)
                //設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的簽發時間
                .setIssuedAt(now)
                //代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什麼userid,roldid之類的,作爲什麼用戶的唯一標誌。
                .setSubject(subject)
                //設置簽名使用的簽名算法和簽名使用的祕鑰
                .signWith(signatureAlgorithm, TOKEN_SECRET);
        long expMillis = nowMillis + OUT_TIME;
        Date exp = new Date(expMillis);
        //設置過期時間
        builder.setExpiration(exp);
        return builder.compact();
    }


    /**
     * Token的解密
     * @param token 加密後的token
     * @return
     */
    public static Claims parseJWT(String token) {
        //簽名祕鑰,和生成的簽名的祕鑰一模一樣
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //設置簽名的祕鑰
                .setSigningKey(TOKEN_SECRET)
                //設置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }


    /**
     * 校驗token
     * 在這裏可以使用官方的校驗,我這裏校驗的是token中攜帶的密碼於數據庫一致的話就校驗通過
     * @param claims Payload(載荷)
     * @param user
     * @return
     */
    public static Boolean isVerify(Claims claims, UserInfo user) {
        if (claims.get("password").equals(user.getPassword())) {
            return true;
        }
        return false;
    }

}

3.3 攔截器

  除了登錄、註冊請求外,其他請求都需要攜帶 token,因此需要設置一個攔截器進行攔截。

3.3.1 @LoginToken 登錄註解

  方法上用到該註解的則跳過 token 驗證。

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

/**
 * @author PkyShare
 * @date 2020/1/3 0003 17:25
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
    boolean required() default true;
}

3.3.2 @CheckToken 校驗註解

  方法上用到該註解的則校驗 token。

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

/**
 * @author PkyShare
 * @date 2020/1/3 0003 17:24
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
    boolean required() default true;
}

3.3.3 攔截器配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author PkyShare
 * @date 2020/1/3 0003 17:49
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");    // 攔截所有請求,通過判斷是否有 @LoginRequired 註解 決定是否需要登錄
    }

    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

3.3.4 Token 攔截器

import com.alibaba.fastjson.JSONObject;
import com.huanda.chetaijitoc.admin.annotation.CheckToken;
import com.huanda.chetaijitoc.admin.annotation.LoginToken;
import com.huanda.chetaijitoc.admin.annotation.QueryToken;
import com.huanda.chetaijitoc.commons.constant.HttpStatus;
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import com.huanda.chetaijitoc.commons.service.userdb.UserInfoService;
import com.huanda.chetaijitoc.commons.service.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * token 攔截器
 * @author PkyShare
 * @date 2020/1/3 0003 17:28
 */
public class AuthenticationInterceptor implements HandlerInterceptor{

    @Autowired
    UserInfoService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        // 從 http 請求頭中取出 token
        String token = request.getHeader("token");
        // 如果不是映射到方法直接通過
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        //檢查是否有LoginToken註釋,有則跳過認證
        if (method.isAnnotationPresent(LoginToken.class)) {
            LoginToken loginToken = method.getAnnotation(LoginToken.class);
            if (loginToken.required()) {
                return true;
            }
        }
        //檢查是否有QueryToken註釋,有則判斷token
        if(method.isAnnotationPresent(QueryToken.class)) {
            QueryToken queryToken = method.getAnnotation(QueryToken.class);
            if(queryToken.required()) {
                return setRequest(request, response, token);
            }
        }
        //檢查有沒有需要用戶權限的註解
        if (method.isAnnotationPresent(CheckToken.class)) {
            CheckToken checkToken = method.getAnnotation(CheckToken.class);
            if (checkToken.required()) {
                return checkToken(request, response, token);
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {

    }

    /**
     * 違章查詢時設置request
     * @param request
     * @param response
     * @param token
     * @return
     * @throws Exception
     */
    private boolean setRequest(HttpServletRequest request, HttpServletResponse response, String token) throws Exception {
        if(StringUtils.isBlank(token)) {
            return true;
        }
        return checkToken(request, response, token);
    }

    /**
     * 校驗 token
     * @param request
     * @param response
     * @param token
     * @return
     */
    private boolean checkToken(HttpServletRequest request, HttpServletResponse response, String token) throws Exception {
        // 執行認證
        if (StringUtils.isBlank(token)) {
            setResultJson(response, HttpStatus.MISSING_PARAMS.getCode(), "Token 不可爲空");
            return false;
        }
        // 獲取 token 中的 user id
        Long userId;
        Claims claims;
        try {
            claims = JwtUtil.parseJWT(token);
            userId = (Long) claims.get("id");
        }
        catch (ExpiredJwtException e) { // 簽名過期
            setResultJson(response, HttpStatus.TOKEN_EXPIRED.getCode(), HttpStatus.TOKEN_EXPIRED.getTitle());
            return false;
        }
        catch (SignatureException e) { // 簽名錯誤
            setResultJson(response, HttpStatus.TOKEN_ERROR.getCode(), HttpStatus.TOKEN_ERROR.getTitle());
            return false;
        }
        catch (Exception e) { // 其他未知異常
            setResultJson(response, HttpStatus.ABNORMAL_ACCESS.getCode(), HttpStatus.ABNORMAL_ACCESS.getTitle());
            return false;
        }
        UserInfo user = userService.getById(userId);
        if (user == null) {
            setResultJson(response, HttpStatus.USER_NOT_EXIT.getCode(), HttpStatus.USER_NOT_EXIT.getTitle());
            return false;
        }
        Boolean verify = JwtUtil.isVerify(claims, user);
        if (!verify) {
            setResultJson(response, HttpStatus.USER_INFO_ERROR.getCode(), HttpStatus.USER_INFO_ERROR.getTitle());
            return false;
        }
        request.setAttribute("userInfo", user);
        return true;
    }

    /**
     * 設置響應json數據
     * @param response
     * @param code 狀態碼
     * @param title 返回說明
     * @return
     */
    private void setResultJson(HttpServletResponse response, Integer code, String title) throws Exception{
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        JSONObject resultJson = new JSONObject();
        resultJson.put("code", code);
        resultJson.put("count", 0);
        resultJson.put("title", title);
        writer.append(resultJson.toJSONString());
    }

}

3.4 自定義返回狀態碼

/**
 * HTTP 狀態碼
 */
public enum HttpStatus {
    OK(20000, "請求成功"),
    SUCCESS(20001, "保存成功"),
    DELETE(20004, "刪除成功"),
    FORBIDDEN(40003, "權限不足"),
    NOT_FOUND(40004, "資源未找到"),
    USER_NOT_EXIT(40081, "用戶不存在"),
    TOKEN_ERROR(40082, "簽名錯誤"),
    TOKEN_EXPIRED(40083, "Token 過期,請重新登錄"),
    USER_INFO_ERROR(40084, "賬號或密碼錯誤"),
    ABNORMAL_ACCESS(40091, "異常訪問"),

    private Integer code;
    private String title;

    HttpStatus(Integer code, String title) {
        this.code = code;
        this.title = title;
    }

    public Integer getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }
}

3.5 Controller

package com.huanda.chetaijitoc.admin.controller.userdb;

import com.huanda.chetaijitoc.admin.annotation.LoginToken;
import com.huanda.chetaijitoc.admin.controller.base.AbstractBaseController;
import com.huanda.chetaijitoc.commons.constant.HttpStatus;
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import com.huanda.chetaijitoc.commons.dto.AbstractBaseResult;
import com.huanda.chetaijitoc.commons.dto.LoadReturnData;
import com.huanda.chetaijitoc.commons.service.userdb.UserInfoService;
import com.huanda.chetaijitoc.commons.utils.BeanValidator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;

/**
 * 用戶基本信息表控制器
 */
@RestController
@RequestMapping(value = "users")
public class UserInfoController extends AbstractBaseController<UserInfo> {

    @Autowired
    UserInfoService userInfoService;

    /**
     * 登錄
     * @param userInfo 用戶信息
     * @return
     */
    @PostMapping(value = "login")
    @LoginToken
    public AbstractBaseResult login(@RequestBody UserInfo userInfo) {
        LoadReturnData<UserInfo> loadReturnData = new LoadReturnData<>();
        loadReturnData = userInfoService.getByUsername(loadReturnData, userInfo);
        return result(loadReturnData.getCode(), loadReturnData.getMsg(), loadReturnData.getToken());
    }

    /**
     * 註冊
     * @param userInfo 用戶信息
     * @return
     */
    @LoginToken
    @PostMapping(value = "register")
    public AbstractBaseResult register(@RequestBody UserInfo userInfo) {
        // 數據校驗
        String message = BeanValidator.validator(userInfo);
        if(StringUtils.isNotBlank(message)) {
            return result(HttpStatus.MISSING_PARAMS.getCode(), 0, message, null);
        }
        LoadReturnData<UserInfo> loadReturnData = new LoadReturnData<>();
        loadReturnData = userInfoService.registe(loadReturnData, userInfo);
        return result(loadReturnData.getCode(), 0, loadReturnData.getMsg(), null);
    }
    
    @CheckToken
    @PostMapping(value = "/test")
    public AbstractBaseResult test() {
        return result(HttpStatus.OK.getCode(), 0, HttpStatus.OK.getTitle(), null);
    }
    
}

注:上述 AbstractBaseResult(統一返回結果)、LoadReturnData、BeanValidator 和 AbstractBaseController 是自己封裝的,這裏可以暫不理會,用自己的寫的返回即可。

3.6 UserInfoserviceImpl

    /**
     * 通過用戶名獲取用戶信息並設置 token
     * @param loadReturnData 承載數據模型
     * @param userInfo 登錄用戶信息
     * @return
     */
    @Override
    public LoadReturnData<UserInfo> getByUsername(LoadReturnData<UserInfo> loadReturnData, UserInfo userInfo){
        Example example = new Example(UserInfo.class);
        example.createCriteria().andEqualTo("username", userInfo.getUsername());
        UserInfo userDB = userInfoMapper.selectOneByExample(example);
        if(userDB == null) {
            loadReturnData.setMsg(HttpStatus.USER_NOT_EXIT.getTitle());
            loadReturnData.setCode(HttpStatus.USER_NOT_EXIT.getCode());
            return loadReturnData;
        }
        if(!userDB.getPassword().equals(userInfo.getPassword())) {
            loadReturnData.setMsg(HttpStatus.USER_INFO_ERROR.getTitle());
            loadReturnData.setCode(HttpStatus.USER_INFO_ERROR.getCode());
            return loadReturnData;
        }
        loadReturnData.setCode(HttpStatus.OK.getCode());
        loadReturnData.setMsg("登錄成功");
        loadReturnData.setToken(JwtUtil.createJWT(userDB)); 
        return loadReturnData;
    }

3.7 Token 校驗測試

  • 登錄測試
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 需要校驗 token

在這裏插入圖片描述
在這裏插入圖片描述

至此,springboot 整合 JWT 基本完成。

發佈了32 篇原創文章 · 獲贊 6 · 訪問量 2428
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章