在荔枝之前的一篇博客中,已经对Spring Security的鉴证授权的相关知识进行了梳理,弄清楚了为什么使用JWT以及用户登录后台执行的相关的鉴证授权的逻辑。在下面的文章中荔枝主要是补充梳理一下在真实的项目中如何使用Spring Security来实现接口动态权限控制的功能。
前言
一、什么是动态权限控制
二、实现动态权限控制
2.1 SecurityConfig配置中添加动态权限过滤器
2.2 创建动态权限过滤器
总结
在看这篇文章之前,如果不了解JWT鉴权认证的可以看看荔枝在脚手架系列博客中梳理的有关Spring Security登录授权的流程:
Mall脚手架总结(一)——SpringSecurity实现鉴权认证_荔枝当大佬的博客-CSDN博客
大家有需要的话也可以看看荔枝的专栏:项目学习——Mall_荔枝当大佬的博客-CSDN博客
在脚手架中我们学习了用户的鉴证授权功能的流程:定义一个SecurityConfig配置类配置好JWT、加密方式等信息的配置,还需要一个UserDetails用户信息接口的封装实现类AdminUserDetails,然后就是用户鉴权管理的接口方法及其实现类(具体的功能实现),然后controller提供相关的接口等。用户在登录接口login传入username和passward进行Security的UsernamePasswordAuthenticationToken校验。成功之后会将该token对象交给SecurityHolder来监控,再调用jwtToken中的方法来根据用户信息获取JWT token,这就完成了基本的登录鉴权。
那么接口是怎么授权的呢?
在Controller接口中我们可以使用Security中的@PreAuthorize注解来定义该接口访问者需要的权限,而对于Security已经在登录的时候就已经将UserDetail中的权限信息全部拿到了
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
@PreAuthorize("hasAuthority('brand:create')")
public CommonResult createBrand(@RequestBody PmsBrand pmsBrand) {....}
但是这样写对于一个大型项目来说无疑不是很友好,凭借着松耦合的思想,我们需要能够让接口自动的根据路径来配置它自己的权限规则,这就是动态权限控制。在mall项目中也是基于Srping Security来实现的。
首先我们需要修改一下之前的SpringConfig类,在定义之前我们需要明确的是,由于mall项目是有两个后端服务:后台管理+前台商城后端,因此这里判断只有相应的SecurityConfig配置类中有动态权限的相关的Bean才会开启动态权限。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;
@Autowired(required = false)
private DynamicSecurityFilter dynamicSecurityFilter;
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//白名单的资源路径允许访问
for (String url : ignoreUrlsConfig.getUrls()) {
registry.antMatchers(url).permitAll();
}
//允许跨域请求的OPTIONS请求
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
// 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及禁用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类,返回权限处理结果
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint)
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//有动态权限配置时添加动态权限校验过滤器
if(dynamicSecurityService!=null){
registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
}
return httpSecurity.build();
}
}
这篇文章最为重要的内容就是弄清楚怎么创建一个动态权限的过滤器!
要自定义一个过滤器我们需要实现Filter方法并在doFilter方法中完成我们的过滤规则定义,下面是涉及到的相关类和接口
isPattern(String path)
用于判断给定的路径是否是一个模式字符串,match(String pattern, String path)
用于根据当前PathMatcher的匹配策略,检查指定的路径和模式是否匹配。过滤器
接着我们自定义一个过滤器继承Security中的AbstractSecurityInterceptor类并重写Filter接口方法:因为是基于路径的权限控制,所以不可避免地使用到了HttpServletRequest对象。
/**
* 动态权限过滤器,用于实现基于路径的动态权限过滤
*/
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
@Autowired
private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Autowired
public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
super.setAccessDecisionManager(dynamicAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
//OPTIONS请求直接放行
if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
//继续执行过滤器链中的下一个过滤器或目标资源,也就是放行
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
//白名单请求直接放行
PathMatcher pathMatcher = new AntPathMatcher();
for (String path : ignoreUrlsConfig.getUrls()) {
if(pathMatcher.match(path,request.getRequestURI())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
}
/**
* 此处会调用AccessDecisionManager中的decide方法进行鉴权操作,
* 这个调用对于保护应用程序的安全性和实现细粒度的访问控制非常重要。
* 这里鉴权不通过可能会在上层代码中抛出异常
*/
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return dynamicSecurityMetadataSource;
}
}
这里需要注意的就是使用了super.beforeInvocation()方法来调用AccessDecisionManager中的decide方法来做出用户是否有权限访问的判断。
动态权限决策管理类
public class DynamicAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 当接口未被配置资源时直接放行
if (CollUtil.isEmpty(configAttributes)) {
return;
}
//创建一个资源迭代器
Iterator iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
//将访问所需资源或用户拥有资源进行比对
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority: authentication.getAuthorities()) {
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("抱歉,您没有访问权限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
这里设计只要用户的权限中有一个符合该接口的权限需求就允许访问,比如这个接口定义了一些通配符下的配置和其它的路径配置等,只要用户有相关的访问资源姐可以为其开启权限。
获取动态权限资源
该类实现了Spring Security中FilterInvocationSecurityMetadataSource 接口并重写其方法。
/**
* 动态权限数据源,用于获取动态权限规则
*
* 这个类比较简单,首先我们需要一个配置着用户名和权限路径字段的map对象
* 总的来说就是根据请求的路径,从configAttributeMap配置源对象中找到对应接口的权限对象配置信息
* 再存在一个list ConfigAttribute对象中
*/
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static Map configAttributeMap = null;
@Autowired
private DynamicSecurityService dynamicSecurityService;
/**
* 该注解是为了自动在构造函数执行完后,在初始化方法之前执行loadDataSource将数据加载进configAttribute
*/
@PostConstruct
public void loadDataSource(){
configAttributeMap = dynamicSecurityService.loadDataSource();
}
public void clearDataSource(){
configAttributeMap.clear();
configAttributeMap = null;
}
/**
* 获取权限方法
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
//查看配置信息的map是否有信息
if(configAttributeMap==null){
this.loadDataSource();
}
List configAttributes = new ArrayList<>();
//获取当前请求的url
String url = ((FilterInvocation) object).getRequestUrl();
//从url中提取路径
String path = URLUtil.getPath(url);
//创建一个路径匹配对象
PathMatcher pathMatcher = new AntPathMatcher();
//configAttributeMap的所有键(这些键是路径模式)的迭代器
Iterator iterator = configAttributeMap.keySet().iterator();
//获取到访问资源
while (iterator.hasNext()){
String patten = iterator.next();
if(pathMatcher.match(patten,path)){
configAttributes.add(configAttributeMap.get(patten));
}
}
return configAttributes;
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
注意这里的supports方法中的返回布尔值应该置为true。
那么我们再使用Spring Security开启动态权限配置就可以直接定义一个Security的配置类并声明一个DynamicSecurityService的Bean对象即可。
@Configuration
public class MallSecurityConfig {
@Autowired
private UmsAdminService adminService;
@Autowired
private UmsResourceService resourceService;
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> adminService.loadUserByUsername(username);
}
@Bean
//实现动态权限的bean
public DynamicSecurityService dynamicSecurityService() {
return new DynamicSecurityService() {
@Override
public Map loadDataSource() {
Map map = new ConcurrentHashMap<>();
List resourceList = resourceService.listAll();
for (UmsResource resource : resourceList) {
map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
}
return map;
}
};
}
}
总算彻底弄清楚了Spring Security的权限认证流程,但是对于相关的类和接口的使用可能还需要后续自己在项目中锻炼熟练度吧哈哈哈哈,希望这篇文章能帮助到有需要的小伙伴~~~
今朝已然成为过去,明日依然向往未来!我是荔枝,在技术成长之路上与您相伴~~~
如果博文对您有帮助的话,可以给荔枝一键三连嘿,您的支持和鼓励是荔枝最大的动力!
如果博文内容有误,也欢迎各位大佬在下方评论区批评指正!!!