JWT在專案中的簡單應用

莫非技術棧發表於2020-10-08

JWT在專案中的簡單應用

JWT介紹

JWT(JSON WEB TOKEN):JSON網路令牌,JWT是一個輕便的安全跨平臺傳輸格式,定義了一個緊湊的自包含的方式在不同實體之間安全傳輸資訊(JSON格式)。它是在Web環境下兩個實體之間傳輸資料的一項標準。實際上傳輸的就是一個字串。廣義上講JWT是一個標準的名稱;狹義上JWT指的就是用來傳遞的那個token字串。

JWT的專案應用

引入依賴:

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

定義兩個自定義註解,LoginToken表示登陸,可以跳過token驗證。CheckToken需要做token驗證。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginToken {
    boolean required() default true;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckToken {
    boolean required() default true;
}

定義生成token的JWT工具類,包含了token的生成、校驗、重新整理等。

/**
 * @ClassName: JwtUtil
 */
public class JwtUtil {

    //有效時間,如果有效時間小於20分鐘,重新整理token
    public final static Integer checkDate = 20*60*1000;
    public final static Integer validDate = 30*60*1000;
    public final static String TOKEN = "token";

    /**
     * 使用者登入成功後生成Jwt
     * 使用Hs256演算法  私匙使用使用者密碼
     *
     * @param ttlMillis jwt過期時間
     * @param user      登入成功的user物件
     * @return
     */
    public static String createJWT(long ttlMillis, User 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<String, Object>();
        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, key);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //設定過期時間
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * Token的解密
     * @param token 加密後的token
     * @param user  使用者的物件
     * @return
     */
    public static Claims parseJWT(String token, User user) {
        //簽名祕鑰,和生成的簽名的祕鑰一模一樣
        String key = user.getPassword();

        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //設定簽名的祕鑰
                .setSigningKey(key)
                //設定需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

    /**
     * 校驗token
     * 在這裡可以使用官方的校驗,我這裡校驗的是token中攜帶的密碼於資料庫一致的話就校驗通過
     * @param token
     * @param user
     * @return
     */
    public static String isVerify(String token, User user) {
        try {
            //簽名祕鑰,和生成的簽名的祕鑰一模一樣
            String key = user.getPassword();

            //得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    //設定簽名的祕鑰
                    .setSigningKey(key)
                    //設定需要解析的jwt
                    .parseClaimsJws(token).getBody();
            if (claims == null) {
                return null;
            }
            if (claims.get("password").equals(user.getPassword())) {
                return refreshJwt(token,claims,user);
            }
            return null;
        } catch (ExpiredJwtException e) {
            e.printStackTrace();
        } catch (UnsupportedJwtException e) {
            e.printStackTrace();
        } catch (MalformedJwtException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
        return  null;
    }
    /***
     * 重新整理token,區分不同功能的配置
     * @param claims
     * @return
     */
    private static String refreshJwt(String jwt, Claims claims,User user) {
        Date exp = claims.getExpiration();
        // 當 過期時間-當前時間(分)<配置時間 時重新整理
        if ( exp.getTime()-System.currentTimeMillis() <= checkDate) {
            String newjwt = createJWT(validDate, user);
            if (newjwt != null) {
                return newjwt;
            }
        }
        return jwt;
    }
}

定義攔截器:

/**
 * @ClassName: AuthenticationInterceptor
 * @Description: 攔截器
 */
public class AuthenticationInterceptor  implements HandlerInterceptor {

    @Autowired
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {

        // 從 http 請求頭中取出 token
        String token = httpServletRequest.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;
            }
        }

        //檢查有沒有需要使用者許可權的註解
        if (method.isAnnotationPresent(CheckToken.class)) {
            CheckToken checkToken = method.getAnnotation(CheckToken.class);
            if (checkToken.required()) {
                // 執行認證
                if (token == null) {
                    throw new RuntimeException("無token,請重新登入");
                }
                // 獲取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getClaim("id").asString();
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("訪問異常!");
                }
                User user = userService.findUserById(userId);
                if (user == null) {
                    throw new RuntimeException("使用者不存在,請重新登入");
                }
                String jwt = JwtUtil.isVerify(token, user);
                if (StringUtils.isEmpty(jwt)) {
                    throw new RuntimeException("非法訪問!");
                }
                httpServletResponse.setHeader(JwtUtil.TOKEN,jwt);
                return true;
            }
        }
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

攔截器的配置:

@Configuration
public class InterceptorConfig  implements WebMvcConfigurer {

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

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

測試方法

@RestController
@RequestMapping("/api")
public class UserController {
    @Autowired
    private UserService userService;

    //登入
    @PostMapping("/login")
    @LoginToken
    public Object login(@RequestBody @Valid User user) {

        JSONObject jsonObject = new JSONObject();
        User userForBase = userService.findByUsername(user);
        if (userForBase == null) {
            jsonObject.put("message", "登入失敗,使用者不存在");
            return jsonObject;
        } else {
            if (!userForBase.getPassword().equals(user.getPassword())) {
                jsonObject.put("message", "登入失敗,密碼錯誤");
                return jsonObject;
            } else {
                String token = JwtUtil.createJWT(JwtUtil.validDate, userForBase);
                jsonObject.put("token", token);
                jsonObject.put("user", userForBase);
                return jsonObject;
            }
        }
    }

    //檢視個人資訊
    @CheckToken
    @GetMapping("/getMessage")
    public String getMessage() {
        return "你已通過驗證";
    }
}

測試結果
登陸:
在這裡插入圖片描述

校驗:
在這裡插入圖片描述

關注公眾號免費領取學習資料~

在這裡插入圖片描述

相關文章