前言:常规来说,我们在做权限的时候,基本就是这么几个要素:用户、角色、资源(权限点)。角色本质上是给资源分组,这样不同的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中抽离出来,独立成自己的权限校验框架。
来源:oschina
链接:https://my.oschina.net/u/576855/blog/1605700