史上最簡單的JWT教程,看不懂來捶我!
阿新 • • 發佈:2019-01-14
史上最簡單的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引數
//驗證成功