前言
RBAC 是基于角色的访问控制(Role-Based Access Control )。权限管理是在任何一个系统里都无法绕过的问题。没有权限控制管理的系统是非常不安全的。
在Spring框架体系中。有非常多的RBAC框架,如Spring Shiro、Spring Security。
而本文介绍的是使用SpringMVC自带的拦截器来实现RBAC权限控制。功能虽然不够全面,但胜在够轻量,能够实现最基本的权限拦截。
实现的关键点:
1.拦截器的使用
2.正则表达式的使用,反向生成权限URL的正则表达式。
3.建立正确的RBAC权限模型
权限数据模型
本文教程使用通用的RBAC权限模型:
用户、角色、权限、用户-角色、角色-权限。如下图所示:
关于RBAC权限模型的更多资料,推荐几个链接:
权限系统与RBAC模型概述[绝对经典]
http://csrc.nist.gov/groups/SNS/rbac/index.html
https://csrc.nist.gov/projects/role-based-access-control
拦截器的实现
这里记录几个要点:
1.拦截器继承自:HandlerInterceptorAdapter
2.拦截器需要加上@Component注解,为了被Ioc容器扫描到
3.拦截器要对不鉴权的URL进行放行
4.给出的代码并非完整代码,只记录了大概的思路。
方法伪代码:
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import lombok.extern.slf4j.Slf4j;
/**
* @author ljx
*/
@Slf4j
@Component
public class ValidInterceptor extends HandlerInterceptorAdapter {
//公开的URL前缀
private static final String[] PUBLIC_URL_PREFIXES = new String[]{
"/upload",
"/error",
"/web",
};
// 公开的URL前缀中要过滤的接口,即公开前缀中需要鉴权的URL
private static final String[] PUBLIC_URLS_EXCLUDES = new String[]{};
private static final String HEADER_AUTHORIZATION = "Authorization";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 对 Options 方法放行
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
return true;
}
String method = request.getMethod();
String uri = request.getRequestURI();
if (StringKit.isNotBlank(uri)) {
// uri的一些处理
uri = uri.replace("/tomcat", "");
}
// 不拦截公开的URL
if (isPublicUrl(uri)) {
return true;
}
// 从请求中获取访问者信息,通常为用户信息
// 可以获取Session或者token来识别信息,如果没有用户信息则可以判定为无权限,返回false
// 用户信息通常保存到一个全局的地方。可以是全局缓存(Redis或者内存缓存中)或者数据库中
// 代码略
// RBAC校验
// 由获取到的用户信息,去查询用户拥有的角色,再根据角色去获取对应的权限信息,
// 然后对URL和权限列表进行匹配。
// 这里每一次都会去数据库查询权限数据,好处是可以动态改变权限,不好的地方是会频繁查库。
// 优化方法:
// 1.拦截器第一次对某进行拦截时,将该用户的权限信息放到缓存,后面可以从缓存中直接取数据。(无法动态改变权限信息)
// 2.没想到,读者自行补充(滑稽脸)
Set<String> userRoles; // 用户的角色ID
Set<String> roleItems; // Set保证权限ID唯一
List<PermissionEntity> permissions; // 由权限ID集合获取到所有的权限具体信息(不包含菜单类型的URL)
// Permission是权限具体信息。必须包含权限的URL,对应的Http方法。
// hasPermission方法是对Restful URL进行鉴权的关键方法
if (!hasPermission(uri, method, permissions)) {
throw new RuntimeException("access.denied");
}
return super.preHandle(request, response, handler);
}
private boolean hasPermission(String url, String method, List<PermissionEntity> permissions) {
if (ArrayKit.isNullOrEmpty(permissions)) {
return false;
}
for (PermissionEntity permission : permissions) {
// getItem() 返回的是权限URL
if (StringKit.isBlank(permission.getItem())) {
continue;
}
// 将Restful风格的url占位符部分改成正则表达式,去匹配url。
// 例如/users/{uid}/role的正则表达式为:/users/[^/]*/role
// url和method都匹配成功视为有权限
String matchRegex = permission.getItem().replaceAll("\\{[^/]*\\}", "[^/]*");
if (url.matches(matchRegex)) {
// URL的请求方式为*则视为支持所有请求方式
if ("*".equals(permission.getMethod()) ||
method.toUpperCase(Locale.ROOT).equals(StringKit.toUpperCaseWithSafe(permission.getMethod()))) {
return true;
}
}
}
return false;
}
/**
* 是否公开的URL
*
* @param url 链接地址
* @return 是否公开
*/
private boolean isPublicUrl(String url) {
for (String publicUrl : PUBLIC_URL_PREFIXES) {
if (url.startsWith(publicUrl)) {
for (String publicUrlExclude : PUBLIC_URLS_EXCLUDES) {
if (url.contains(publicUrlExclude)) {
return false;
}
}
return true;
}
}
return false;
}
}
总结:
1.要有一个设计良好的RBAC权限模型。
2.对于Restful风格的URL权限一定要描述其URL和Method才能够准确鉴权(因为Restful风格的方法存在URL相同,Method不同的情况)。
3.框架的拦截器设计思想。
4.正则表达式的运用,反向生成权限URL的正则表达式。
5.URL放行的匹配可以利用Ant路径表达式来实现(待改进)
以上是工作时的经验所得。如有不正确的地方,还请各位读者指正。