1. 程式人生 > >史上最簡單的JWT教程,看不懂來捶我!

史上最簡單的JWT教程,看不懂來捶我!

史上最簡單的Elasticsearch教程已經開始更新!

(幫到到您請點點關注,文章持續更新中!)

(個人Git主頁:https://github.com/Mydreamandreality)

JWT鑑權機制:我放狠話出去,看不懂儘管來捶我(反正你也錘不到,哈哈,
如果有不懂的可以留言一起交流學習,關注一哈,之後會更新Elasticsearch,cloud Java
的乾貨,一起學習,部落格是在印象筆記的記錄,copy出來樣式有一些亂,見諒

1. JWT是什麼?

	1. JWT是一種基於JSON的令牌安全驗證(在某些特定的場合可以替代Session或者Cookie)
2. JWT是什麼樣子的?

	1. JWT是由三個部分組成的,分別是:

		1. 頭部資訊(header)

			1. 作用:指定該JWT使用的簽名
		2. 訊息體(playload)

			1. 作用:JWT的請求資料
		3. 簽名( signatrue)

			1. 簽名是三個部分組合成的

				1. Base64編碼後的頭部資訊,訊息體,且加密後拼接而成,[,]分割
				2. 私有的Key計算,並且Base64編碼
3. JWT該怎麼用?

	1. JWT經常被用來保護伺服器的資源,客戶端一般通過HTTP/header的Authorzation把JWT傳送給服務端
	2. 服務端使用自己儲存的Key進行計算,驗證簽名JWT是否合法

生產專案中JWT的使用
程式碼:

/**
* Created by 張燿峰
* JWT常量
* @author 孤
* @date 2019/1/2
* @Varsion 1.0
*/
public interface JwtConstants {

    String AUTH_HEADER = "Authorization";

    String SECRET = "defaultSecret";

    String AUTH_PATH = "/attackApi/auth";

    Long EXPIRATION = 604800L;
}

//AUTH_HEADER    是HTTPHeader請求的引數
//SECRET         是具體的加密演算法
//AUTH_PATH      是提供給客戶端獲取JWT引數的介面/需要提供正確的使用者名稱以及密碼
//EXPIRATION     是計算JWT過期時間需要用到的          
/**
* Created by 張燿峰
* RestApi鑑權攔截器
* @author 孤
* @date 2019/1/2
* @Varsion 1.0
*/
public class RestApiInteceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        //此處應是靜態資源請求
        if (handler instanceof org.springframework.web.servlet.resource.ResourceHttpRequestHandler) {
            return true;
    }
        return check(request,response);
    }

    /**
     * toKen檢查
     * @return
     */
    private boolean check(HttpServletRequest request, HttpServletResponse response) {
        //此處是指獲取Token的介面
        if (request.getServletPath().equals(JwtConstants.AUTH_PATH)) {
            return true;
        }
        final String requestHeader = request.getHeader(JwtConstants.AUTH_HEADER);
        String authToken = null;
        if (requestHeader != null && requestHeader.startsWith("attackFind ")) {
            authToken = requestHeader.substring(11);
            try {
                //包含了驗證jwt是否正確
                boolean isExpired = AuthUtil.isTokenExpired(authToken);
                if (isExpired) {
                    RenderUtil.renderJson(response, new ErrorResponseData(BizExceptionEnum.TOKEN_EXPIRED.getCode(), BizExceptionEnum.TOKEN_EXPIRED.getMessage()));
                    return false;
                }
            } catch (JwtException e) {
                //有異常就是token解析失敗
                RenderUtil.renderJson(response, new ErrorResponseData(BizExceptionEnum.TOKEN_ERROR.getCode(), BizExceptionEnum.TOKEN_ERROR.getMessage()));
                return false;
            }
        } else {
            RenderUtil.renderJson(response, new ErrorResponseData(BizExceptionEnum.TOKEN_ERROR.getCode(), BizExceptionEnum.TOKEN_ERROR.getMessage()));
            return false;
        }
        return true;
    }
}

//preHandle:
//首先需要判斷請求是否是靜態資源
//如果是則不驗證該請求,不是則執行check方法
//check中先驗證該請求是否為獲取JWT引數
//獲取請求頭中的Authorization引數資訊
//呼叫我們介面的第三方約定好JWT之前追加attackFind ,如果沒有這個資訊那麼JWT就驗證失敗
//isTokenExpired是檢測JWT是否過期 true過期
/**
* Created by 張燿峰
* <p>Jwt工具類</p>
* jwt的claim裡一般包含以下幾種資料:
* 1. iss -- token的發行者
* 2. sub -- 該JWT所面向的使用者
* 3. aud -- 接收該JWT的一方
* 4. exp -- token的失效時間
* 5. nbf -- 在此時間段之前,不會被處理
* 6. iat -- jwt釋出時間
* 7. jti -- jwt唯一標識,防止重複使用
*
* @author 孤
* @date 2019/1/2
* @Varsion 1.0
*/
public class AuthUtil {

    /**
     * 從Token中獲取使用者名稱稱
     *
     * @param token
     * @return 使用者名稱稱
     */
    public static String getUserNameFromToken(String token) {
        return getClaimsFromToken(token).getSubject();
    }

    /**
     * 從Token中獲取JWT釋出時間
     *
     * @param token
     * @return 釋出時間
     */
    public static Date getIsseudAtDateFromToken(String token) {
        return getClaimsFromToken(token).getIssuedAt();
    }

    /**
     * 從Token中獲取JWT過期時間
     *
     * @param token
     * @return 過期時間
     */
    public static Date getExPirationDateFromToken(String token) {
        return getClaimsFromToken(token).getExpiration();
    }

    /**
     * 從Token中獲取JWT接收者
     *
     * @param token
     * @return 接收者
     */
    public static String getAyduebceFromToken(String token) {
        return getClaimsFromToken(token).getAudience();
    }

    /**
     * 從Token中獲取私有的JWT claim
     *
     * @param token
     * @param key
     * @return claim
     */
    public static String getPrivateClaimsFromToken(String token, String key) {
        return getClaimsFromToken(token).get(key).toString();
    }

    public static Claims getClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(JwtConstants.SECRET)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 檢查Token是否正確
     *
     * @param token
     */
    public static void parseToken(String token) {
        Jwts.parser().setSigningKey(JwtConstants.SECRET).parseClaimsJws(token).getBody();
    }

    /**
     * 檢查Token是否過期
     *
     * @param token
     * @return false未過期  true過期
     */
    public static boolean isTokenExpired(String token) {
        try {
            final Date expiration = getExPirationDateFromToken(token);
            return expiration.before(new Date());
        } catch (ExpiredJwtException expiredJwtException) {
            return true;
        }
    }

    /**
     * 生成Token
     *
     * @param userId
     * @return Token
     */
    public static String generateToken(String userId) {
        Map<String, Object> claims = new HashMap<>();
        return doGeneratorToken(claims, userId);
    }

    private static String doGeneratorToken(Map<String, Object> claims, String subject) {
        final Date startDate = new Date();
        final Date endDate = new Date(startDate.getTime() + JwtConstants.EXPIRATION * 1000);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(startDate)
                .setExpiration(endDate)
                .signWith(SignatureAlgorithm.HS512,JwtConstants.SECRET)
                .compact();
    }

    /**
     * 獲取混淆MD5簽名用的隨機字串
     */
    public static String getRandomKey() {
        return ToolUtil.getRandomString(6);
    }

//這個類中的程式碼都是JWT的工具


/**
* 自動渲染當前使用者資訊登入屬性 的過濾器
*/
public class AttributeSetInteceptor extends HandlerInterceptorAdapter {

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

        //沒有檢視的直接跳過過濾器
        if (modelAndView == null || modelAndView.getViewName() == null) {
            return;
        }

        //檢視結尾不是html的直接跳過
        if (!modelAndView.getViewName().endsWith("html")) {
            return;
        }

        ShiroUser user = ShiroKit.getUser();

        if (user == null) {
            throw new AuthenticationException("當前沒有登入賬號!");
        } else {
            modelAndView.addObject("menus", user.getId());
            modelAndView.addObject("name", user.getName());
        }
    }
}

//業務程式碼無需關注
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private AttackProperties attackProperties;

    /**
     * 增加swagger的支援
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if (attackProperties.getSwaggerOpen()) {
            registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    }

    /**
     * 增加對rest api鑑權的spring mvc攔截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List<String> NONE_PERMISSION_RES = CollectionUtil.newLinkedList
                ("/static/**", "/attackApi/**", "/login", "/global/sessionError", "/kaptcha","/cornemail","/init/start","/websocket/new_message","/global/error","/api/**");
        registry.addInterceptor(new RestApiInteceptor()).addPathPatterns("/attackApi/**");
        registry.addInterceptor(new AttributeSetInteceptor()).excludePathPatterns(NONE_PERMISSION_RES).addPathPatterns("/**");
    }

    /**
     * 預設錯誤頁面,返回json
     */
    @Bean("error")
    public AttackErrorView error() {
        return new AttackErrorView();
    }
    ....................其他程式碼省略
}
//由於提供給第三方服務時專案是用ShiroSession會話管理管控的
//現在要在此基礎上增加JWT鑑權
//所以過濾 attackApi/**的請求,[不通過Shiro認證,JWT攔截器會攔截attackApi/**的請求]
//此處沒有使用Shiro就不用關注
Map<String, String> hashMap = new LinkedHashMap<>();
List<String> NONE_PERMISSION_RES = CollectionUtil.newLinkedList
        ("/static/**", "/attackApi/**", "/login", "/global/sessionError", "/kaptcha","/cornemail","/init/start","/websocket/new_message","/global/error","/api/**");
for (String nonePermissionRe : NONE_PERMISSION_RES) {
    hashMap.put(nonePermissionRe, "anon");
}
hashMap.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(hashMap);
/**
* Created by 張燿峰
*
* @author 孤
* @date 2019/1/2
* @Varsion 1.0
*/
@RestController
@RequestMapping(value = "/attackApi")
public class AttackApi extends BaseController {
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/auth")
    public Object auth(@RequestParam("username") String username,
                       @RequestParam("password") String password) {

        //封裝請求賬號密碼為shiro可驗證的token
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password.toCharArray());

        //獲取資料庫中的賬號密碼,準備比對
        User user = userMapper.getByAccount(username);
        if (user == null) {
            return new ErrorResponseData(BizExceptionEnum.ACCOUNT_OR_PWD_ERROR.getCode(), BizExceptionEnum.ACCOUNT_OR_PWD_ERROR.getMessage());
        }
        String credentials = user.getPassword();
        String salt = user.getSalt();
        ByteSource credentialsSalt = new Md5Hash(salt);
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                new ShiroUser(), credentials, credentialsSalt, "");

        //校驗使用者賬號密碼
        HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
        md5CredentialsMatcher.setHashAlgorithmName(ShiroKit.hashAlgorithmName);
        md5CredentialsMatcher.setHashIterations(ShiroKit.hashIterations);
        boolean passwordTrueFlag = md5CredentialsMatcher.doCredentialsMatch(
                usernamePasswordToken, simpleAuthenticationInfo);

        if (passwordTrueFlag) {
            HashMap<String, Object> result = new HashMap<>();
            result.put("token", AuthUtil.generateToken(String.valueOf(user.getId())));
            return result;
        } else {
            return new ErrorResponseData(BizExceptionEnum.ACCOUNT_OR_PWD_ERROR.getCode(), BizExceptionEnum.ACCOUNT_OR_PWD_ERROR.getMessage());
        }
    }

    /**
     * 測試介面是否走鑑權
     */
    @GetMapping(value = "/test")
    public Object test() {
        return SUCCESS_TIP;
    }



//首先訪問attackApi/auth介面,用正確的使用者密碼獲取JWT
//請求attackApi/test 介面 什麼都不攜帶會丟擲令牌驗證失敗的異常
//在Header中攜帶Authorization:attackFind jwt引數
//驗證成功