基于角色的权限控制在springMVC框架中的实现


声明:本文转载自https://my.oschina.net/wnjustdoit/blog/1605700,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

前言:常规来说,我们在做权限的时候,基本就是这么几个要素:用户、角色、资源(权限点)。角色本质上是给资源分组,这样不同的group具有不同的权限来控制用户更方便一些。

一般情况下,web应用的权限控制都会设计成把请求路径(也就是url,实质是uri)作为权限点来赋予角色不同的权限,在拦截器获取用户信息后,根据用户的角色找到对应的权限点,并与当前的请求路径匹配,最终返回是否具有权限。

那么,今天我想说的是,在一般的web项目中,在spring(MVC)框架下,我们是怎么灵活使用spring框架本身完成权限校验的。

对于一个web请求来说,我们都能得到一个HttpServletRequest对象,那么这个request对象有很多信息决定了这个请求的唯一性:请求路径uri、请求方法(常用rest风格的GET/POST/PUT/DELETE...)、请求参数params、请求头header(主要包括Content-Type、Referer、User-Agent、Cookie)等,可惜传统的权限控制实现方式是比较局限的,而且严重限制了制定rest风格的url。

所以,springmvc是怎么将当前request对象和所有controller的请求进行匹配的呢?我们可以利用这个机制实现权限控制。

OK,源码分析正式开始:

part I

springmvc继承了servlet的核心处理类:

org.springframework.web.servlet.DispatcherServlet extends javax.servlet.http.HttpServlet

而方法核心处理方法

org.springframework.web.servlet.DispatcherServlet#doService

调用了

org.springframework.web.servlet.DispatcherServlet#doDispatch

继续往下,又调用了

org.springframework.web.servlet.DispatcherServlet#getHandler

OK,这个方法里面我们会看到一个接口org.springframework.web.servlet.HandlerMapping,这是处理映射的最基础的接口。

来看看它的实现:

org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

再看调用:

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
// Handler method lookup  	/** 	 * Look up a handler method for the given request. 	 */ 	@Override 	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { 		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);// 查找当前的uri 		if (logger.isDebugEnabled()) { 			logger.debug("Looking up handler method for path " + lookupPath); 		} 		this.mappingRegistry.acquireReadLock(); 		try { 			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);// 查找处理方法 			if (logger.isDebugEnabled()) { 				if (handlerMethod != null) { 					logger.debug("Returning handler method [" + handlerMethod + "]"); 				} 				else { 					logger.debug("Did not find handler method for [" + lookupPath + "]"); 				} 			} 			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); 		} 		finally { 			this.mappingRegistry.releaseReadLock(); 		} 	}

来看org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod

/** 	 * Look up the best-matching handler method for the current request. 	 * If multiple matches are found, the best match is selected. 	 * @param lookupPath mapping lookup path within the current servlet mapping 	 * @param request the current request 	 * @return the best-matching handler method, or {@code null} if no match 	 * @see #handleMatch(Object, String, HttpServletRequest) 	 * @see #handleNoMatch(Set, String, HttpServletRequest) 	 */ 	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { 		List<Match> matches = new ArrayList<Match>(); 		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);// 根据uri获取所有的请求匹配,这里是一个列表,因为有些请求可能uri相同,method、参数等不同         // 其中这个类有两个变量很重要,1、org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#mappingLookup是一个RequestMappingInfo为key,HanderMethod为value的Map;2、org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#urlLookup是一个uri为key,RequestMappingInfo为value的Map 		if (directPathMatches != null) { 			addMatchingMappings(directPathMatches, matches, request);// 这里把直接匹配的进行二次筛选,具体看下面代码分析 		} 		if (matches.isEmpty()) { 			// No choice but to go through all mappings... 			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); 		}  		if (!matches.isEmpty()) { 			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));// 给匹配到的列表根据优先级排序,以选择最佳匹配 			Collections.sort(matches, comparator); 			if (logger.isTraceEnabled()) { 				logger.trace("Found " + matches.size() + " matching mapping(s) for [" + 						lookupPath + "] : " + matches); 			} 			Match bestMatch = matches.get(0); 			if (matches.size() > 1) { 				if (CorsUtils.isPreFlightRequest(request)) { 					return PREFLIGHT_AMBIGUOUS_MATCH; 				} 				Match secondBestMatch = matches.get(1); 				if (comparator.compare(bestMatch, secondBestMatch) == 0) { 					Method m1 = bestMatch.handlerMethod.getMethod(); 					Method m2 = secondBestMatch.handlerMethod.getMethod(); 					throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + 							request.getRequestURL() + "': {" + m1 + ", " + m2 + "}"); 				} 			} 			handleMatch(bestMatch.mapping, lookupPath, request); 			return bestMatch.handlerMethod; 		} 		else { 			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); 		} 	}

继续看怎么进行二次筛选的:

/** 	 * Checks if all conditions in this request mapping info match the provided request and returns 	 * a potentially new request mapping info with conditions tailored to the current request. 	 * <p>For example the returned instance may contain the subset of URL patterns that match to 	 * the current request, sorted with best matching patterns on top. 	 * @return a new instance in case all conditions match; or {@code null} otherwise 	 */ 	@Override 	public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {         // 这里很关键了,这个方法是进一步把request对象和当前RequestMappingInfo的各个条件做比对进行匹配。所以这里匹配分为两步:第一步,匹配uri,第二步匹配其他condition。而这里陈列的conditions也是区分request对象是否唯一的所有条件。 		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request); 		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request); 		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request); 		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request); 		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);  		if (methods == null || params == null || headers == null || consumes == null || produces == null) { 			return null; 		}  		PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request); 		if (patterns == null) { 			return null; 		}  		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request); 		if (custom == null) { 			return null; 		}  		return new RequestMappingInfo(this.name, patterns, 				methods, params, headers, consumes, produces, custom.getCondition()); 	}

part II

接下来,我们看看spring怎么初始化所有的controller请求到内存的:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet (实现了接口InitializingBean,bean实例化完成时执行)

这个方法会调用

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods

然后调用

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods

继而调用

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerHandlerMethod

紧接着调用

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#register

public void register(T mapping, Object handler, Method method) {             // 这个方法就是将controller的请求和所在的类、方法一起注册到对应的变量中,放在内存供后续使用 			this.readWriteLock.writeLock().lock(); 			try { 				HandlerMethod handlerMethod = createHandlerMethod(handler, method); 				assertUniqueMethodMapping(handlerMethod, mapping);  				if (logger.isInfoEnabled()) { 					logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); 				} 				this.mappingLookup.put(mapping, handlerMethod);// 赋值为map1,结构上文有说  				List<String> directUrls = getDirectUrls(mapping); 				for (String url : directUrls) { 					this.urlLookup.add(url, mapping);// 赋值为map2,结构上文有说 				}  				String name = null; 				if (getNamingStrategy() != null) { 					name = getNamingStrategy().getName(handlerMethod, mapping); 					addMappingName(name, handlerMethod); 				}  				CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); 				if (corsConfig != null) { 					this.corsLookup.put(handlerMethod, corsConfig); 				}  				this.registry.put(mapping, new MappingRegistration<T>(mapping, handlerMethod, directUrls, name)); 			} 			finally { 				this.readWriteLock.writeLock().unlock(); 			} 		}

注:核心的RequestMappingInfo这个类就是@RequestMapping注解的映射;

有些注释在贴的代码中夹杂着。。

part III

那么分析基本告一段落,我们只需要把

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod

稍微改造一下就行了。

附上改造后的入口代码:

package com.xxx.cms.web.interceptor;  import com.google.common.collect.Maps; import com.xxx.cms.ucenter.domain.resource.PlatResource; import com.xxx.cms.ucenter.domain.user.User; import com.xxx.cms.ucenter.service.role.AccessPermissionService; import com.xxx.cms.web.access.AbstractHandlerMethodMapping; import com.xxx.cms.web.access.RequestMappingHandlerMapping; import com.xxx.cms.web.base.Constant; import com.xxx.cms.web.component.UserInfoService; import com.xxx.session.SessionException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo;  import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.List; import java.util.Map;  /**  * 用户权限校验拦截器  *  * @author caiya  * @since 1.0  */ public class UserAccessInterceptor extends HandlerInterceptorAdapter {      private static final Logger logger = LoggerFactory.getLogger(UserAccessInterceptor.class);      public static Map<String, AbstractHandlerMethodMapping<RequestMappingInfo>> MAPPING_CACHE_MAP = Maps.newConcurrentMap();      private final UserInfoService userInfoService;      private final AccessPermissionService accessPermissionService;      public UserAccessInterceptor(UserInfoService userInfoService, AccessPermissionService accessPermissionService) {         this.userInfoService = userInfoService;         this.accessPermissionService = accessPermissionService;     }      @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         // 获取用户信息         User user = userInfoService.getUserInfo(Constant.getSessionId(request));         if (user == null) {             throw new SessionException("用户会话失效!");         }          // 权限校验         if (!match(user.getRoleId(), request)) {             throw new IllegalAccessException("用户没有权限做此操作!");         }          return super.preHandle(request, response, handler);     }      private boolean match(Long currentRoleId, HttpServletRequest request) throws Exception {         // 先查询缓存,这里以role为key进行缓存         String currentAccessCacheKey = "access:roleId-" + currentRoleId;         AbstractHandlerMethodMapping<RequestMappingInfo> currentMapping = MAPPING_CACHE_MAP.get(currentAccessCacheKey);// 从本地缓存获取数据         if (currentMapping == null) {             // 查询数据库(这里会有另外的缓存)             Map<Long, List<PlatResource>> accessMap = accessPermissionService.getRoleResources();             for (Map.Entry<Long, List<PlatResource>> entry : accessMap.entrySet()) {                 AbstractHandlerMethodMapping<RequestMappingInfo> mapping = new RequestMappingHandlerMapping();                 List<PlatResource> resources = entry.getValue();                 for (PlatResource resource : resources) {                     if (StringUtils.isBlank(resource.getUrlPath()) && StringUtils.isBlank(resource.getMethod())) {                         continue;                     }                     PatternsRequestCondition patternsCondition = new PatternsRequestCondition(resource.getUrlPath());                     RequestMethodsRequestCondition methodsCondition = new RequestMethodsRequestCondition(RequestMethod.valueOf(resource.getMethod()));                     // reserve other conditions..                     RequestMappingInfo requestMappingInfo = new RequestMappingInfo(null, patternsCondition, methodsCondition, null, null, null, null, null);                     mapping.registerMapping(requestMappingInfo, this.getClass(), this.getClass().getMethods()[0]);// ignore these params                 }                 if (entry.getKey().equals(currentRoleId)) {                     currentMapping = mapping;                 }                 try {                     // TODO 设置本地缓存,注意缓存更新策略                     String accessCacheKey = "access:roleId-" + entry.getKey(); //                    MAPPING_CACHE_MAP.put(accessCacheKey, mapping);                 } catch (Exception e) {                     logger.error(e.getMessage(), e);                 }             }         }          if (currentMapping == null) {             return false;         }          AbstractHandlerMethodMapping.Match match = currentMapping.getBestMatch(request);         return match != null && match.getMapping() != null;     } } 

其中改造了三个spring的类:

import com.xxx.cms.ucenter.service.role.AccessPermissionService;
import com.xxx.cms.web.access.AbstractHandlerMethodMapping;
import com.xxx.cms.web.access.RequestMappingHandlerMapping;

我想,这里就不必贴了吧。

文章编写急促,还请谅解。另欢迎交流~

本文发表于2018年01月11日 20:32
(c)注:本文转载自https://my.oschina.net/wnjustdoit/blog/1605700,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1976 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1