springboot+springsecurity如何实现动态url细粒度权限认证

【springboot+springsecurity如何实现动态url细粒度权限认证】谨记:Url表只储存受保护的资源,不在表里的资源说明不受保护,任何人都可以访问
1、MyFilterInvocationSecurityMetadataSource 类判断该访问路径是否被保护

@Component//用于设置受保护资源的权限信息的数据源public class MyFilterInvocationSecurityMetadataSource implementsFilterInvocationSecurityMetadataSource {@Beanpublic AntPathMatcher getAntPathMatcher(){return new AntPathMatcher(); }@Autowired //获取数据库中的保存的urlUrl表只储存受保护的资源,不在表里的资源说明不受保护,任何人都可以访问private RightsMapper rightsMapper; @Autowiredprivate AntPathMatcher antPathMatcher; @Override/** @param 被调用的保护资源* @return 返回能够访问该保护资源的角色集合,如果没有,则应返回空集合。*/public Collection getAttributes(Object object)throws IllegalArgumentException {FilterInvocation fi = (FilterInvocation) object; //获取用户请求的UrlString url = fi.getRequestUrl(); //先到数据库获取受权限控制的UrlList us = rightsMapper.queryAll(); //用于储存用户请求的Url能够访问的角色Collection rs=new ArrayList(); for(Rights u:us){if (u.getUrl() != null) {//逐一判断用户请求的Url是否和数据库中受权限控制的Url有匹配的if (antPathMatcher.match(u.getUrl(), url)) {//如果有则将可以访问该Url的角色储存到Collectionrs.add(rightsMapper.queryById(u.getId())); }}}if(rs.size()>0) {return rs; }//没有匹配到,就说明此资源没有被控制,所有人都可以访问,返回null即可,返回null则不会进入之后的decide方法return null; }@Overridepublic Collection getAllConfigAttributes() {// TODO 自动生成的方法存根return null; }@Overridepublic boolean supports(Class clazz) {// TODO 自动生成的方法存根return FilterInvocation.class.isAssignableFrom(clazz); }}

rights表中的部分内容:
表结构
springboot+springsecurity如何实现动态url细粒度权限认证
文章图片

内容:
springboot+springsecurity如何实现动态url细粒度权限认证
文章图片

2、MyAccessDecisionManager 类判断该用户是否有权限访问
@Component//用于设置判断当前用户是否可以访问被保护资源的逻辑public class MyAccessDecisionManager implements AccessDecisionManager {@Override/** @param 请求该保护资源的用户对象* @param 被调用的保护资源* @param 有权限调用该资源的集合*/public void decide(Authentication authentication, Object object,Collection configAttributes)throws AccessDeniedException, InsufficientAuthenticationException {Iterator ite = configAttributes.iterator(); //遍历configAttributes,查看当前用户是否有对应的权限访问该保护资源while (ite.hasNext()) {ConfigAttribute ca = ite.next(); String needRole = ca.getAttribute(); for (GrantedAuthority ga : authentication.getAuthorities()) {if (ga.getAuthority().equals(needRole)) {// 匹配到有对应角色,则允许通过return; }}}// 该url有配置权限,但是当前登录用户没有匹配到对应权限,则禁止访问throw new AccessDeniedException("not allow"); }@Overridepublic boolean supports(ConfigAttribute attribute) {return true; }@Overridepublic boolean supports(Class clazz) {return true; }}

3、在SecurityConfig 类中配置说明
@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredMyUserDetailsService myUserDetailsService; @Autowiredprivate SendSmsSecurityConfig sendSmsSecurityConfig; @Autowiredprivate MyAccessDecisionManager myAccessDecisionManager; @Autowiredprivate MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource; //加密机制@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance(); // 不加密}//认证@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); }@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//对请求授权.antMatchers("/**").permitAll().anyRequest()//任何请求.authenticated()//登录后访问.withObjectPostProcessor(new ObjectPostProcessor() {@Overridepublic O postProcess(O fsi) {fsi.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource); fsi.setAccessDecisionManager(myAccessDecisionManager); return fsi; }}).and().csrf().disable(); }}

配置如下代码:
springboot+springsecurity如何实现动态url细粒度权限认证
文章图片

至此完成所有配置!!!
SpringSecurity解决公共接口自定义权限验证失效问题,和源码分析 背景:
自定义权限认证,一部分接口必须要有相应的角色权限,一部分接口面向所有访问者,一部分接口任何人都不能访问。但是在使用 SpringSecurity的过程中发现,框架会将没有指定角色列表的URL资源直接放行,不做拦截。
用户登录认证成功后,携带Token访问URL资源,spring security 根据Token(请求头Authorization中)来分辨不同用户。
用户权限数据源是一个Map:以 URL资源为Key,以有权访问的Key的角色列表为Value。
使用时发现当一个接口有Key,但是Value为空或null时,spring security 框架自动放行,导致了权限失效问题。
解决方法有两种:
第一种方法:
默认rejectPublicInvocations为false。
对需要控制权限的URL资源添加标志,以防止roleList为空,跳过了权限验证.
公共权限设置为null,不进行权限验证
第二种方法:
配置rejectPublicInvocations为true
此后roleList为空,或者没有找到URL资源时,都为拒绝访问
需要控制权限的URL资源,即使对应角色为空,也会进行权限验证
公共权限设置为所有角色和匿名角色,不进行权限验证
package org.springframework.security.access.intercept; /** * 对安全对象(访问请求+用户主体)拦截的抽象类源码 */public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware { // ... 其他方法省略 protected InterceptorStatusToken beforeInvocation(Object object) {Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {throw new IllegalArgumentException("Security invocation attempted for object "+ object.getClass().getName()+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "+ getSecureObjectClass()); }// 从权限数据源获取了当前 对应的 <角色列表>Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object); // 框架在此处判断URL资源对应的角色列表是否为空if (attributes == null || attributes.isEmpty()) {// rejectPublicInvocations默认为false // 可以配置为true,即角色列表为空的时候不进行放行if (rejectPublicInvocations) {throw new IllegalArgumentException("Secure object invocation "+ object+ " was denied as public invocations are not allowed via this interceptor. "+ "This indicates a configuration error because the "+ "rejectPublicInvocations property is set to 'true'"); }if (debug) {logger.debug("Public object - authentication not attempted"); }publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation}if (debug) {logger.debug("Secure object: " + object + "; Attributes: " + attributes); }// 如果当前用户权限对象为nullif (SecurityContextHolder.getContext().getAuthentication() == null) {credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"),object, attributes); }Authentication authenticated = authenticateIfRequired(); // Attempt authorization,此处调用accessDecisionManager 进行鉴权try {this.accessDecisionManager.decide(authenticated, object, attributes); }catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException)); throw accessDeniedException; }if (debug) {logger.debug("Authorization successful"); }if (publishAuthorizationSuccess) {publishEvent(new AuthorizedEvent(object, attributes, authenticated)); }// Attempt to run as a different user,这里可以另外配置或修改用户的权限对象,特殊场景使用Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes); if (runAs == null) {if (debug) {logger.debug("RunAsManager did not change Authentication object"); }// no further work post-invocationreturn new InterceptorStatusToken(SecurityContextHolder.getContext(), false,attributes, object); }else {if (debug) {logger.debug("Switching to RunAs Authentication: " + runAs); }SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocationreturn new InterceptorStatusToken(origCtx, true, attributes, object); } } // ... 其他方法略}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    推荐阅读