基于拦截器和注解实现页面的访问权限控制
在 web 系统中,经常需要对每个页面的访问进行权限控制。譬如,要进入 xx 公司的开放 平台, isv 需要注册成为开发者,开发者的状态有审核中、有效、冻结、拒绝、删除等状态,然后根据不同的状态,开发者可以访问不同的页面。只有有效或冻结状态可以访问只读功能的页面(即该页面的访问不会造成后台数据的变化),只有有效状态可以访问具有写功能的页面。
如何实现该访问控制的需求呢?最直观的做法就是:在每个页面对应的 controller 里,都去调用查询开发者的服务,然后判断开发者的状态。
@Controller
@RequestMapping("/appDetail.htm")
public class AppDetailController {
@RequestMapping(method = RequestMethod.GET)
public String doGet(ModelMap modelMap, HttpServletRequest httpServletRequest) {
//1. 开发者有效性判断
Developer developer = developerManageServiceClient
.getByCardNo(cardNo);
if (null == developer){
return ERROR_VM;
}
if (DeveloperStatus.VALID != developer.getStatus() && DeveloperStatus.FREEZE != developer.getStatus()) {
return ERROR_VM;
}
//2. 业务操作,此处省略
}
}
@Controller
@RequestMapping("/appBaseInfoEdit.htm")
public class AppBaseInfoEditController {
@RequestMapping(method = RequestMethod.POST)
public String modify(ModelMap modelMap, HttpServletRequest httpServletRequest, AppBaseInfoForm appBaseInfoForm) {
//1. 开发者有效性判断
Developer developer = developerManageServiceClient
.getByCardNo(cardNo);
if (null == developer){
return ERROR_VM;
}
if (DeveloperStatus.VALID != developer.getStatus()) {
return ERROR_VM;
}
//2. 业务操作,此处省略
}
}
appDetail.htm 对应的页面需要开发者的状态为有效或者冻结, appBaseInfoEdit.htm 对应的页面需要开发者的状态为有效。
采用这种方式有以下缺点:
- 多个 controller 里实现同样的代码,造成代码冗余;
- 对于每个 controller 的主体功能来说,对开发者状态的检查是一个横切关注点,将这种关注点掺和在主功能里,会使得主体业务逻辑不清晰。
所以,可以基于AOP 将这种横切关注点以拦截器的方式实现,但存在的一个问题是,拦截器如何知道某个页面的访问对开发者状态的要求呢?可以基于注解实现。譬如:
@Controller
@RequestMapping("/appDetail.htm")
@Permission(permissionTypes = { PermissionEnum.DEVELOPER_VALID })
public class AppDetailController {
@RequestMapping(method = RequestMethod.GET)
public String doGet(ModelMap modelMap, HttpServletRequest httpServletRequest) {
//1. 业务操作,此处省略
}
}
@Controller
@RequestMapping("/appBaseInfoEdit.htm")
@Permission(permissionTypes = { PermissionEnum.DEVELOPER_VALID, PermissionEnum.DEVELOPER_FREEZE })
public class AppBaseInfoEditController {
@RequestMapping(method = RequestMethod.POST)
public String modify(ModelMap modelMap, HttpServletRequest httpServletRequest, AppBaseInfoForm appBaseInfoForm) {
//1. 业务操作,此处省略
}
}
@Permission(permissionTypes = { PermissionEnum.DEVELOPER_VALID }) ,表示开发者的状态必须是有效; @Permission(permissionTypes = { PermissionEnum.DEVELOPER_VALID, PermissionEnum.DEVELOPER_FREEZE }) ,表示开发者的状态必须是有效或者冻结。这样,每个 controller 的主体业务逻辑就清晰了。
下面分析一下注解和拦截器是如何实现的:
注解实现:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
/** 检查项枚举 */
PermissionEnum[] permissionTypes() default {};
/** 检查项关系 */
RelationEnum relation() default RelationEnum.OR;
}
RelationEnum 该枚举表示各检查项( permissionTypes )之间的关系, OR 表示至少需要满足其中一个检查项, AND 表示需要满足所有检查项
/**
* 权限检查拦截器
*
* @author xianwu.zhang
* @version $Id: PermissionCheckInterceptor.java, v 0.1 2012-10-25 下午07:48:11 xianwu.zhang Exp $
*/
public class PermissionCheckInterceptor extends HandlerInterceptorAdapter {
/** 权限检查服务 */
private PermissionCheckProcessor permissionCheckProcessor;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Class<?> clazz = handler.getClass();
if (clazz.isAnnotationPresent(Permission.class)) {
Permission permission = (Permission) clazz.getAnnotation(Permission.class);
return permissionCheckProcessor.process(permission, request,response);
}
return true;
}
}
* 权限检查器
* @author xianwu.zhang
* @version $Id: PermissionCheckProcessor.java, v 0.1 2012-11-5 下午05:13:17 xianwu.zhang Exp $
*/
public class PermissionCheckProcessor {
public boolean process(Permission permission, HttpServletRequest request, HttpServletResponse response) {
PermissionEnum[] permissionTypes = permission.permissionTypes();
try {
String cardNo = OperationContextHolder.getPrincipal().getUserId();
HttpSession session = request.getSession(false);
If(null != session){
//查询开发者
Developer developer = developerManageServiceClient .getByCardNo(cardNo);
if (null != developer && checkPermission(permissionTypes, permission.relation(),developer .getStatus())) {
return true;
}
}
sendRedirect(response, ISV_APPLY_URL);
return false;
} catch (Exception e) {
sendRedirect(response, ISV_APPLY_URL);
return false;
}
}
//省略
}
private void sendRedirect(HttpServletResponse response, String redirectURI) {
URIBroker uriBroker = uriBrokerManager.getUriBroker(redirectURI);
String url = uriBroker.render();
try {
response.sendRedirect(url);
} catch (IOException e) {
logger.error("转向页面:" + url + "跳转出错:", e);
}
}
}
Xml 配置如下:
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"> <property name="interceptors"> <list> <ref bean="permissionCheckInterceptor" /> </list> </property> </bean> <bean id="permissionCheckInterceptor" class="com.xxx.xxx.web.home.interceptor.PermissionCheckInterceptor" /> <bean id="permissionCheckProcessor" class="com.xxx.xxx.web.home.interceptor.PermissionCheckProcessor" />
还有一个小细节可以优化一下,现在是每访问一个页面,都会经过这个拦截器,拦截器里面都有一次 webservice 调用以查询开发者信息(开发者 cardNo 和开发者状态)。但真实场景中, 99.99% 的情况是,用户在同一个 session 下完成所有的业务,即访问的所有页面都具有同一个 sessison ,因此可以在开发者第一次访问页面时,通过 webservice 调用查询到开发者信息,如果权限校验通过,则将开发者 cardNo 和开发者状态放在 session 里,那么在 session 未失效前, 开发者再次访问其他页面时,可以在拦截器里先判断目前登陆的用户卡号是否与 session 里存储的 cardNo 相同,如果相同,则就不需要再调用 webserivce 了,可以直接取 session 里存储的开发者状态来进行权限校验,这样就减少了大量的不必要的 webservice 调用。
/**
* 权限检查器
* @author xianwu.zhang
* @version $Id: PermissionCheckProcessor.java, v 0.1 2012-11-5 下午05:13:17 xianwu.zhang Exp $
*/
public class PermissionCheckProcessor {
public boolean process(Permission permission, HttpServletRequest request,
HttpServletResponse response) {
PermissionEnum[] permissionTypes = permission.permissionTypes();
try {
String cardNo = OperationContextHolder.getPrincipal().getUserId();
HttpSession session = request.getSession(false);
if (null != session) {
String developerCardNo = (String) session.getAttribute("developerCardNo");
if (StringUtil.isNotBlank(cardNo) && StringUtil.equals(cardNo, developerCardNo)) {
String status = (String) session.getAttribute("status");
if (checkPermission(permissionTypes, permission.relation(), status)) {
return true;
}
} else {
Developer developer = developerManageServiceClient .getByCardNo(cardNo);
if (null != developer
&& checkPermission(permissionTypes, permission.relation(), developer
.getStatus())) {
session.setAttribute("status", developer.getStatus());
session.setAttribute("developerCardNo ", cardNo);
return true;
}
}
}
sendRedirect(response, ISV_APPLY_URL);
return false;
} catch (Exception e) {
sendRedirect(response, ISV_APPLY_URL);
return false;
}
}
本文为原创,转载请注明出处