其他分享
首页 > 其他分享> > 理解Spring Security和实现动态授权

理解Spring Security和实现动态授权

作者:互联网

一、Spring Security架构

SpringSecurity 是基于 Spring AOP 和 Servlet 过滤器的安全框架,提供全面的安全性解决方案。

Spring Security核心功能包括用户认证(Authentication)、用户授权(Authorization)和攻击防护3个部分:

  • 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限
  • 攻击防护即防止伪造身份

Spring security大量使用了责任链和委托的代码设计风格,过滤器负责对请求进行安全校验和设置,某个过滤器涉及认证或授权时,认证/授权具体实现委派给认证管理器和授权管理器,过滤器不负责具体实现

image-20220825110620010

SpringSecurity过滤器链采用的是责任链的设计模式,它有一条很长的过滤器链:

1.1 FilterChainProxy

Spring Security Filter并不是直接嵌入到 Web Filter中的,而是通过 FilterChainProxy来统一管理 Spring Security Filter,FilterChainProxy本身则通过Spring提供的DelegatingFilterProxy代理过滤器嵌入到Servlet Filter 之中

1.1.1 DelegatingFilterProxy

问题:在Spring MVC应用中,需要先启动Servlet容器再启动Spring容器,servlet过滤器位于spring容器前无法被spring容器管理(例如,无法在实现Filter接口的类中使用@Value和@Autowire注解)

Spring 提供了一个名为DelegatingFilterProxyFilter实现。这个 Servet 在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立了桥接。Servlet 容器用自己的标准注册 Filter,但它对 Spring Bean 无感知。 DelegatingFilterProxy 通过标准 Servlet 容器机制注册到 Servlet 中,但将所有工作都委托给了实现 Filter 的 Spring Bean

DelegatingFilterProxy 伪代码如下:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// Lazily get Filter that was registered as a Spring Bean
	// For the example in DelegatingFilterProxy, delegate is an instance of Bean Filter0
	Filter delegate = getFilterBean(someBeanName);
	// delegate work to the Spring Bean
	delegate.doFilter(request, response);
}

DelegatingFilterProxy通过过滤器名获取bean,并委托bean进行请求处理

1.1.2 FilterChainProxy

Spring Security 对 Servlet 的支持包含在 FilterChainProxy。 FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter。它通过过滤功能代理给 SecurityFilterChain 维护的一组Filter链

当请求到达 FilterChainProxy 之后,FilterChainProxy 会根据请求的路径,将请求转发到不同的 Spring Security Filters 上面去,不同的 Spring Security Filters 对应了不同的过滤器,也就是不同的请求将经过不同的过滤器

// FilterChainProxy源码
private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
		".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
private HttpFirewall firewall = new StrictHttpFirewall();


@Override
public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {
	boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
	if (clearContext) {
		try {
			request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
			doFilterInternal(request, response, chain);
		}
		finally {
			SecurityContextHolder.clearContext();
			request.removeAttribute(FILTER_APPLIED);
		}
	}
	else {
		doFilterInternal(request, response, chain);
	}
}

private void doFilterInternal(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {
	FirewalledRequest fwRequest = firewall
			.getFirewalledRequest((HttpServletRequest) request);
	HttpServletResponse fwResponse = firewall
			.getFirewalledResponse((HttpServletResponse) response);
	List<Filter> filters = getFilters(fwRequest);
	if (filters == null || filters.size() == 0) {
		if (logger.isDebugEnabled()) {
			logger.debug(UrlUtils.buildRequestUrl(fwRequest)
					+ (filters == null ? " has no matching filters"
							: " has an empty filter list"));
		}
		fwRequest.reset();
		chain.doFilter(fwRequest, fwResponse);
		return;
	}
	VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
	vfc.doFilter(fwRequest, fwResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
	for (SecurityFilterChain chain : filterChains) {
		if (chain.matches(request)) {
			return chain.getFilters();
		}
	}
	return null;
}

1.1.3 SecurityFilterChain

SecurityFilterChain中的 Filter是 Spring Bean,它们是注册 FilterChainProxy 中,而不是在 DelegatingFilterProxy 注册的。相比较直接向 Servlet 容器或 DelegatingFilterProxy 注册,FilterChainProxy 有许多优势。

多个SecurityFilterChain, FilterChainProxy 使用第一个匹配的 SecurityFilterChain进行请求过滤

多过滤器链配置示例:

@Configuration
public class SecurityConfig {
    @Configuration
    @Order(1)
    static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/foo/**")
                    .authorizeRequests()
                    .anyRequest().hasRole("admin")
                    .and()
                    .csrf().disable();
        }
    }

    @Configuration
    @Order(2)
    static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/bar/**")
                    .authorizeRequests()
                    .anyRequest().hasRole("user")
                    .and()
                    .formLogin()
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    }
}

1.2 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter负责表单认证,继承自AbstractAuthenticationProcessingFilter抽象类。

其父类AbstractAuthenticationProcessingFilterdoFilte方法是一个模板方法,定义了认证的流程:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
    // 根据请求路径,判断是否需要认证,不需要认证直接调用下个过滤器
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    try {
        // 返回请求认证,UsernamePasswordAuthenticationFilter实现此方法
        Authentication authenticationResult = attemptAuthentication(request, response);
        // token为空直接返回
        if (authenticationResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            return;
        }
        
        // 会话相关策略设置
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);
        
        // Authentication success
        // 认证后是否继续调用下个过滤器,默认false
        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        // 钩子,提供扩展点
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
        unsuccessfulAuthentication(request, response, failed);
    }
    catch (AuthenticationException ex) {
        // Authentication failed
        unsuccessfulAuthentication(request, response, ex);
    }
}

UsernamePasswordAuthenticationFilter实现attemptAuthentication方法:

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
    // 如果不是post请求,抛出异常
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }
	
    //从请求中获取用户名、密码
    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }

    username = username.trim();
    
	// 构建token,现在token还未认证
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);

    // Allow subclasses to set the "details" property
    // 把请求中的远传地址等信息设置到token中
    setDetails(request, authRequest);
    
	// 获取认证管理器,并委派认证管理器进行认证,返回认证token(此时,token携带认证是否成功信息)
    return this.getAuthenticationManager().authenticate(authRequest);
}

1.3 AuthenticationManager

在 Spring Security 中,用来处理身份认证的类是 AuthenticationManager,我们也称之为认证管理器。

AuthenticationManager 中规范了 Spring Security 的过滤器要如何执行身份认证,并在身份认证成功后返回一个经过认证的 Authentication 对象。AuthenticationManager 是一个接口,我们可以自定义它的实现,但是通常我们使用更多的是系统提供的 ProviderManager

spring-security-arch

1.3.1 ProviderManager

ProviderManager 是的最常用的 AuthenticationManager 实现类。

ProviderManager 管理了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来处理不同的 Authentication 对象的认证。一次完整的身份认证流程可能会经过多个 AuthenticationProvider。

每一个 ProviderManager 管理多个 AuthenticationProvider,同时每一个 ProviderManager 都可以配置一个 parent,如果当前的 ProviderManager 中认证失败了,还可以去它的 parent 中继续执行认证,所谓的 parent 实例,一般也是 ProviderManager,也就是 ProviderManager 的 parent 还是 ProviderManager

一个系统中,我们可以配置多个 HttpSecurity(多个过滤器链),而每一个 HttpSecurity 都有一个对应的 AuthenticationManager 实例(局部 AuthenticationManager),这些局部的 AuthenticationManager 实例都有一个共同的 parent,那就是全局的 AuthenticationManager。

ProviderManager类认证方法authenticate

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Class<? extends Authentication> toTest = authentication.getClass();
    
    // 获取当前认证管理器的所有Provider
	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}
        
        // 如果token存在对应的Provider,则用此Provider进行认证
		result = provider.authenticate(authentication);
		if (result != null) {
			copyDetails(authentication, result);
			break;
		}
	}
    
    // 当前局部认证管理器没认证成功,则调用父认证管理器进行认证
	if (result == null && parent != null) {
		result = parentResult = parent.authenticate(authentication);
	}
	if (result != null) {
		if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
			((CredentialsContainer) result).eraseCredentials();
		}
		if (parentResult == null) {
			eventPublisher.publishAuthenticationSuccess(result);
		}
		return result;
	}
	throw lastException;
}
  1. 首先获取 authentication 的 Class,判断当前 provider 是否支持该 authentication。
  2. 如果支持,则调用 provider 的 authenticate 方法开始做校验,校验完成后,会返回一个新的 Authentication。一会来和大家捋这个方法的具体逻辑。
  3. 这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个 Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验。
  4. copyDetails 方法则用来把旧的 Token 的 details 属性拷贝到新的 Token 中来。
  5. 接下来会调用 eraseCredentials 方法擦除凭证信息,也就是你的密码,这个擦除方法比较简单,就是将 Token 中的 credentials 属性置空。
  6. 最后通过 publishAuthenticationSuccess 方法将登录成功的事件广播出去

1.3.2 AuthenticationProvider

AuthenticationProvider 定义了 Spring Security 中的验证逻辑:

public interface AuthenticationProvider {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	boolean supports(Class<?> authentication);
}

每个AuthenticationProvider和token一一对应,UsernamePasswordAuthenticationToken对应的Provider是DaoAuthenticationProvider,DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider,其父类方法authenticate定义认证逻辑:

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}
}
  1. 首先从 Authentication 提取出登录用户名。
  2. 然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadUserByUsername 方法(调用UserDetailsService的loadUserByUsername方法)
  3. 接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等。
  4. additionalAuthenticationChecks 方法则是做密码比对的,
  5. 最后在 postAuthenticationChecks.check 方法中检查密码是否过期。
  6. 接下来有一个 forcePrincipalAsString 属性,这个是是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性我们一开始在 UsernamePasswordAuthenticationFilter 类中其实就是设置为字符串的(即 username),但是默认情况下,当用户登录成功之后, 这个属性的值就变成当前用户这个对象了。之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。
  7. 最后,通过 createSuccessAuthentication 方法构建一个新的 UsernamePasswordAuthenticationToken。

DaoAuthenticationProvider类主要实现了父类的additionalAuthenticationChecks方法,定义如何比较密码:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
 @SuppressWarnings("deprecation")
 protected void additionalAuthenticationChecks(UserDetails userDetails,
   UsernamePasswordAuthenticationToken authentication)
   throws AuthenticationException {
  if (authentication.getCredentials() == null) {
   throw new BadCredentialsException(messages.getMessage(
     "AbstractUserDetailsAuthenticationProvider.badCredentials",
     "Bad credentials"));
  }
  String presentedPassword = authentication.getCredentials().toString();
  if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
   throw new BadCredentialsException(messages.getMessage(
     "AbstractUserDetailsAuthenticationProvider.badCredentials",
     "Bad credentials"));
  }
 }
}

1.3.3 UserDetails

public interface UserDetalls extends Serializble {
    Collection<? extend GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccontNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

1.4 FilterSecurityInterceptor

FilterSecurityInterceptor决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
	
    // 是否执行过该过滤器的标记
	private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
    
	// 访问的资源元数据,默认是ExpressionBasedFilterInvocationSecurityMetadataSource
	private FilterInvocationSecurityMetadataSource securityMetadataSource;
    
    // 是否每次只请求一次该过滤器,例如在jsp进行转发的时候,会多次经过该过滤器,这个标记就是用来
    // 判断此时需不需要spring-security再进行一次安全检查
	private boolean observeOncePerRequest = true;

	public void init(FilterConfig arg0) throws ServletException {
	}
    
	public void destroy() {
	}

	// 过滤方法,实际上是new一个FilterInvocation然后委托给它执行
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
        // 核心调用
		invoke(fi);
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
		this.securityMetadataSource = newSource;
	}
	
    // 安全对象类型
	public Class<?> getSecureObjectClass() {
		return FilterInvocation.class;
	}

	public void invoke(FilterInvocation fi) throws IOException, ServletException {
        // 如果request不为空并且已经执行过该过滤器并且observeOncePerRequest = true
        // (只请求一次该过滤器)则过滤器继续往下走,不执行spring-security检查
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// 如果请求不为空并且只请求一次该过滤器,设置已经执行过该过滤器的标记
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
			
            // 安全对象调用前进行权限判断
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
                // 过滤链继续执行
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
                
                // 安全对象调用完成后,清理AbstractSecurityInterceptor的工作
				super.finallyInvocation(token);
			}
			
            // 安全对象调用完成后,完成AbstractSecurityInterceptor的工作。
			super.afterInvocation(token, null);
		}
	}

	public boolean isObserveOncePerRequest() {
		return observeOncePerRequest;
	}

	public void setObserveOncePerRequest(boolean observeOncePerRequest) {
		this.observeOncePerRequest = observeOncePerRequest;
	}
}


1.5 FilterInvocationSecurityMetadataSource

FilterInvocationSecurityMetadataSource是一个标记接口,用来获取资源角色元数据,包含3个方法:

实现实例:

@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;  //从数据库加载url及关联的角色
    
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    // 入参object就是受保护的对象
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        
        // 获取当前请求路径
        String requestURI = 
                   ((FilterInvocation) object).getRequest().getRequestURI();
        
        List<Menu> allMenu = menuService.getAllMenu();
        
        // 遍历以查找当前请求路径所需要的角色/权限
        for (Menu menu : allMenu) {
            if (antPathMatcher.match(menu.getPattern(), requestURI)) {
                String[] roles = menu.getRoles().stream()
                               .map(r -> r.getName()).toArray(String[]::new);
                
                return SecurityConfig.createList(roles);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象

1.6 AccessDecisionManager

当用户想要访问某一个资源时,授权管理器通过持有的投票器根据用户的角色投出赞成或者反对票;

  • 所谓投票器其实就是判断方法,授权管理器调用decide方法时会委派给持有的投票器进行判断
  • 一个授权管理器可以持有多个投票器,如何综合每个投票器的结果做出判断就是所谓的表决机制
public interface AccessDecisionManager {
    // 决策 主要通过其持有的 AccessDecisionVoter 来进行投票决策
    void decide(Authentication authentication, Object object,
    Collection<ConfigAttribute> configAttributes) throws
    AccessDeniedException,
    InsufficientAuthenticationException;
    
    // 以确定AccessDecisionManager是否可以处理传递的ConfigAttribute
    boolean supports(ConfigAttribute attribute);
    
    //以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。
    boolean supports(Class<?> clazz);
}

AccessDecisionManager有三个默认实现(表决机制):

1.7 AccessDecisionVoter

AccessDecisionManager授权管理器依赖投票器AccessDecisionVoterAccessDecisionVoter定义如下:

public interface AccessDecisionVoter<S> {
	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;
	boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
}
  1. 从常量名字中就可以看出每个常量的含义,1 表示赞成;0 表示弃权;-1 表示拒绝。
  2. 两个 supports 方法用来判断投票器是否支持当前请求。
  3. vote 则是具体的投票方法。在不同的实现类中实现。三个参数,authentication 表示当前登录主体;object 是一个 ilterInvocation,里边封装了当前请求;attributes 表示当前所访问的接口所需要的角色集合

常用投票器有:

1.7.1 RoleVoter

public int vote(Authentication authentication, Object object,
		Collection<ConfigAttribute> attributes) {
	if (authentication == null) {
		return ACCESS_DENIED;
	}
	int result = ACCESS_ABSTAIN;
	Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
	for (ConfigAttribute attribute : attributes) {
		if (this.supports(attribute)) {
			result = ACCESS_DENIED;
			for (GrantedAuthority authority : authorities) {
				if (attribute.getAttribute().equals(authority.getAuthority())) {
					return ACCESS_GRANTED;
				}
			}
		}
	}
	return result;
}

如果当前登录主体为 null,则直接返回 ACCESS_DENIED 表示拒绝访问;否则就从当前登录主体 authentication 中抽取出角色信息,然后和 attributes 进行对比,如果具备 attributes 中所需角色的任意一种,则返回 ACCESS_GRANTED 表示允许访问

1.7.2 RoleHierarchyVoter

RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承

RoleHierarchyVoter接口定义如下:

public interface RoleHierarchy {
 Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
   Collection<? extends GrantedAuthority> authorities);

}

该接口中只有一个方法,返回值是一个可访问的权限集合

RoleHierarchy 接口有两个实现类:

public class RoleHierarchyImpl implements RoleHierarchy {

	private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);

	private String roleHierarchyStringRepresentation = null;

	private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
    
	private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
    
	public void setHierarchy(String roleHierarchyStringRepresentation) {
		this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;

		logger.debug("setHierarchy() - The following role hierarchy was set: "
				+ roleHierarchyStringRepresentation);

		buildRolesReachableInOneStepMap();
		buildRolesReachableInOneOrMoreStepsMap();
	}

	
	private void buildRolesReachableInOneStepMap() {
		Pattern pattern = Pattern.compile("(\\s*([^\\s>]+)\\s*>\\s*([^\\s>]+))");

		Matcher roleHierarchyMatcher = pattern
				.matcher(this.roleHierarchyStringRepresentation);
		this.rolesReachableInOneStepMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();

		while (roleHierarchyMatcher.find()) {
			GrantedAuthority higherRole = new SimpleGrantedAuthority(
					roleHierarchyMatcher.group(2));
			GrantedAuthority lowerRole = new SimpleGrantedAuthority(
					roleHierarchyMatcher.group(3));
			Set<GrantedAuthority> rolesReachableInOneStepSet;

			if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
				rolesReachableInOneStepSet = new HashSet<GrantedAuthority>();
				this.rolesReachableInOneStepMap.put(higherRole,
						rolesReachableInOneStepSet);
			}
			else {
				rolesReachableInOneStepSet = this.rolesReachableInOneStepMap
						.get(higherRole);
			}
			addReachableRoles(rolesReachableInOneStepSet, lowerRole);

			logger.debug("buildRolesReachableInOneStepMap() - From role " + higherRole
					+ " one can reach role " + lowerRole + " in one step.");
		}
	}

	
	private void buildRolesReachableInOneOrMoreStepsMap() {
		this.rolesReachableInOneOrMoreStepsMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();
		// iterate over all higher roles from rolesReachableInOneStepMap

		for (GrantedAuthority role : this.rolesReachableInOneStepMap.keySet()) {
			Set<GrantedAuthority> rolesToVisitSet = new HashSet<GrantedAuthority>();

			if (this.rolesReachableInOneStepMap.containsKey(role)) {
				rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(role));
			}

			Set<GrantedAuthority> visitedRolesSet = new HashSet<GrantedAuthority>();

			while (!rolesToVisitSet.isEmpty()) {
				// take a role from the rolesToVisit set
				GrantedAuthority aRole = rolesToVisitSet.iterator().next();
				rolesToVisitSet.remove(aRole);
				addReachableRoles(visitedRolesSet, aRole);
				if (this.rolesReachableInOneStepMap.containsKey(aRole)) {
					Set<GrantedAuthority> newReachableRoles = this.rolesReachableInOneStepMap
							.get(aRole);

					// definition of a cycle: you can reach the role you are starting from
					if (rolesToVisitSet.contains(role)
							|| visitedRolesSet.contains(role)) {
						throw new CycleInRoleHierarchyException();
					}
					else {
						// no cycle
						rolesToVisitSet.addAll(newReachableRoles);
					}
				}
			}
			this.rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet);

			logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + role
					+ " one can reach " + visitedRolesSet + " in one or more steps.");
		}

	}

}

1.8 ObjectPostProcessor

在 Spring Security 中,由于框架本身大量采用了 Java 配置,并且没有将对象的各个属性都暴露出来,这样做的本意是为了简化配置。然而这样带来的一个问题就是需要我们手动将 Bean 注册到 Spring 容器中去,ObjectPostProcessor 就是为了解决该问题。一旦将 Bean 注册到 Spring 容器中了,我们可以用ObjectPostProcessor 去增强一个 Bean 的功能,或者需修改一个 Bean 的属性

package org.springframework.security.config.annotation;
public interface ObjectPostProcessor<T> {
	<O extends T> O postProcess(O object);
}

Spring Security 框架源码中,随处可见手动装配。Spring Security 中,过滤器链中的所有过滤器都是通过对应的 xxxConfigure 来进行配置的,而所有的 xxxConfigure 都是继承自 SecurityConfigurerAdapter,而在这些 xxxConfigure 的 configure 方法中,无一例外的都会让他们各自配置的管理器去 Spring 容器中走一圈,例如 AbstractAuthenticationFilterConfigurer#configure 方法:

public void configure(B http) throws Exception {
	...
    ...
	F filter = postProcess(authFilter);
	http.addFilter(filter);
}

例如,权限管理本身是由 FilterSecurityInterceptor 控制的,系统默认的 FilterSecurityInterceptor 已经创建好了,而且我也没办法修改它的属性,那么怎么办呢?我们可以利用 withObjectPostProcessor 方法,去修改 FilterSecurityInterceptor 中的相关属性

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })
                .and()
                ...
    }
}

上面这个配置生效的原因之一是因为 FilterSecurityInterceptor 在创建成功后,会重走一遍 postProcess 方法,这里通过重写 postProcess 方法就能实现属性修改

二、自定义登录认证

2.1 数据库建表

image-20220827232538551

-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu`  (
  `mid` bigint(20) NOT NULL COMMENT '菜单ID',
  `pattern` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单URL',
  PRIMARY KEY (`mid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role`  (
  `id` bigint(20) NOT NULL COMMENT 'ID',
  `mid` bigint(20) NOT NULL COMMENT '菜单ID',
  `rid` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单-角色权限映射表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `rid` bigint(20) NOT NULL COMMENT '角色ID',
  `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
  `note` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色描述',
  PRIMARY KEY (`rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` bigint(20) NOT NULL COMMENT '用户ID',
  `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户帐号',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
  `enabled` tinyint(4) NOT NULL COMMENT '帐号是否启用',
  `locked` tinyint(4) NOT NULL COMMENT '帐号是否锁定',
  PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` bigint(20) NOT NULL COMMENT 'ID',
  `uid` bigint(20) NOT NULL COMMENT '用户ID',
  `rid` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户-角色映射表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

2.2 创建实体类和服务

2.1 实体类(省略set/get方法)

2.3 编写服务层

2.4 认证相关配置

2.4.1 配置userDetailsService

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                UserDetail userDetail = userService.loadUserByUsername(username);
                if (userDetail == null) {
                    throw new UsernameNotFoundException("用户不存在!");
                }
                return userDetail;
            }
        };
    }

}

2.4.2 认证成功、失败回调

三、自定义动态授权

动态授权配置分为两步:

  • 实现FilterInvocationSecurityMetadataSource接口,返回请求资源所需要的权限
  • 配置权限管理器,实现权限鉴定,有两种方式:
    • 实现AccessDecisionManager接口,自定义投票逻辑,不依赖系统内置投票器,但实现权限继承比较麻烦
    • 配置内置授权管理器和分层投票器,实现角色继承

3.1 实现FilterInvocationSecurityMetadataSource

@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private MenuService menuService;
    @Autowired
    private AntPathMatcher antPathMatcher;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        // 获取当前保护对象的url
        String requestURI = ((FilterInvocation) o).getRequest().getRequestURI();
        // 查询所有url对应的角色
        List<MenuDetail> menuDetails = menuService.queryAllMenuDetails();

        // 查找当前请求所需要的角色
        for (MenuDetail menuDetail : menuDetails) {
            if (antPathMatcher.match(menuDetail.getPattern(), requestURI)) {
                String[] list = menuDetail.getRoles().stream().map(r -> r.getName()).toArray(String[]::new);
                return SecurityConfig.createList(list);
            }
        }
        return null;
    }

    @Override
    // 如果不为空,security启动时会做校验,一般直接返回null即可
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        // 类的isAssignableFrom判断当前类是否是入参的接口或父类
        // 权限过滤器会封装请求、响应、调用链为FilterInvocation。所已本方法返回true
        // 当方法返回true,才能调用getAttributes
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

3.2 自定义AccessDecisionManager

FilterInvocationSecurityMetadataSource返回值会作为给AccessDecisionManager.decide方法入参

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
    @Override
    /**
     *
     * @author weixia
     * @date 2022/8/27
     * @param authentication 当前请求的用户,包含当前用户所拥有的权限信息
     * @param o 待保护对象
     * @param collection 目标请求所需要的权限
     */
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        if (collection == null || collection.size() == 0) {
            // 如果受保护对象不需要权限,则直接返回放行
            return;
        }

        if (authentication == null) {
            throw new AccessDeniedException("请用户登录再访问");
        }

        Iterator<ConfigAttribute> iterator = collection.iterator();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            for (GrantedAuthority authority : authorities) {
                if (configAttribute.getAttribute().equalsIgnoreCase(authority.getAuthority())) {
                    return;
                }
            }

            throw new AccessDeniedException("请用户登录再访问");
        }

    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

3.3 使用内置管理器和投票器实现权限继承

利用ObjectPostProcessor重写权限过滤器的属性:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomSecurityMetadataSource customSecurityMetadataSource;
    @Autowired
    private CustomAccessDecisionManager customAccessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        
                        // 可以注入系统内置管理器也可以注入自己实现的授权管理器
                        o.setAccessDecisionManager(accessDecisionManager());
                        o.setSecurityMetadataSource(customSecurityMetadataSource);
                        return o;
                    }
                });
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy(
                "ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher"
        );
        return roleHierarchy;
    }

    
    @Bean
    public AffirmativeBased accessDecisionManager() {
        RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());

        return new AffirmativeBased(
                Arrays.asList(roleHierarchyVoter)
        );
    }

}

3.4 授权异常回调

3.5 认证与动态授权完整配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    @Autowired
    private LoginFailHandler loginFailHandler;
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Autowired
    private CustomSecurityMetadataSource customSecurityMetadataSource;
    @Autowired
    private CustomAccessDecisionManager customAccessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(accessDecisionManager());
                        o.setSecurityMetadataSource(customSecurityMetadataSource);
                        return o;
                    }
                })
                .and()
                    .formLogin()
                    .loginProcessingUrl("/login").permitAll()
                    .successHandler(loginSuccessHandler)
                    .failureHandler(loginFailHandler)
                .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(myAuthenticationEntryPoint)
                    .accessDeniedHandler(myAccessDeniedHandler);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/user/regist");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                UserDetail userDetail = userService.loadUserByUsername(username);
                if (userDetail == null) {
                    throw new UsernameNotFoundException("用户不存在!");
                }
                return userDetail;
            }
        };
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }

    @Bean
    public AntPathMatcher antPathMatcher() {
        return new AntPathMatcher();
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy(
                "ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher"
        );
        return roleHierarchy;
    }

    @Bean
    public AffirmativeBased accessDecisionManager() {
        RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());

        return new AffirmativeBased(
                Arrays.asList(roleHierarchyVoter)
        );
    }

}

标签:return,Spring,private,new,过滤器,授权,Security,null,public
来源: https://www.cnblogs.com/weixia-blog/p/16631862.html