SpringBoot & SpringSecurity 下 - 动态角色和权限验证
之前写的是 SpringSecurity 的标准用法,每个接口需要提前定义哪些角色可以访问,下面讲解如何动态增加角色以及动态配置链接可访问角色。
一、修改 WebSecurityConfig
package com.bjy.qa.util.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
/**
* SpringSecurity 配置
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别的权限认证,后续不需要,要删除
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 过滤器
@Resource
AuthenticationEntryPointImpl autoetaticaticcAutryPointImpl; // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发)
@Resource
AccessDeniedPointImpl accessDeniedPointImpl; // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发)
@Resource
CustomFilterInvocationSecurityMetadataSource appFilterInvocationSecurityMetadataSource;
@Resource
CustomAccessDecisionManager customerAccessDecisionManger;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 关闭跨站请求防护
.cors().and() // 配置 CORS支持
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不通过 session 获取 SecurityContext
// 配置自定义的权限验证类
http
.authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(appFilterInvocationSecurityMetadataSource); // 自定义请求 URL 与配置属性之间的映射类(根据请求的 URL,查找适用于该 URL 的权限配置属性,比如角色,权限等信息)
o.setAccessDecisionManager(customerAccessDecisionManger); // 自定义密名账户验证类,它负责根据 Authentication 对象、访问主体的权限信息以及资源的安全配置,决定主体是否有权限访问资源
return o;
}
});
// 配置异常处理器
http
.exceptionHandling()
.authenticationEntryPoint(autoetaticaticcAutryPointImpl) // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发)
.accessDeniedHandler(accessDeniedPointImpl); // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发)
// 添加 JWT 过滤器
http
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
主要改动如下:
1、去掉 EnableWebSecurity,不去也没问题
2、替换自定义权限和认证

二、CustomAccessDecisionManager
package com.bjy.qa.util.security;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 自定义密名账户验证类,它负责根据 Authentication 对象、访问主体的权限信息以及资源的安全配置,决定主体是否有权限访问资源
*/
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
/**
* 判断当前登录的用户是否具备当前请求URL所需要的角色信息。如果不具备,就抛出 AccessDeniedException 异常,否则不做任何事即可
* @param authentication 当前登录用户的信息
* @param object FilterInvocation对象,可以获取当前请求对象
* @param configAttributes FilterInvocationSecurityMetadataSource 中的 getAttributes() 方法的返回值,即当前请求URL所需的角色
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> auths = authentication.getAuthorities(); // 获取当前登录用户的角色信息(之前在 JwtAuthenticationTokenFilter 中存入的用户角色信息)
// 遍历当前请求 URL 所需要的角色信息(这里角色以 |admin|user| 形式放回所以只判断一次)
for (ConfigAttribute configAttribute : configAttributes) {
if ("ROLE_ANONYMOUS".equals(configAttribute.getAttribute())) {
return;
}
// 遍历当前登录用户的角色信息,判断是否具备当前请求 URL 所需要的角色信息
for (GrantedAuthority authority : auths) {
if (configAttribute.getAttribute().contains("|" + authority.getAuthority() + "|")) {
return;
}
}
}
throw new AccessDeniedException("拒绝访问 - 未授权");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
三、CustomFilterInvocationSecurityMetadataSource
package com.bjy.qa.util.security;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 自定义请求 URL 与配置属性之间的映射类(根据请求的 URL,查找适用于该 URL 的权限配置属性,比如角色,权限等信息)
*/
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static List<MenuRole> menuRoleList; // 所有权限
private List<MenuRole> getRoleMenu() {
if (menuRoleList != null) {
return menuRoleList;
}
// 临时写死 url 对应权限,后续要从数据库中查询
menuRoleList = new ArrayList<>();
MenuRole menuRole = new MenuRole();
menuRole.setUrl("/user/user/login123/*"); // url 链接
menuRole.setRole("|ADMIN|user|"); // 角色信息,以 | 包裹,这个链接哪些角色可以访问都写到一起
CustomFilterInvocationSecurityMetadataSource.menuRoleList.add(menuRole);
MenuRole menuRole1 = new MenuRole();
menuRole1.setUrl("/user/user/logout");
menuRole1.setRole("|admin|");
CustomFilterInvocationSecurityMetadataSource.menuRoleList.add(menuRole1);
return menuRoleList;
}
/**
* 判断当前请求的 URL 是否需要权限验证,如果是返回角色信息(目前是现实的黑名单,未匹配到都放行)
* @param url
* @return
*/
// TODO: 2023/7/25 后续这里放到 starter-cache 中,不用每次都匹配
private String matcher(String url) throws IllegalArgumentException {
AntPathMatcher antPathMatcher = new AntPathMatcher(); // Spring URL 匹配器,支持 Ant 风格的路径匹配表达式(支持通配符 * 和 **。`*匹配单路径段,**`匹配多路径段。)
for (MenuRole menu : getRoleMenu()) {
if (antPathMatcher.match(menu.getUrl(), url)) {
return menu.getRole(); // 匹配到了,需要验证权限
}
}
return "ROLE_ANONYMOUS"; // 没有匹配到,不需要验证权限
}
/**
* 返回当前请求 URL 所需要的角色信息
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
return SecurityConfig.createList(matcher(requestUrl));
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
四、MenuRole
package com.bjy.qa.util.security;
public class MenuRole {
String url;
String role;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
五、测试
1、login 登录正常

2、logout接口有权限可以访问

3、getinfo没在黑名单,可以访问

4、没有权限,拒绝访问

参考文档:
https://blog.csdn.net/weixin_43740982/article/details/120979904

浙公网安备 33010602011771号