基于JWT与Redis的权限控制
最近处理的一个需求,让在管理平台上做一个权限控制,原本打算使用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是否抛出异常信息来判断是否具有调用接口的权限;