简单配置
先说一下简单的配置,使用Security已经实现的接口直接配置,库表设计如下:
package cn.seaboot.admin.security.manager; import cn.seaboot.admin.security.bean.entity.SecurityChain; import cn.seaboot.admin.security.service.SecurityChainService; import cn.seaboot.common.core.CommonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.List; /** * Security configuration * * @author Mr.css * @date 2020-05-07 23:38 */ @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class); @Resource private SecurityChainService securityChainService; /** * HttpSecurity相关配置 * * @param http HttpSecurity */ @Override protected void configure(HttpSecurity http) throws Exception { //鉴权配置,将数据库的权限配置加到系统中去 ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests(); List<SecurityChain> chains = securityChainService.queryList(); if (CommonUtils.isNotEmpty(chains)) { for (SecurityChain chain : chains) { if (!chain.getIsDisable() && !"anon".equals(chain.getExps())) { if (chain.getMethod() == null) { registry.antMatchers(chain.getUrl()).access(chain.getExps()); } else { registry.antMatchers(HttpMethod.resolve(chain.getMethod()), chain.getUrl()).access(chain.getExps()); } logger.info("[security] matcher: {} -> {}", chain.getUrl(), chain.getExps()); } } } //以下省略http其它配置 } /** * 白名单配置,因为默认的投票决策方式,是1票通过的方式,只要满足1个条件,就算满足权限要求,因此白名单配置的所有Url,都是可以直接访问的 * * @param web - */ @Override public void configure(WebSecurity web) { WebSecurity.IgnoredRequestConfigurer ignoredRequestConfigurer = web.ignoring(); List<SecurityChain> chains = securityChainService.queryList(); if (CommonUtils.isNotEmpty(chains)) { for (SecurityChain chain : chains) { if (!chain.getIsDisable() && "anon".equals(chain.getExps())) { ignoredRequestConfigurer.antMatchers(chain.getUrl()); logger.info("[security] ignore: {}", chain.getUrl()); } } } } }
复杂配置
自定义权限匹配规则,这主要涉及到3个对象的使用。
AccessDecisionManager:投票决策,与数据库种的权限配置进行比较,判断用户是不是有操作权限;
FilterInvocationSecurityMetadataSource:元数据查找,相当于DAO,根据URL查找权限配置信息;
ObjectPostProcessor:一个代码切面,帮你将代码植入特定鉴权环节的接口。
具体配置如下:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; /** * Security configuration * * @author Mr.css * @date 2020-05-07 23:38 */ @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class); /** * HttpSecurity相关配置 * * @param http HttpSecurity */ @Override protected void configure(HttpSecurity http) throws Exception { AuthorityPostProcessor urlPostProcessor = new AuthorityPostProcessor(); urlPostProcessor.setAuthorityAccessDecisionManager(new AccessDecisionManager()); urlPostProcessor.setAuthorityMetadataSource(new FilterInvocationSecurityMetadataSource()); http.authorizeRequests() .antMatchers("/login").permitAll() .withObjectPostProcessor(urlPostProcessor) .anyRequest().authenticated(); //下面省略其它配置 } }
AccessDecisionManager
系统默认实现有下面这么几个,Security是以投票的方式实现的,根据满足条件的数量,确定是否有权限进入系统(比如默认的,只要1个满足配置,就能直接进入系统)。
如何切换默认的3种决策方式?咱也不知道,推荐查找AbstractSecurityInterceptor相关的代码。
* {@link AffirmativeBased} 只需有一个投票赞成即可通过;
* {@link ConsensusBased} 需要大多数投票赞成即可通过;
* {@link UnanimousBased} 需要所有的投票赞成才能通过。
import cn.seaboot.common.exception.ServiceException; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.vote.AffirmativeBased; import org.springframework.security.access.vote.ConsensusBased; import org.springframework.security.access.vote.UnanimousBased; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.util.Collection; /** * 投票决策 * <p> * 默认实现: * - {@link AffirmativeBased} 只需有一个投票赞成即可通过; * - {@link ConsensusBased} 需要大多数投票赞成即可通过; * - {@link UnanimousBased} 需要所有的投票赞成才能通过。 * * @author Mr.css * @date 2022-01-06 19:32 */ @Component public class AuthorityAccessDecisionManager implements AccessDecisionManager { /** * 与角色对比,查看是否满足权限 * * @param authentication 用户凭证 * @param object 当前请求路径 * @param configAttributes 权限集合 * @throws AccessDeniedException - */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { System.out.println("通过认证:" + authentication); // throw new AccessDeniedException("access denied!"); } /** * 因为我们的特殊写法,这些函数不应该被调用,直接抛出异常,方便我们跟踪意外调用的场景 * * @param attribute - * @return - */ @Override public boolean supports(ConfigAttribute attribute) { throw new ServiceException("UrlAccessDecisionManager's supports(ConfigAttribute attribute) method can not be used"); } /** * 因为我们的特殊写法,这些函数不应该被调用,直接抛出异常,方便我们跟踪意外调用的场景 * * @param clazz - * @return - */ @Override public boolean supports(Class<?> clazz) { throw new ServiceException("UrlAccessDecisionManager's supports(Class<?> clazz) method can not be used"); } }
FilterInvocationSecurityMetadataSource
import cn.seaboot.common.exception.ServiceException; 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.Collection; /** * 权限数据源管理 * * @author Mr.css * @date 2022-01-06 19:29 */ @Component public class AuthorityMetadataSource implements FilterInvocationSecurityMetadataSource { /** * 路径匹配工具 */ private AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * 根据Url查找匹配的路径列表 * * @param object url * @return 所需的权限集合 * @throws IllegalArgumentException - */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { System.out.println("获取URL所需的权限列表"); String requestUrl = ((FilterInvocation) object).getRequestUrl(); return SecurityConfig.createList("ROLE_LOGIN"); } /** * 因为我们的特殊写法,这些函数不应该被调用,直接抛出异常,方便我们跟踪意外调用的场景 * * @return - */ @Override public Collection<ConfigAttribute> getAllConfigAttributes() { throw new ServiceException("SecurityMetadataSource's getAllConfigAttributes() method can not be used"); } /** * 因为我们的特殊写法,这些函数不应该被调用,直接抛出异常,方便我们跟踪意外调用的场景 * * @param clazz - * @return - */ @Override public boolean supports(Class<?> clazz) { throw new ServiceException("SecurityMetadataSource's supports(Class<?> clazz) method can not be used"); } }