前言
最近在工作中开发的项目,是基于一个SpringBoot单体架构的权限管理系统模板来做的,项目整合了SpringSecurity
来做登录鉴权,整合了Thymeleaf
来完成页面展示。项目的权限模块,是基于RBAC权限模型
来设计的,个人觉的还是很有学习的必要。
所以,我就自己开发了一个简单的权限管理系统,也是整合了SpringSecurity和Thymeleaf,主要是自己实现了一遍权限架构的设计、鉴权功能等等。也算是从工作中学到了东西。本文就来分享一下,权限架构的设计 以及一些功能细节。希望可以帮到大家 谢谢。
RBAC权限模型
先来介绍一下RBAC权限模型:
RBAC权限模型,全称Role-Based Access Control,是一种基于角色
的权限访问控制。
这种机制特点,是它并不是将权限直接赋予用户,而是引入了角色概念,不同的角色赋予不同的权限,然后将角色绑定到用户。也就是说,它将用户按照角色进行归类,通过用户的角色来确定用户对某项资源是否具备操作权限。
RBAC简化了用户与权限的管理,它将用户与角色关联、角色与权限关联、权限与资源关联,这种关联模式使得用户的授权管理变的简单和易于维护。
表结构
下面介绍一下表结构设计,主要就分享一下权限这块的表结构
根据上面的分析,要实现RBAC模型设计,我们需要提供三个概念:用户、角色、权限
。那么至少需要用户表、角色表、权限表 三个表,而系统中的权限,主要体现为菜单权限和功能操作权限,所以权限表就分为菜单表
和功能操作表
。然后还有用户与角色的绑定、以及角色与权限的绑定,还需要两个关联表:前者就叫角色用户关联表、后者就叫授权表。
综上,我们至少需要用户表、角色表、角色用户关联表、菜单表、功能操作表、授权表。下面看一下表结构:
用户表 t_user
角色表 t_role
角色用户关联表 t_role_user
菜单表 t_menu
功能操作表 t_operate
授权表 t_permission
上面的表结构还是比较简单的,有几个细节在这里要说明一下:
- 每个表都有一些通用的重复字段,例如
create_time、create_user
、逻辑删除字段is_delete
等。 - 菜单表、功能操作表都有一个字段
权限表达式
。这个字段其实就是自定义的一个字符串,可以为任意内容。代码中可以根据这个表达式来判断是否具备对应权限。 - 授权表中的字段可能稍微有点复杂。主要集中在后四个字段:
- obj_id就是
要授权的对象的id
,具体值要根据obj_type来判断,例如 如果obj_type是role,说明是为角色授权,obj_id就是角色id。org和user是考虑后面可能还会开发数据权限、以及针对单个用户的授权,这里先预留了类型,目前是只有角色授权。 - func_id就是
具体授权的功能的id
,具体值也要根据func_type来判断,例如 func_type为menu,那就是将某个菜单的权限赋予了角色,这里就填菜单的id;反之就是功能操作的id。
项目依赖
项目基于 SpringBoot v2.6.13
,pom文件依赖如下:
xml
<dependencies>
<!--Spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf整合springsecurity 可以在html页面中进行鉴权-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--web场景启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--devtools 热部署工具 快捷键 ctrl+f9-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<!--参数校验依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--引入aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
因为项目引入了Mybatis-plus作为持久层框架,所以上面表设计中对应的Entity、Mapper、Service等代码已提前生成好了。
功能模块
登录
简单起见,登录功能就直接使用SpringSecurity默认的表单登录流程。我们要做的就是提供一些配置相关的代码、以及自定义了一个登录页面。
SpringSecurity配置类
首先要创建一个SpringSecurity的配置类,进行一些个性化配置。有一些细节需要注意一下:
- CustomUserDetailsService :这是一个自定义的用户数据获取的Service,它实现了
UserDetailsService
与UserDetailsPasswordService
接口,并重写了里面的方法。前者定义了loadUserByUsername
方法,认证过程中会调用这个方法,根据username用户名获取用户数据;后者是一个密码升级需要用到的Service,里面定义了updatePassword
方法,当用户的密码需要升级时,会调用这个方法完成密码升级。所谓密码升级就是将明文的密码进行加密。 - CustomAuthenticationSuccessHandler /CustomAuthenticationFailureHandler :这两个是自定义的
认证成功&失败处理器
。分别实现了AuthenticationSuccessHandler
接口与AuthenticationFailureHandler
接口,重写这两个接口的内定义的方法,可以自定义认证成功或失败后的执行逻辑。比方说:可以在登录成功之后,进行登录日志记录。 - MessageSource :这是Spring提供的
国际化资源
组件,SpringSecurity认证过程中,如果抛出异常,方会调用MessageSourceAccessor
的getMessage
方法,根据自定义的code,获取提前定义好的错误信息。这里将MessageSource配置之后,就可以在类路径下,定义i18n/messages.properties
文件,将自定义的错误信息定义在里面。SpringSecurity就能获取到,并返回。 - CustomAuthenticationFilter :是一个自定义的过滤器,替换了默认的
UsernamePasswordAuthenticationFilter
,这里其实是自定义了登录时,用户名、密码以及其他信息(例如验证码)的获取逻辑。以及将上面提到的组件进行配置。 - 其他的方法都有详细的注释,可自行理解。上面提到的自定义组件的源码会贴在下面。
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private MessageSource messageSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置用户数据源
auth.userDetailsService(customUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//用自定义的Filter替代框架默认的UsernamePasswordAuthenticationFilter
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//配置拦截规则
http.authorizeRequests()
//放行登录相关请求
.antMatchers("/login/**").permitAll()
//其余请求都需要认证
.anyRequest().authenticated();
//配置表单登录
http.formLogin()
.loginPage(LoginConstants.LOGIN_URL.getValue());
//设置注销目录
http.logout().logoutUrl("/logout");
//关闭csrf防护
http.csrf().disable();
//解决iframe页面不能显示的问题
http.headers().frameOptions().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
//放行静态资源
web.ignoring().antMatchers("/static/**");
}
/**
* 注册自定义登录过滤器
*
* @return
*/
@Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
//设置登录请求路径
filter.setFilterProcessesUrl(LoginConstants.LOGIN_AUTH_URL.getValue());
//提供认证管理器
filter.setAuthenticationManager(authenticationManager());
//自定义认证成功处理器
filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
//自定义认证失败处理器
filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
//设置消息源
filter.setMessageSource(messageSource);
return filter;
}
/**
* 注册认证管理器
*
* @return
* @throws Exception
*/
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
认证成功&失败处理器
登录成功事件中,注入了一个事件发布器,自定义了一个登录成功事件,在登录成功之后进行了事件发布,这里是为了记录登录日志
。
java
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//发布登录事件
applicationEventPublisher.publishEvent(new LoginEvent(this, authentication));
//跳转到首页
response.sendRedirect("/");
}
}
登录失败的处理器中,保存了错误信息到Session中,并重定向到了登录页面。
java
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//将错误信息保存到session
request.getSession().setAttribute("error", exception.getMessage());
//跳转到登录页
response.sendRedirect("/login");
}
}
自定义登录参数过滤器
这个过滤器的主要功能就是自定义登录参数的获取逻辑,如下,以后如果引入登录验证码,我可以在这里添加验证码的判断逻辑。
这个过滤器会在上面的配置类中,替换UsernamePasswordAuthenticationFilter,这样就可以实现自定义的逻辑。
java
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//判断是否为post请求
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//是否为json格式入参//校验验证码
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
try {
// //校验验证码
// String captcha = "";
// Object obj = request.getSession().getAttribute("captchaCode");
// if (obj == null) throw new AuthenticationServiceException("验证码已过期!");
// String captchaCode = obj.toString();
// if (!captchaCode.equals(captcha) && !captchaCode.toLowerCase().equals(captcha) && !captchaCode.toUpperCase().equals(captcha)) {
// throw new AuthenticationServiceException("验证码输入错误!");
// }
// request.getSession().removeAttribute("captchaCode");
//获取用户名密码 进行认证操作
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (Exception e) {
e.printStackTrace();
}
}
return super.attemptAuthentication(request, response);
}
}
用户数据获取&密码升级Service
上面说到了,SpringSecurity需要一个获取用户数据的Service,认证过程中,用户数据就通过这个service来获取。在SpringSecurity中,这个组件被封装为了一个接口UserDetailsService
,里面定义了一个方法loadUserByUsername
,用于根据用户名获取用户信息。
SpringSecurity还存在密码升级的机制,如果框架检测到目前密码还是进行的明文存储,如果系统配置了密码升级的Service,那么系统会自动的进行密码升级,升级时修改数据库的操作就封装到了这个接口中:UserDetailsPasswordService
,里面定义了一个方法updatePassword
,实现类重写这个方法来完成密码修改。
项目中自定义了一个CustomUserDetailsService,实现了上述两个接口,重写了其中的方法,代码如下:
java
@Component
public class CustomUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
/**
* 根据username查询用户
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userService.getOne(new QueryWrapper<UserEntity>().eq("username", username));
if (user == null) throw new UsernameNotFoundException("用户名不存在");
//封装认证用户对象
AuthUser authUser = new AuthUser();
BeanUtils.copyProperties(user, authUser);
//封装权限信息
List<GrantedAuthority> authorities = permissionService.getPermissionExpressionByAuthUser(authUser.getId()).stream()
.filter(pe -> StrUtil.isNotBlank(pe))
.map(pe -> new SimpleGrantedAuthority(pe)).collect(Collectors.toList());
authUser.setAuthorities(authorities);
return authUser;
}
/**
* 密码自动升级
*/
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
Integer result = userService.updatePassword(user.getUsername(), newPassword);
if (result == 1) ((AuthUser) user).setPassword(newPassword);
return user;
}
}
需要注意的是:在loadUserByUsername方法中,获取到用户信息之后,会将用户信息封装到另一个对象AuthUser
中,这个对象将作为在SpringSecurity传递的用户信息,返回之前获取了当前用户所具备的所有权限表达式,将他们进行了封装。
在SpringSecurity中,用户信息的定义为UserDetails
接口,所以AuthUser自然要实现这个接口,并重写方法。代码如下:
java
@Data
public class AuthUser implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
/**
* ID
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 用户姓名
*/
private String name;
/**
* 是否为管理员 管理员默认跳过所有权限判断
*/
private Boolean isAdmin;
/**
* 是否禁用
*/
private Boolean isDisabled;
/**
* 用户所具备的权限表达式集合
*/
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return !isDisabled;
}
}
用户管理
用户管理,就是简单对用户表的增删改查,稍微需要注意一点的就是,用户管理中需要对是否为管理员这一项进行维护,因为系统的设计中,规定了管理员用户能够跳过所有鉴权。代码就不贴了,非常的简单。
菜单管理
菜单管理,因为菜单数据是一个树结构,所以我这里就用了一个树形组件,支持对菜单数据的增删改查。这里的菜单 可以定义一个权限表达式,来代表这个菜单的权限。
菜单管理除了基本的菜单数据的增删改查外,还支持在菜单下面,维护功能操作信息。这里所谓的功能操作,可以理解为就是菜单里面的按钮权限,当然,也可以用在其他的地方。只不过一般应用最多的就是按钮鉴权。
在引入了thymeleaf-extras-springsecurity5
这个依赖之后,就可以直接在html页面中,通过sec:
标签来进行按钮的显示控制,示例代码如下:
html
<!DOCTYPE html>
<!--需要在最上面引入对应的命名空间信息-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<!-- 中间代码省略... -->
<!--
在想要鉴权的标签上面 使用sec:authorize属性,
可以调用hasAuthority方法,传入权限表达式,就可以判断当前用户是否具备这个权限,
从而控制按钮是否展示
-->
<button sec:authorize="hasAuthority('menu::add')" class="layui-btn layui-btn-sm" onclick="openForm()">新增根菜单</button>
角色管理
角色管理本身也是一个简单的增删改查,与上面不同的是,这里又多了两个功能,权限分配
、授权用户
。
权限分配 :其实就是一个给角色授权
的操作,这里设计的是打开表单,是一个树形结构,包含所有的菜单+功能操作数据,可以点击勾选。保存之后,角色就具备了这些已勾选的菜单权限和功能操作权限,后台实际上就是在授权表t_permission
存入了一条给角色授权的数据。
授权用户 :这里其实就是维护了用户和角色的绑定关系,后台实际是存了角色用户关联表t_role_user
。
鉴权
截止到这里,就将整个权限模块的数据维护好了。下面要实现系统中的鉴权操作,这里简单的分为三类:
菜单鉴权
:也就是主页查询菜单数据时,只把用户所绑定的菜单查询出来。页面鉴权
:当用户不通过菜单,直接访问页面时,如果当前用户没有权限,需要将其拦截,并返回一个权限不足的提示页面。这里应用了自定义切面。方法鉴权
:当绕过前端,直接调用后端的接口时,也需要在具体的方法上进行鉴权。这里使用的是SpringSecurity提供的相关API。
菜单鉴权
所谓菜单鉴权,其实就是查询用户菜单的时候,只查询出用户已绑定的菜单信息,这里提供一个sql,因个人sql水平有限,肯定不是最优解。也没开始考虑优化,仅供参考。
sql
SELECT *
FROM t_menu
WHERE is_delete = 0
AND is_disabled = 0
AND EXISTS (SELECT *
FROM t_permission
WHERE is_delete = 0
AND func_id = t_menu.id
AND func_type = 'menu'
AND obj_type = 'role'
AND obj_id IN (SELECT role_id
FROM t_role_user
WHERE is_delete = 0 AND user_id = #{userId}))
ORDER BY sort;
这个sql是用于查询出所有有权限的菜单。代码中会再封装为树形结构,有需要的朋友可以参考文末给出的链接。
页面鉴权
页面鉴权,就是为了防止用户直接访问未授权的页面地址,如果不做权限控制,可能会访问到权限之外的资源。这里使用自定义切面,在页面跳转时,进行拦截鉴权。代码如下:
java
@Aspect
@Component
public class PageAuthenticationAspect {
@Autowired
private MenuService menuService;
@Before("@annotation(getMapping)")
public void before(JoinPoint joinPoint, GetMapping getMapping) {
//如果当前还没有认证 不需要鉴权(主要针对 /login)
if (!AuthUtil.isAuthenticated()) {
return;
}
AuthUser authUser = AuthUtil.getAuthUser();
//如果当前登录用户为管理员 不需要鉴权
if (authUser.getIsAdmin()) {
return;
}
//判断 当前GetMapping 是否为页面跳转请求
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
//标注了GetMapping + 返回值类型为String + 没有标注@ResponseBody注解 = 页面跳转请求
if (method.getReturnType() == String.class && method.getAnnotation(ResponseBody.class) == null &&
method.getDeclaringClass().getAnnotation(ResponseBody.class) == null &&
method.getDeclaringClass().getAnnotation(RestController.class) == null) {
//判断当前访问路径是否在菜单管理中已配置,如果已配置 取出其权限表达式进行判断
HttpServletRequest request = RequestUtil.getHttpServletRequestInstanceFromCurrentThread();
MenuEntity menu = menuService.getOne(new QueryWrapper<MenuEntity>().eq("is_disabled", "0").eq("url", request.getRequestURI()));
if (menu == null) return;
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) authUser.getAuthorities();
boolean b = authorities.stream().anyMatch(authority -> authority.getAuthority().equals(menu.getPermissionExpression()));
if (!b) throw new PageAuthenticationFailureException("您没有权限访问该页面!");
}
}
}
代码中注释很详细了,这里就不再赘述了。代码中用到了一个工具类AuthUtil,这是我自己封装的获取用户信息的工具类。
java
public class AuthUtil {
/**
* 获取认证用户
*/
public static AuthUser getAuthUser() {
return ((AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
/**
* 当前是否已认证
*
* @return
*/
public static Boolean isAuthenticated() {
return !SecurityContextHolder.getContext().getAuthentication().isAuthenticated() ||
!"anonymousUser".equals(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
}
}
还有一个自定义的异常类PageAuthenticationFailureException
,这里是配合SpringBoot的全局异常拦截机制。
java
public class PageAuthenticationFailureException extends RuntimeException{
public PageAuthenticationFailureException(String message) {
super(message);
}
}
全局异常处理类
java
@ControllerAdvice
public class GlobalExceptionHandler {
//省略其它拦截方法...
/**
* 访问页面权限不足时的处理
*
* @param exception
* @return
*/
@ExceptionHandler(PageAuthenticationFailureException.class)
public ModelAndView PageAuthenticationFailureExceptionHandler(PageAuthenticationFailureException exception) {
return toError(HttpStatus.FORBIDDEN.value(), exception.getMessage());
}
/**
* 跳转到错误页
*
* @param code
* @param message
* @return
*/
private ModelAndView toError(String code, String message) {
ModelAndView view = new ModelAndView("error");
view.addObject("code", code);
view.addObject("message", message);
view.addObject("path", RequestUtil.getHttpServletRequestInstanceFromCurrentThread().getRequestURI());
view.addObject("time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
return view;
}
}
通用错误页面
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:replace="common/common-import::header('通用错误页面')"></th:block>
<!--省略css...-->
</head>
<body>
<div class="errDiv">
<div class="errTitle"><h2>系统错误页面</h2></div>
<div class="errContent">
<table>
<tr>
<td>错误代码</td>
<td th:text="${code}"></td>
</tr>
<tr>
<td>错误描述</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>页面地址</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>发生时间</td>
<td th:text="${time}"></td>
</tr>
</table>
</div>
</div>
</body>
</html>
拦截效果:
方法鉴权
方法鉴权就是避免用户直接访问后端接口跳过鉴权的机制,主要借助于SpringSecurity提供的API,以及全局异常处理来返回错误页面。
要实现这个功能,需要先开启配置,也就是SpringSecurity对于方法鉴权的配置。
首先需要提供一个配置类,需要标注@EnableGlobalMethodSecurity注解,还需要提供一个AccessDecisionManager,这是一个接口,是SpringSecurity中的决策器。它用来决定某个操作,是否有足够的权限。
java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Bean
@Override
protected AccessDecisionManager accessDecisionManager() {
AbstractAccessDecisionManager decisionManager = (AbstractAccessDecisionManager) super.accessDecisionManager();
List<AccessDecisionVoter<?>> decisionVoters = decisionManager.getDecisionVoters();
decisionVoters.add(new CustomAccessDecisionVoter());//添加自定义投票器,对方法上的权限表达式有效
return new AffirmativeBased(decisionVoters);//自定义权限决策管理器
}
}
上面的代码中,使用了一个CustomAccessDecisionVoter
,这是一个投票器对象。投票器也是SpringSecurity中提供的一个组件,是以接口形式定义的:AccessDecisionVoter
,接口中存在一个vote方法,也就是具体的投票方法,返回值为int类型,1为通过,0为弃权,-1为拒绝。
这里的投票器进行投票操作之后,具体是否通过,要交由前面提到的决策器来决定。
自定义投票器的代码如下:
java
public class CustomAccessDecisionVoter implements AccessDecisionVoter<Object> {
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
//如果认证信息为空 拒绝访问
if (authentication == null) {
return ACCESS_DENIED;
}
Object principal = authentication.getPrincipal();
//如果为管理员角色,允许访问
if (principal instanceof AuthUser && ((AuthUser) principal).getIsAdmin()) {
return ACCESS_GRANTED;
}
//遍历用户的权限表达式列表,进行比对
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
}
如果请求被拦截了,抛出的是AccessDeniedException
异常,所以还要再全局异常处理类中再加一个方法:
java
@ExceptionHandler(AccessDeniedException.class)
public Object handleAccessDeniedException(AccessDeniedException exception) {
return toError(HttpStatus.FORBIDDEN.value(), "权限不足");
}
拦截效果:
总结
以上,就完成了一个简单的权限模块的构建。构建完毕后,支持灵活的权限控制,权限控制支持菜单控制、url控制、方法控制,最精细可以到按钮级别。因为这个项目只是个人学习权限设计做的,难免出现一些代码不规范,UI简陋等问题,请大家谅解。
如果有兴趣的朋友,可以 点击这里 下载项目,希望可以帮到你,谢谢阅读。