SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容其实就是我们配置的访问规则, 读取访问策略如下:
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...
要实现动态的权限验证,首先要有对应的访问权限资源,Spring Security是通过SecurityMetadataSource来加载访问时所需要的具体权限
public interface SecurityMetadataSource extends AopInfrastructureBean {
//根据提供的受保护对象的信息(URI),获取该URI配置的所有角色
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;
//获取全部角色,如果返回了所有定义的权限资源,Spring Security会在启动时
//校验每个ConfigAttribute是否配置正确,不需要校验直接返回null
Collection<ConfigAttribute> getAllConfigAttributes();
//对特定的安全对象是否提供ConfigAttribute支持
//web项目一般使用FilterInvocation来判断,或者直接返回true
boolean supports(Class<?> var1);
}
SecurityMetadataSource是一个接口,同时还有一个接口FilterInvocationSecurityMetadataSource继承于它,但其只是一个标识接口,对应于FilterInvocation,本身并无任何内容
public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {
}
有了权限资源,知道了当前访问的url需要的具体权限,接下来就是决策当前的访问是否能通过权限验证了,AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication 是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果做出最终决策
public interface AccessDecisionManager {
// 决策主要通过其持有的 AccessDecisionVoter 来进行投票决策
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
// 以确定AccessDecisionManager是否可以处理传递的ConfigAttribute
boolean supports(ConfigAttribute var1);
// 以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。
boolean supports(Class<?> var1);
}
public interface AccessDecisionVoter<S> {
// 赞成
int ACCESS_GRANTED = 1;
// 弃权
int ACCESS_ABSTAIN = 0;
// 否决
int ACCESS_DENIED = -1;
// 用于判断对于当前ConfigAttribute访问规则是否支持
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
// 如果支持的情况下,vote方法对其进行判断投票返回对应的授权结果
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
投票返回值
Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是 AffirmativeBased、ConsensusBased和UnanimousBased
基于肯定的决策器,用户持有一个同意访问的角色就能通过
基于共识的决策器,用户持有同意的角色数量多于禁止的角色数量
基于一致的决策器,用户持有的所有角色都同意访问才能放行
@Service
public class UmsAdminServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username){
// 数据库获取用户信息
UmsAdmin admin = getAdminByUsername(username);
if (admin != null) {
// 获取用户权限
List<UmsPermission> permissionList = getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
}
}
Spring Security把用户拥有的权限值和接口上注解定义的权限值进行比对,如果包含则可以访问,反之就不可以访问;但是这样做会带来一些问题,我们需要在每个接口上都定义好访问该接口的权限值,而且只能挨个控制接口的权限,无法批量控制
/**
* 动态权限相关业务类
*/
public interface DynamicSecurityService {
/**
* 加载资源ANT通配符和资源对应MAP
*/
Map<String, ConfigAttribute> loadDataSource();
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MallSecurityConfig {
@Autowired
private UmsResourceService resourceService;
@Bean
public DynamicSecurityService dynamicSecurityService() {
return new DynamicSecurityService() {
@Override
public Map<String, ConfigAttribute> loadDataSource() {
Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
List<UmsResource> resourceList = resourceService.listAll();
for (UmsResource resource : resourceList) {
map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getName()));
}
return map;
}
};
}
}
/**
* 动态权限数据源,用于获取动态权限规则
*/
@Component
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static Map<String, ConfigAttribute> configAttributeMap = null;
@Autowired
private DynamicSecurityService dynamicSecurityService;
@PostConstruct
public void loadDataSource() {
configAttributeMap = dynamicSecurityService.loadDataSource();
}
public void clearDataSource() {
configAttributeMap.clear();
configAttributeMap = null;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (configAttributeMap == null) {
this.loadDataSource();
}
List<ConfigAttribute> configAttributes = new ArrayList<>();
//获取当前访问的路径
String url = ((FilterInvocation) o).getRequestUrl();
String path = URLUtil.getPath(url);
PathMatcher pathMatcher = new AntPathMatcher();
Iterator<String> iterator = configAttributeMap.keySet().iterator();
//获取访问该路径所需资源
while (iterator.hasNext()) {
String pattern = iterator.next();
if (pathMatcher.match(pattern, path)) {
configAttributes.add(configAttributeMap.get(pattern));
}
}
// 未设置操作请求权限,返回空集合
return configAttributes;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
流程如下
注意:菜单权限是每次都要全量查询数据库,如果数据多的话,可能会影响性能,这里改造读取缓存,但是新增修改菜单时,记得更新缓存数据
/**
* 动态权限决策管理器,用于判断用户是否有访问权限
*/
@Component
public class DynamicAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// 当接口未被配置资源时直接放行
if (CollUtil.isEmpty(configAttributes)) {
return;
}
Iterator<ConfigAttribute> 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;
}
}
/**
* JWT登录授权过滤器
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
/**
* 动态权限过滤器,用于实现基于路径的动态权限过滤
*/
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
@Autowired
private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Autowired
private DynamicAccessDecisionManager dynamicAccessDecisionManager;
@Autowired
public void setMyAccessDecisionManager() {
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;
}
}
接下来我们需要修改Spring Security的配置类SecurityConfig,当有动态权限业务类时在FilterSecurityInterceptor过滤器前添加我们的动态权限过滤器。这里在创建动态权限相关对象时,还使用了@ConditionalOnBean这个注解,当没有动态权限业务类时就不会创建动态权限相关对象,实现了有动态权限控制和没有这两种情况的兼容,FilterInvocation会进行参数校验,可以安全的拿到HttpServletRequest 和 HttpServletResponse对象
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.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);
}
}
//....
}
当前端跨域访问没有权限的接口时,会出现跨域问题,只需要在没有权限访问的处理类RestfulAccessDeniedHandler中添加允许跨域访问的响应头即可
/**
* 自定义返回结果:没有权限访问时
*/
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control","no-cache");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
}