基于角色的权限控制在springMVC框架中的实现
博客专区 > 菜蚜 的博客 > 博客详情
基于角色的权限控制在springMVC框架中的实现
菜蚜 发表于1周前
基于角色的权限控制在springMVC框架中的实现
  • 发表于 1周前
  • 阅读 652
  • 收藏 46
  • 点赞 0
  • 评论 6
标题:腾讯云 新注册用户域名抢购1元起>>>   
前言:常规来说,我们在做权限的时候,基本就是这么几个要素:用户、角色、资源(权限点)。角色本质上是给资源分组,这样不同的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#registerMapping 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;
我想,这里就不必贴了吧。 文章编写急促,还请谅解。另欢迎交流~ ----------补充说明---------- 由于本文直接使用springmvc框架,依赖的框架本身的很多类,所以当spring版本变化的时候,需要注意是否会影响到本文实现的内容。当然,如果有精力的话,可以把请求匹配机制从spring中抽离出来,独立成自己的权限校验框架。
共有 人打赏支持
粉丝 19
博文 54
码字总数 29198
评论 (6)
键盘敲得贼溜的猴子
就是借用了spring mvc的match流程。既学习了源码也模拟运用,活学活用嘛
菜蚜

引用来自“键盘敲得贼溜的猴子”的评论

就是借用了spring mvc的match流程。既学习了源码也模拟运用,活学活用嘛
我们为了兼容springMVC提供的rest风格的url,并且想知道spring是怎么映射一个request对象到每一个controller的方法,所以需要分析源码以及做适配。如果不用springmvc框架,或许需要借鉴这套匹配机制重写一套request映射框架了(其实尝试过,只是依赖太深不好办,也不方便序列化把mapping存进redis)。
键盘敲得贼溜的猴子

引用来自“键盘敲得贼溜的猴子”的评论

就是借用了spring mvc的match流程。既学习了源码也模拟运用,活学活用嘛

引用来自“菜蚜”的评论

我们为了兼容springMVC提供的rest风格的url,并且想知道spring是怎么映射一个request对象到每一个controller的方法,所以需要分析源码以及做适配。如果不用springmvc框架,或许需要借鉴这套匹配机制重写一套request映射框架了(其实尝试过,只是依赖太深不好办,也不方便序列化把mapping存进redis)。
突然想起个问题,如果我有 /app/module/resource/{id}的权限,没有/app/module/resource/list的权限,当让两者都是GET请求,我能正常访问后者吗?
菜蚜

引用来自“键盘敲得贼溜的猴子”的评论

就是借用了spring mvc的match流程。既学习了源码也模拟运用,活学活用嘛

引用来自“菜蚜”的评论

我们为了兼容springMVC提供的rest风格的url,并且想知道spring是怎么映射一个request对象到每一个controller的方法,所以需要分析源码以及做适配。如果不用springmvc框架,或许需要借鉴这套匹配机制重写一套request映射框架了(其实尝试过,只是依赖太深不好办,也不方便序列化把mapping存进redis)。

引用来自“键盘敲得贼溜的猴子”的评论

突然想起个问题,如果我有 /app/module/resource/{id}的权限,没有/app/module/resource/list的权限,当让两者都是GET请求,我能正常访问后者吗?
可以的,org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingPattern先通过equals完全匹配,再通过org.springframework.util.AntPathMatcher#doMatch占位符匹配。
键盘敲得贼溜的猴子

引用来自“键盘敲得贼溜的猴子”的评论

就是借用了spring mvc的match流程。既学习了源码也模拟运用,活学活用嘛

引用来自“菜蚜”的评论

我们为了兼容springMVC提供的rest风格的url,并且想知道spring是怎么映射一个request对象到每一个controller的方法,所以需要分析源码以及做适配。如果不用springmvc框架,或许需要借鉴这套匹配机制重写一套request映射框架了(其实尝试过,只是依赖太深不好办,也不方便序列化把mapping存进redis)。

引用来自“键盘敲得贼溜的猴子”的评论

突然想起个问题,如果我有 /app/module/resource/{id}的权限,没有/app/module/resource/list的权限,当让两者都是GET请求,我能正常访问后者吗?

引用来自“菜蚜”的评论

可以的,org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingPattern先通过equals完全匹配,再通过org.springframework.util.AntPathMatcher#doMatch占位符匹配。
可是这样就不符合权限验证的逻辑了,我没有后者的权限应该不能访问才对。当然实际项目中这样的权限分配是有点变态了。。。
菜蚜

引用来自“键盘敲得贼溜的猴子”的评论

就是借用了spring mvc的match流程。既学习了源码也模拟运用,活学活用嘛

引用来自“菜蚜”的评论

我们为了兼容springMVC提供的rest风格的url,并且想知道spring是怎么映射一个request对象到每一个controller的方法,所以需要分析源码以及做适配。如果不用springmvc框架,或许需要借鉴这套匹配机制重写一套request映射框架了(其实尝试过,只是依赖太深不好办,也不方便序列化把mapping存进redis)。

引用来自“键盘敲得贼溜的猴子”的评论

突然想起个问题,如果我有 /app/module/resource/{id}的权限,没有/app/module/resource/list的权限,当让两者都是GET请求,我能正常访问后者吗?

引用来自“菜蚜”的评论

可以的,org.springframework.web.servlet.mvc.condition.PatternsRequestCondition#getMatchingPattern先通过equals完全匹配,再通过org.springframework.util.AntPathMatcher#doMatch占位符匹配。

引用来自“键盘敲得贼溜的猴子”的评论

可是这样就不符合权限验证的逻辑了,我没有后者的权限应该不能访问才对。当然实际项目中这样的权限分配是有点变态了。。。
1、有 /app/module/resource/{id}的权限,没有/app/module/resource/list的权限;
2、没有 /app/module/resource/{id}的权限,有/app/module/resource/list的权限;
可以改造一下成为:先匹配equals完全/精准匹配的请求(含include的权限和exclude的权限),再匹配占位符的权限;如果真有这种需要可以这样改,但是没必要搞得太复杂了。
×
菜蚜
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: