前言
这段时间在做一个电商网站项目的服务端,基本每个请求都需要判断用户是否登录,对于管理员用户特有的功能更是要做权限的验证。
在v1.0版本,我在每个方法中都会从session中取出user来判断,费时费力。现在准备使用spring mvc的拦截器来重新实现这个逻辑,减少代码量,程序更加直观。
正文
首先看一个之前的例子,这个方法是添加一个商品分类,毫无疑问是只有管理员才能进行的操作。
@RequestMapping("/manage/category/add_category.do")
@ResponseBody
public ServerResponse addCategory(HttpServletRequest httpServletRequest, String categoryName, @RequestParam(value = "parentId", defaultValue = "0") int parentId){
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
if (StringUtils.isEmpty(loginToken)) {
return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
}
//从redis中获取user的json字符串
String userJsonStr = RedisShardedPoolUtil.get(loginToken);
//将user字符串转化成user对象
User user = JsonUtil.string2Obj(userJsonStr, User.class);
if( user==null ){
return ServerResponse.createByErrorCodeMessage(ResponseCode.NEED_LOGIN.getCode(), "用户未登录,请登录");
}
//校验一下是否是管理员
if( iUserService.checkAdminRole(user).isSuccess() ){
//是管理员
//增加处理分类的逻辑
return iCategoryService.addCategory(categoryName, parentId);
}else{
return ServerResponse.createByErrorMessage("无权限操作,需要管理员权限");
}
}
可以看到根据用户是否登录,以及用户是否是管理员,返回了不同的对象,真正实现该方法功能的就一行。
这里就引入了Spring MVC的拦截器来减少重复代码。
拦截器工作方式
springMVC拦截器的实现一般有两种方式:
第一种方式是自定义Interceptor
类来实现Spring的 HandlerInterceptor
接口;
第二种方式是继承实现了HandlerInterceptor
接口的类,比如Spring已经提供的实现了HandlerInterceptor
接口的抽象类 HandlerInterceptorAdapter
HandlerInterceptor 接口中定义了三个方法,我们就是通过这三个方法来对用户的请求进行拦截处理的。
-
preHandle(): 这个方法在业务处理器处理请求之前被调用,SpringMVC 中的Interceptor 是链式的调用的,在一个应用中或者说是在一个请求中可以同时存在多个Interceptor 。每个Interceptor 的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor 中的preHandle 方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是 Boolean 类型的,当它返回为 false 时,表示请求结束,后续的Interceptor 和 Controller 都不会再执行;当返回值为true 时就会继续调用下一个Interceptor 的preHandle 方法,如果已经是最后一个Interceptor 的时候就会是调用当前请求的Controller 方法。
-
postHandle():这个方法在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的 ModelAndView 对象进行操作。postHandle 方法被调用的方向跟preHandle 是相反的,也就是说先声明的Interceptor 的postHandle 方法反而会后执行。
-
afterCompletion():该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。
具体工作流程如下:
拦截器的使用
首先需要在dispatcher-servlet.xml文件里面规定要拦截哪些请求。
这里我要拦截的是有关管理员的操作的请求,我对管理员的请求写法是这样的:/manage/user/xxxx
,或者这样的:/manage/order/xxxx
,这就要求对/manage
开头的,以及它下面的子请求进行拦截,于是可以这么写:
<mvc:interceptors>
<!-- 定义在这里bean,所有都会拦截 -->
<mvc:interceptor>
<!--拦截类型-->
<!-- /manage/a.do /manage/* -->
<!-- /manage/b.do /manage/* -->
<!-- /manage/product/save.do /manage/** -->
<!-- /manage/product/detail.do /manage/**-->
<!--此处拦截/manage下请求及其子请求-->
<mvc:mapping path="/manage/**"/>
<!--防止login方法也被拦截器拦下-->
<!--<mvc:exclude-mapping path="/manage/user/login.do" />-->
<bean class="com.mall.controller.common.interceptor.AuthorityInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
Tips:
/manage/*
所匹配的是/manage/xxx
这种的,不能匹配二级请求,/manage/**
可以匹配其下所有的请求。- login请求不能拦截掉,否则就无法完成登录了,因为拦截器就是要防止未登录情况。这里注释掉是因为我是在Interceptor方法里做了处理的,所以这里可以不需要。这两种方法都是可行的。
接下来是自定义的AuthorityInterceptor
方法,注释写的很详细。关于富文本那段可以忽略掉,只是返回值跟其他的方法不一样,所以需要单独处理。
/**
* Created by makersy on 2019
*/
//mvc拦截器
@Slf4j
public class AuthorityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
log.info("preHandle");
//请求Controller中的方法全限定名
HandlerMethod handlerMethod = (HandlerMethod) o;
//解析HandlerMethod,获取请求的方法的非限定名,和请求的方法所在的简单类名
String methodName = handlerMethod.getMethod().getName();
String className = handlerMethod.getBean().getClass().getSimpleName();
//解析参数。key和value,注意value类型为String[]
StringBuffer requestParamBuffer = new StringBuffer();
Map paramMap = httpServletRequest.getParameterMap();
Iterator it = paramMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String mapKey = (String)entry.getKey();
String mapValue = StringUtils.EMPTY;
//request中map的value返回的是一个String[],判断是否是String[]类型并将其拼接
Object obj = entry.getValue();
if (obj instanceof String[]) {
String[] strs = (String[]) obj;
mapValue = Arrays.toString(strs);
}
requestParamBuffer.append(mapKey).append("=").append(mapValue);
}
//不拦截login请求
if (StringUtils.equals(className, "UserManageController") && StringUtils.equals(methodName, "login")) {
log.info("权限拦截器拦截到请求,className:{}, methodName:{}", className, methodName);
//如果是拦截到登录请求,不打印参数,因为参数中有密码,全部会打印到日志中,防止日志泄露
return true;
}
//日志打印
log.info("权限拦截器拦截到请求,className:{}, methodName:{}, param:{}", className, methodName, paramMap.toString());
//从cookie中获取token
User user = null;
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
//从缓存中获取token对应的user对象
if (!StringUtils.isEmpty(loginToken)) {
String userJsonStr = RedisShardedPoolUtil.get(loginToken);
user = JsonUtil.string2Obj(userJsonStr, User.class);
}
//如果user未登录或不是管理员,需要返回不同的值
if (user == null || (user.getRole().intValue() != Const.Role.ROLE_ADMIN)) {
//返回false,即不会调用controller里的方法
httpServletResponse.reset(); //重写response输出,要添加reset,否则报异常getWriter() has already been called for this response
httpServletResponse.setCharacterEncoding("UTF-8"); //要设置编码,否则乱码
httpServletResponse.setContentType("application/json;charset=UTF-8"); //要设置返回值类型。因为都是json接口
PrintWriter out = httpServletResponse.getWriter();
//细化逻辑
if (user == null) {
//要特别注意富文本上传方法,要返回特定格式,其余的直接返回一个ServerResponse即可
if (StringUtils.equals(className, "ProductManageController") && StringUtils.equals(methodName, "richTextImgUpload")) {
//如果是富文本上传
Map resultMap = Maps.newHashMap();
resultMap.put("success", false);
resultMap.put("msg", "请登录管理员");//未登录情况
out.print(JsonUtil.obj2String(resultMap));
} else {
//其他普通请求
out.print(JsonUtil.obj2String(ServerResponse.createByErrorMessage("拦截器拦截,用户未登录")));
}
}else {
if (StringUtils.equals(className, "ProductManageController") && StringUtils.equals(methodName, "richTextImgUpload")) {
Map resultMap = Maps.newHashMap();
resultMap.put("success", false);
resultMap.put("msg", "无权限操作");//登录但不是管理员
out.print(JsonUtil.obj2String(resultMap));
} else {
out.print(JsonUtil.obj2String(ServerResponse.createByErrorMessage("用户无权限操作")));
}
}
out.flush(); //关闭前要清空流
out.close(); //out流关闭
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
log.info("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
log.info("afterCompletion");
}
}
这样就完成拦截器啦,接下来就是去各个方法中去掉那些冗余的权限验证代码块了。
像开头的那段代码,优化后就成为这样:
@RequestMapping("/manage/category/add_category.do")
@ResponseBody
public ServerResponse addCategory(HttpServletRequest httpServletRequest, String categoryName, @RequestParam(value = "parentId", defaultValue = "0") int parentId){
//全部通过拦截器验证是否登录以及权限
return iCategoryService.addCategory(categoryName, parentId);
}