最近處理的一個需求,讓在管理平台上做一個權限控制,原本打算使用shiro完成,基於項目架構最后選擇使用攔截器 配合jwt以及redis完成;
JWT:
jwt呢,這里簡單說一下,項目里主要用到的就是token傳遞驗證身份,這里的話,不多介紹jwt使用方法,列幾個網址可以了解一下;
https://www.cnblogs.com/cjsblog/p/9277677.html
https://www.cnblogs.com/lyzg/p/6028341.html
Demo目錄:(沒法傳圖片,手寫一下)
controller -> LoginController filter -> FilterConfiguration,LoginFilter util ->jwt,redis jwt->JwtUtil redis->redisUtil,redisConfig util ->CommonUtil,Constants,StringUtil
LoginController (登錄接口)
@RequestMapping(value = "/fly/login")
public Map<String,Object> login(@RequestBody Map<String,Object> params) { //根據params判斷用戶是否存在(可以利用第三方) //代碼省略 String userName = params.get("userName").toString(); //定義返回map Map<String,Object> returnMap = new HashMap<>(); //定義請求路徑(這一塊會有很多邏輯操作,這里簡化成簡單的從數據庫查詢結果,具體使用自行封裝) List<String> urls = new ArrayList<>(); urls = service.gainUrlFromDB(userName); //(這里做法是將數據庫里對應到用戶的權限全部查出來) //使用JWT生成token 放入返回Map內(這一塊是驗證身份信息以及token過期時間用到的) Map<String, Object> map = Maps.newHashMap(); map.put("username", userName); returnMap.put("token", JwtUtil.createToken(map)); //將用戶數據,權限放入redis(過期時間2小時) Map<String,Object> userRedisMsg = new HashMap<>(); userRedisMsg.put("url",urls); userRedisMsg.put("timeout",System.currentTimeMillis() + 2*60*60*1000); redisUtil.set(userName, userRedisMsg,2*60*60*1000L, TimeUnit.MILLISECONDS);
//保存用戶名信息
request.getSession().setAttribute("userName", userName);
return returnMap;
}
JwtUtil
public class JwtUtil { private static final byte[] SECRET = Constants.TOKEN_SECRET.getBytes(); private static final JWSHeader HEADER = new JWSHeader( JWSAlgorithm.HS256, JOSEObjectType.JWT, null, null, null, null, null, null, null, null, null, null, null); /** * 生成token,該方法只在用戶登錄成功后調用 * @param payload Map集合,可以存儲用戶id,token生成時間,token過期時間等自定義字段 * @return token字符串,若失敗則返回null */ public static String createToken(Map<String, Object> payload) { String tokenString = null; // 創建一個JWS Object(第二部分) JWSObject jwsObject = new JWSObject(HEADER, new Payload(new JSONObject(payload))); try { // 將jwsObject進行HMAC簽名,相當於加密(第三部分) jwsObject.sign(new MACSigner(SECRET)); tokenString = jwsObject.serialize(); } catch (JOSEException e) { log.error("簽名失敗: {}", e.getMessage()); e.printStackTrace(); } return tokenString; } /** * 校驗token是否合法,返回Map集合,集合中主要包含 * code狀態碼 * data鑒權成功后從token中提取的數據 * 該方法在過濾器中調用,每次請求API時都校驗 * @param token token * @return Map<String, Object> */ public static Map<String, Object> checkToken(String token,String url) { Map<String, Object> resultMap = Maps.newHashMap(); try { JWSObject jwsObject = JWSObject.parse(token); // palload就是JWT構成的第二部分不過這里自定義的是私有聲明(標准中注冊的聲明, 公共的聲明) Payload payload = jwsObject.getPayload(); JWSVerifier verifier = new MACVerifier(SECRET);
//token校驗 if(jwsObject.verify(verifier)) { JSONObject jsonObject = payload.toJSONObject(); // token檢驗成功(此時沒有檢驗是否過期) resultMap.put("username", jsonObject.get("username")); RedisUtil redisUtil = (RedisUtil) SpringContextUtil.getBean("redisUtil"); long time = System.currentTimeMillis(); HashMap<String, Object> userMap = (HashMap)redisUtil.get(resultMap.get("username")); if(time > (long)userMap.get("timeout")){ resultMap.put("msg", "token過期"); }else if(!((Set)userMap.get("url")).contains(url)) { resultMap.put("msg", "用戶權限不足"); }else { //這里就是權限通過,需要重置redis過期時間 resultMap.put("msg", "具有權限"); Map<String,Object> mp = new HashMap<>(); mp.put("url", userMap.get("url")); mp.put("timeout", System.currentTimeMillis() + 2*60*60*1000); redisUtil.set( resultMap.get("username"), mp, 2*60*60*1000, TimeUnit.MILLISECONDS); } resultMap.put("data", jsonObject); } else { // 檢驗失敗 resultMap.put("msg","驗證失敗"); } } catch (Exception e) { e.printStackTrace(); // token格式不合法導致的異常 resultMap.clear(); resultMap.put("msg", "token格式不合法"); } return resultMap; } }
FilterConfiguration(攔截器)
@Configuration public class FilterConfiguration { @Bean public FilterRegistrationBean loginFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setName("loginFilter"); registration.setFilter(new LoginFilter()); registration.addUrlPatterns("/study/*"); registration.setOrder(Integer.MAX_VALUE); return registration; } }
LoginFilter(自定義攔截器) (代碼里的很多文字內容 例如msg字段對應的應該做成一個常量類來封裝,此處寫法只為易懂)
public class LoginFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String path = request.getRequestURI(); String reqUrl = path.substring(path.indexOf("/", 0)); //過濾部分請求 if (!StringUtil.matches(reqUrl, "/study/fly/login")) { String token = request.getHeader("Authorization"); Map<String, Object> resultMap = JwtUtil.validToken(token, path); String msg = CommonUtil.toString(resultMap.get("msg")); switch (msg) { case "具有權限": // 取出payload中數據,放到request作用域中 request.setAttribute("jwt", resultMap.get("data")); break; case "用戶權限不足": request.setAttribute("msg", "您的權限不足"); response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "權限不足"); return; case "token格式不合法": case "token過期": request.setAttribute("msg", "您的token不合法或者過期了,請重新登陸"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "token不合法或者過期了"); return; default: break; } } } catch (Exception e) { log.error(e.getMessage()); e.printStackTrace(); } filterChain.doFilter(request, response); } }
redis的代碼就不貼出來了,寫demo只簡單封裝了下,防止誤導;可以自行度娘一下,需要注意序列化;
數據庫表,需要一個 菜單權限表(table_menu)角色信息表(table_role) 用戶角色關聯表(table_user_role) 角色權限關聯表(table_role_menu)
當權限細致到接口級別(按鈕級)菜單權限表需要將菜單 Id 與 請求url關聯起來,也就是說,table_menu表中需要一個請求url的字段
這里對這部分代碼解釋一下:
首先呢,在用戶登陸的時候,進行一個用戶登錄校驗,這一塊可以自己寫一套驗證,也可以借助第三方完成;然后,根據用戶id去數據庫查出當前用戶所具有的角色權限(超級管理員,普通用戶),然后再根據角色權限去 菜單權限表取出對應的url,將這些url以及redis過期時間一並打包放到redis里,每次用戶發送接口請求,自定義攔截器會攔截請求並 依次做
token 校驗、token過期校驗、權限校驗、redis過期時間更新操作;當完成校驗,攔截器就會根據response是否拋出異常信息來判斷是否具有調用接口的權限;

