基於JWT的許可權驗證及實戰演練
前言:
大部分系統,除了大部分金融類的系統需要嚴格的安全框架(如shiro),一般的系統安全性要求都不是很高,只需要簡單的許可權驗證(比如登入驗證)即可,下面將簡單介紹JWT用法及登入驗證的實現方式(註解方式)
jwt分析(原理啊什麼的廢話就不說了,只說用法)
- header(頭部),頭部用於描述關於該JWT的最基本的資訊,例如其型別以及簽名所用的演算法等。(所以演算法一樣,頭部也是一樣的,下面例子可以看到)
- poyload(負荷),負荷基本就是自己想要存放的資訊(因為資訊會暴露,不應該在載荷裡面加入任何敏感的資料)
- sign(簽名),簽名的作用就是為了防止惡意篡改資料
- 例子:eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3eHkifQ.l2CRkFWjXYWWUqe-_Qu4ANqx-jfxydSTN0vLGi9P51w以逗號分隔的,分為三部分
- 具體的jwt的解析可以去看 前後端分離之JWT使用者認證這篇文章我看了,寫的好不錯,就其中提到的傳統的seesion跟蹤會話的方式做了比較,就我個人理解,下面在解釋一下。
jwt登入驗證和傳統session驗證的區別
- 傳統的session驗證存在安全問題,主要是因為session是存在cookie,具體弊端引用別人說的:
但這樣做問題就很多,如果我們的頁面出現了 XSS 漏洞,由於 cookie 可以被 JavaScript 讀取,XSS 漏洞會導致使用者 token 洩露,而作為後端識別使用者的標識,cookie 的洩露意味著使用者資訊不再安全。儘管我們通過轉義輸出內容,使用 CDN 等可以儘量避免 XSS 注入,但誰也不能保證在大型的專案中不會出現這個問題。
在設定 cookie 的時候,其實你還可以設定 httpOnly 以及 secure 項。設定 httpOnly 後 cookie 將不能被
JS 讀取,瀏覽器會自動的把它加在請求的 header 當中,設定 secure 的話,cookie 就只允許通過 HTTPS
傳輸。secure 選項可以過濾掉一些使用 HTTP 協議的 XSS 注入,但並不能完全阻止。
2.而jwt有什麼特點呢?
- 後端將JWT字串作為登入成功的返回結果返回給前端。前端可以將返回的結果儲存在localStorage或
sessionStorage上,退出登入時前端刪除儲存的JWT即可。 - 簡潔(Compact):可以通過URL, POST 引數或者在 HTTP header 傳送,因為資料量小,傳輸速度快
- 自包含(Self-contained):負載中包含了所有使用者所需要的資訊,避免了多次查詢資料庫
- 前端在每次請求時將JWT放入HTTP Header中的Authorization位。(解決XSS和XSRF問題)
cookie和localStorage又有什麼區別呢?
Cookie 和 LocalStorage 比較
以上是兩者對比,文章看了寫的不錯,具體有興趣可以看看,下面對比一下主要區別:
jwt實戰(登入校驗)
上面說的廢話好像有點多,主要是自己查閱資料後的一些總結和摘取。
引入JWT相關包
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.0.2</version>
</dependency>
生成token
String token = JWT.create().withAudience(name) .sign(Algorithm.HMAC256(passwd));//生成token
redisTemplate.opsForHash().put("tokens", name, token);//存入redis
解釋:以上操作是在使用者點選登陸的時候,根據使用者名稱已經密碼生成的token,並且存入redis,前端登入以後伺服器端會返回token,並且將其存在localStorage中
好處:存入redis的好處是redis可以設定定時刪除,這就可以實現使用者隔一段時間,token失效,從新登陸,從新生成token,進一步提高系統安全性,而且redis查詢資料比資料庫要快的多,而且會頻繁查詢,所以放在redis
圖示:
登陸驗證
現在token已經生成了,並且存入了redis,剩下的就是請求api時的登入驗證了,如果驗證失敗就返回401(沒有許可權提示)
每次請求時,再請求頭中都會攜帶token的資訊,如下圖:
接下來就是後端的事情了,後端採用的是在介面上面加上註解驗證:
註解的具體實現方法是:
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, ?> redis;
@Value("${hashkey.web}")
private String key;
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 如果不是對映到方法直接通過
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 判斷介面是否需要登入
LoginRequired methodAnnotation = method.getAnnotation(LoginRequired.class);
HashRequired methodAnnotation2 = method.getAnnotation(HashRequired.class);
CurrentUser methodAnnotation3 = method.getAnnotation(CurrentUser.class);
// 有 @LoginRequired 註解,需要認證
if (methodAnnotation != null) {
// 執行認證
String token = request.getHeader("authorization"); // 從 http 請求頭中取出 token
if (StringUtil.isEmpty(token)) {
response.sendError(401, "no header,try login !");
return false;
}
String name=null;
try {
name = JWT.decode(token).getAudience().get(0);
if (methodAnnotation3 != null) {
request.setAttribute("user", name);
}
} catch (Exception e) {
response.sendError(401, "wrong token!");
return false;
}
// 驗證token
if(!redis.opsForHash().hasKey("tokens", name)){
response.sendError(401, "no user token,try login!");
return false;
}
String rtoken = redis.opsForHash().get("tokens", name).toString();
if (!token.equals(rtoken)) {
response.sendError(401, "token invalid ,try login!");
return false;
}
return true;
}
if (methodAnnotation2 != null) {
// 執行認證
String hashkey = request.getHeader("hashkey");
if (StringUtil.isEmpty(hashkey)) {
response.sendError(401, "no key,reject !");
return false;
}
// 驗證hashkey
if (!key.equals(hashkey)) {
response.sendError(401, "wrong key,reject !");
return false;
} return true;
}
return true;
}
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
解釋:以上是一個全域性請求攔截器,每次請求都會先判斷這個,根據前端傳來的token,將token解析,在到redis根據name取得原來登入存的token對比即可實現。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
解釋:以上是LoginRequired 介面的實現
總結:專案採用的是springboot+springcloud+maven,基本上全部用的註解,如有問題,歡迎提出來。