1. 程式人生 > >基於JWT的許可權驗證及實戰演練

基於JWT的許可權驗證及實戰演練

前言:

大部分系統,除了大部分金融類的系統需要嚴格的安全框架(如shiro),一般的系統安全性要求都不是很高,只需要簡單的許可權驗證(比如登入驗證)即可,下面將簡單介紹JWT用法及登入驗證的實現方式(註解方式)

jwt分析(原理啊什麼的廢話就不說了,只說用法)

  1. header(頭部),頭部用於描述關於該JWT的最基本的資訊,例如其型別以及簽名所用的演算法等。(所以演算法一樣,頭部也是一樣的,下面例子可以看到)
  2. poyload(負荷),負荷基本就是自己想要存放的資訊(因為資訊會暴露,不應該在載荷裡面加入任何敏感的資料)
  3. sign(簽名),簽名的作用就是為了防止惡意篡改資料
  4. 例子:eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3eHkifQ.l2CRkFWjXYWWUqe-_Qu4ANqx-jfxydSTN0vLGi9P51w以逗號分隔的,分為三部分
  5. 具體的jwt的解析可以去看 前後端分離之JWT使用者認證這篇文章我看了,寫的好不錯,就其中提到的傳統的seesion跟蹤會話的方式做了比較,就我個人理解,下面在解釋一下。

jwt登入驗證和傳統session驗證的區別

  1. 傳統的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有什麼特點呢?

  1. 後端將JWT字串作為登入成功的返回結果返回給前端。前端可以將返回的結果儲存在localStorage或
    sessionStorage上,退出登入時前端刪除儲存的JWT即可。
  2. 簡潔(Compact):可以通過URL, POST 引數或者在 HTTP header 傳送,因為資料量小,傳輸速度快
  3. 自包含(Self-contained):負載中包含了所有使用者所需要的資訊,避免了多次查詢資料庫
  4. 前端在每次請求時將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,基本上全部用的註解,如有問題,歡迎提出來。