在 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;
- }
- }