理解Spring Security和实现动态授权

2022/8/28 6:23:50

本文主要是介绍理解Spring Security和实现动态授权,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、Spring Security架构

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

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

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

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

image-20220825110620010

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

  • ChannelProcessingFilter:ChannelProcessingFilter 通常是用来过滤哪些请求必须用 https 协议, 哪些请求必须用 http协议, 哪些请求随便用哪个协议都行
  • ConcurrentSessionFilter:ConcurrentSessionFilter 主要用来判断session 是否过期以及更新最新的访问时间。
  • WebAsyncManagerIntegrationFilter:WebAsyncManagerIntegrationFilter 用于集成SecurityContext到Spring异步执行机制中的
  • SecurityContextPersistenceFilter:SecurityContextPersistenceFilter 主要控制 SecurityContext 的在一次请求中的生命周期 。请求来临时,创建SecurityContext 安全上下文信息,请求结束时清空 SecurityContextHolder 。SecurityContextPersistenceFilter 通过 HttpScurity#securityContext() 及相关方法引入其配置对象SecurityContextConfigurer 来进行配置。
  • HeaderWriterFilter:HeaderWriterFilter 用来给 http 响应添加一些 Header ,比如 X-Frame-Options , X-XSSProtection, X-Content-Type-Options 。
  • CorsFilter:跨域相关的过滤器。这是Spring MVC Java 配置和XML 命名空间 CORS 配置的替代方法, 仅对依赖于spring-web 的应用程序有用(不适用于spring-webmvc )或 要求在javax.servlet.Filter 级别进行CORS检查的安全约束链接。
  • CsrfFilter:CsrfFilter 用于防止csrf 攻击,一旦开启了CSRF(默认开启),所有经过springsecurity的http请求以及资源都会被CsrfFilter拦截,仅仅GET|HEAD|TRACE|OPTIONS这4类方法会被放行,也就是说post、delete等方法依旧是被拦截掉。前后端使用json交互可以通过 HttpSecurity.csrf() 来关闭它。在你使用 jwt 等 token 技术时,是不需要这个的。
  • LogoutFilter:LogoutFilter是处理注销的过滤器。
  • OAuth2AuthorizationRequestRedirectFilter:和上面的有所不同,这个需要依赖 spring-scurity-oauth2 相关的模块。该过滤器是处理 OAuth2 请求首选重定向相关逻辑的。
  • Saml2WebSsoAuthenticationRequestFilter:这个需要用到 Spring Security SAML 模块,这是一个基于 SMAL 的 SSO 单点登录请求认证过滤器。
  • X509AuthenticationFilter:X509 认证过滤器。
  • AbstractPreAuthenticatedProcessingFilter:AbstractPreAuthenticatedProcessingFilter 处理经过预先认证的身份验证请求的过滤器的基类,目的是从传入请求中提取主体上的必要信息
  • CasAuthenticationFilter:CAS 单点登录认证过滤器 。依赖 Spring Security CAS 模块
  • OAuth2LoginAuthenticationFilter:这个需要依赖 spring-scurity-oauth2 相关的模块,OAuth2 登录认证过滤器,处理通过 OAuth2进行认证登录的逻辑。
  • Saml2WebSsoAuthenticationFilter:这个需要用到 Spring Security SAML 模块,这是一个基于 SMAL 的 SSO 单点登录认证过滤器。
  • UsernamePasswordAuthenticationFilter:处理用户以及密码认证的核心过滤器,认证请求提交的username 和 password 被封装成token 进行一系列的认证
  • OpenIDAuthenticationFilter:基于OpenID 认证协议的认证过滤器,需要依赖额外的相关模块才能启用它。
  • DefaultLoginPageGeneratingFilter:生成默认的登录页,默认 /login 。
  • DefaultLogoutPageGeneratingFilter:生成默认的退出页,默认 /logout 。
  • DigestAuthenticationFilter:Digest 身份验证是 Web 应用程序中流行的可选的身份验证机制 。DigestAuthenticationFilter能够处理 HTTP 头中显示的摘要式身份验证凭据。
  • BasicAuthenticationFilter:和Digest 身份验证一样都是Web 应用程序中流行的可选的身份验证机制 。BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring Security的 Spring Boot 自动配置默认是启用的
  • RequestCacheAwareFilter:用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求
  • SecurityContextHolderAwareRequestFilter:用来实现j2ee中 Servlet Api 一些接口方法, 比如 getRemoteUser 方法、isUserInRole 方法在使用 Spring Security 时其实就是通过这个过滤器来实现的
  • JaasApiIntegrationFilter:适用于JAAS ( Java 认证授权服务)。 如果 SecurityContextHolder 中拥有的Authentication 是一个 JaasAuthenticationToken ,那么该 JaasApiIntegrationFilter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain 。
  • RememberMeAuthenticationFilter:处理记住我功能的过滤器
  • AnonymousAuthenticationFilter:匿名认证过滤器。对于 Spring Security 来说,所有对资源的访问都是有 Authentication 的。对于无需登录( UsernamePasswordAuthenticationFilter )直接可以访问的资源,会授予其匿名用户身份
  • SessionManagementFilter:Session 管理器过滤器,内部维护了一个SessionAuthenticationStrategy 用于管理 Session
  • ExceptionTranslationFilter:主要来传输异常事件
  • FilterSecurityInterceptor:这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。如果你要实现动态权限控制就必须研究该类 。
  • SwitchUserFilter:SwitchUserFilter 是用来做账户切换的

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;
}
  • filterChains不是某个过滤器,而是多个过滤器链的集合

  • 在 doFilter 方法中,正常来说,clearContext 参数每次都是 true,于是每次都先给 request 标记上 FILTER_APPLIED 属性,然后执行 doFilterInternal 方法去走过滤器,执行完毕后,最后在 finally 代码块中清除 SecurityContextHolder 中保存的用户信息,同时移除 request 中的标记

  • doFilterInternal方法:

    • 调用 getFilters 方法找到过滤器链。该方法就是根据当前的请求,从 filterChains 中找到对应的过滤器链,然后由该过滤器链去处理请求
    • 如果找出来的 filters 为 null,或者集合中没有元素,那就是说明当前请求不需要经过过滤器。直接执行 chain.doFilter ,这个就又回到原生过滤器中去了
    • 如果查询到的 filters 中是有值的,那么这个 filters 集合中存放的就是我们要经过的过滤器链了。此时它会构造出一个虚拟的过滤器链 VirtualFilterChain 出来,并执行其中的 doFilter 方法
    private static class VirtualFilterChain implements FilterChain {
    	private final FilterChain originalChain;
    	private final List<Filter> additionalFilters;
    	private final FirewalledRequest firewalledRequest;
    	private final int size;
    	private int currentPosition = 0;
    	private VirtualFilterChain(FirewalledRequest firewalledRequest,
    			FilterChain chain, List<Filter> additionalFilters) {
    		this.originalChain = chain;
    		this.additionalFilters = additionalFilters;
    		this.size = additionalFilters.size();
    		this.firewalledRequest = firewalledRequest;
    	}
    	@Override
    	public void doFilter(ServletRequest request, ServletResponse response)
    			throws IOException, ServletException {
    		if (currentPosition == size) {
    			if (logger.isDebugEnabled()) {
    				logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
    						+ " reached end of additional filter chain; proceeding with original chain");
    			}
    			// Deactivate path stripping as we exit the security filter chain
    			this.firewalledRequest.reset();
    			originalChain.doFilter(request, response);
    		}
    		else {
    			currentPosition++;
    			Filter nextFilter = additionalFilters.get(currentPosition - 1);
    			if (logger.isDebugEnabled()) {
    				logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
    						+ " at position " + currentPosition + " of " + size
    						+ " in additional filter chain; firing Filter: '"
    						+ nextFilter.getClass().getSimpleName() + "'");
    			}
    			nextFilter.doFilter(request, response, this);
    		}
    	}
    }
    
    • VirtualFilterChain 类中首先声明了 5 个全局属性,originalChain 表示原生的过滤器链,也就是 Web Filter;additionalFilters 表示 Spring Security 中的过滤器链;firewalledRequest 表示当前请求;size 表示过滤器链中过滤器的个数;currentPosition 则是过滤器链遍历时候的下标
    • doFilter 方法就是 Spring Security 中过滤器挨个执行的过程,如果 currentPosition == size,表示过滤器链已经执行完毕,此时通过调用 originalChain.doFilter 进入到原生过滤链方法中,同时也退出了 Spring Security 过滤器链。否则就从 additionalFilters 取出 Spring Security 过滤器链中的一个个过滤器,挨个调用 doFilter 方法

1.1.3 SecurityFilterChain

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

  • 首先,它为 Spring Security 提供了一个起点。如果您尝试对 Spring Security 进行故障 debug,那么在 FilterChainProxy 是个合适调试断点。
  • 其次,由于 FilterChainProxy 是 Spring Security 的核心,它可以执行一些关键任务。例如,它可以清除 SecurityContext 以避免内存泄漏。它还可以用 Spring Security HttpFirewall 来保护应用免受某些类型的攻击。
  • 此外,它在确定何时调用一个 SecurityFilterChain 方面提供了更大的灵活性。在 Servlet 容器中,Filter 只能根据 URL 模式匹配调用。但是,FilterChainProxy 可以使用 RequestMatcher 来匹配 HttpServletRequest 中任何内容来调用。

多个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);
}
  • authenticate 方法用来做验证,就是验证用户身份。
  • supports 则用来判断当前的 AuthenticationProvider 是否支持对应的 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;
	}
}


  • 调用父类的beforeInvocation方法,执行授权关键操作:
    • 调用obtainSecurityMetadataSource方法获取当前请求需要的权限列表
    • 调用accessDecisionManager.decide授权管理器方法进行授权

1.5 FilterInvocationSecurityMetadataSource

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

  • Collection getAttributes(Object object)根据提供的受保护对象的信息,其实就是URI,获取该URI 配置的所有角色
  • Collection getAllConfigAttributes()获取全部角色
  • boolean supports(Class<?> clazz)对特定的安全对象是否提供 ConfigAttribute 支持

实现实例:

@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有三个默认实现(表决机制):

  • AffirmativeBased 基于肯定的决策器。 用户持有一个同意访问的角色就能通过
  • ConsensusBased 基于共识的决策器。 用户持有同意的角色数量多于禁止的角色数
  • UnanimousBased 基于一致的决策器。 用户持有的所有角色都同意访问才能放行

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 表示当前所访问的接口所需要的角色集合

常用投票器有:

  • RoleVoter
  • RoleHierarchyVoter

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 接口有两个实现类:

  • NullRoleHierarchy 这是一个空的实现,将传入的参数原封不动返回。
  • RoleHierarchyImpl 这个会完成一些解析操作
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.");
		}

	}

}
  • 用户传入的字符串变量(继承关系字符串)设置给 roleHierarchyStringRepresentation 属性,然后通过 buildRolesReachableInOneStepMap 和 buildRolesReachableInOneOrMoreStepsMap 方法完成对角色层级的解析

  • buildRolesReachableInOneStepMap 方法用来将角色关系解析成一层一层的形式

    假设角色继承关系是 ROLE_A > ROLE_B \n ROLE_C > ROLE_D \n ROLE_C > ROLE_E,Map 中的数据是这样

    • A-->B
    • C-->[D,E]

    假设角色继承关系是 ROLE_A > ROLE_B > ROLE_C > ROLE_D,Map 中的数据是这样:

    • A-->B
    • B-->C
    • C-->D
  • buildRolesReachableInOneOrMoreStepsMap 方法则是对 rolesReachableInOneStepMap 集合进行再次解析,将角色的继承关系拉平。经过 buildRolesReachableInOneOrMoreStepsMap 方法解析之后,新的 Map 中保存的数据如下:

    • A-->[B、C、D]

    • B-->[C、D]

    • C-->D

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方法)

  • User类

    @TableName(value ="user")
    public class User implements Serializable {
        @TableId(value = "uid")
        @TableField(value = "username")
        private String username;
        @TableField(value = "password")
        private String password;
        @TableField(value = "enabled")
        private Integer enabled;
        @TableField(value = "locked")
        private Integer locked;
    }
    
  • UserDetail类(主要用于loadUserByUsername方法)

    @AllArgsConstructor
    @NoArgsConstructor
    public class UserDetail implements UserDetails {
        private Long uid;
        private String username;
        private String password;
        private Integer enabled;
        private Integer locked;
        private List<Role> roles;
        
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return roles.stream()
                    .map(r -> new SimpleGrantedAuthority(r.getName()))
                    .collect(Collectors.toList());
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return locked < 1 ? true : false;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled > 0 ? true : false;
        }
    }
    
  • MenuDetail类(主要用于动态权限)

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class MenuDetail {
        private Long mid;
        private String pattern;
        private List<Role> roles;
    }
    
  • Role类

    @TableName(value ="role")
    public class Role implements Serializable {
        @TableId(value = "rid")
        private Long rid;
        @TableField(value = "name")
        private String name;
        @TableField(value = "note")
        private String note;
    }
    
  • Menu类(资源菜单)

    @TableName(value ="menu")
    public class Menu implements Serializable {
        @TableId(value = "mid")
        private Long mid;
        @TableField(value = "pattern")
        private String pattern;
    }
    
  • UserRole类

    user-role关联表,略

  • MenuRole类

    menu-role关联表,略

2.3 编写服务层

  • UserService

    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserMapper userMapper;
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private UserRoleMapper userRoleMapper;
    
        @Override
        public ResultVO regist(User user) {
            String username = user.getUsername();
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            wrapper.eq("username", username);
            List<User> users = userMapper.selectList(wrapper);
            if (user == null || users.size() == 0) {
    
                user.setEnabled(1);
                user.setLocked(0);
                user.setPassword(passwordEncoder.encode(user.getPassword()));
    
                int i = userMapper.insert(user);
                if (i > 0) {
                    user.setPassword(null);
                    return new ResultVO(ResultStatus.OK, "注册成功", user);
                } else {
                    return new ResultVO(ResultStatus.NO, "注册失败,请重新注册", null);
                }
            } else {
                return new ResultVO(ResultStatus.NO, "用户已存在!", null);
            }
        }
    
        @Override
        public ResultVO setRoles(long uid, List<Long> rids) {
            Long[] success = new Long[rids.size()];
            boolean isSuccess = true;
    
            for (int i = 0;i < rids.size();i++) {
                Long rid = rids.get(i);
                UserRole userRole = new UserRole();
                userRole.setUid(uid);
                userRole.setRid(rid);
                int j = userRoleMapper.insert(userRole);
                if (j > 0) {
                    success[i] = userRole.getId();
                } else {
                    isSuccess = false;
                    break;
                }
            }
    
            if (isSuccess) {
                return new ResultVO(ResultStatus.OK, "角色绑定成功", null);
            } else {
    
                for (int k = 0; k < success.length; k++) {
                    userRoleMapper.deleteById(success[k]);
                }
                return new ResultVO(ResultStatus.NO, "角色绑定失败!", null);
            }
        }
    
        @Override
        public UserDetail loadUserByUsername(String username) {
            return userMapper.loadUserByUsername(username);
        }
    }
    
  • MenuService

    @Service
    @Slf4j
    public class MenuServiceImpl implements MenuService {
        @Autowired
        private MenuMapper menuMapper;
        @Autowired
        private MenuRoleMapper menuRoleMapper;
    
        @Override
        public ResultVO save(Menu menu) {
            int i = menuMapper.insert(menu);
            if (i > 0) {
                return new ResultVO(ResultStatus.OK, "success", menu);
            } else {
                return new ResultVO(ResultStatus.NO, "fail!", null);
            }
        }
    
        @Override
        public ResultVO setRoles(long mid, List<Long> rids) {
            long[] success = new long[rids.size()];
            boolean isSuccess = true;
    
            for (int i = 0; i < rids.size(); i++) {
                long rid = rids.get(i);
                MenuRole menuRole = new MenuRole();
                menuRole.setMid(mid);
                menuRole.setRid(rid);
                int j = menuRoleMapper.insert(menuRole);
                if (j > 0) {
                    success[i] = menuRole.getId();
                } else {
                    isSuccess = false;
                    break;
                }
            }
    
            if (isSuccess) {
                return new ResultVO(ResultStatus.OK, "success", null);
            } else {
                for (int k = 0; k < success.length; k++) {
                    menuRoleMapper.deleteById(success[k]);
                }
                return new ResultVO(ResultStatus.NO, "fail!", null);
            }
        }
    
        @Override
        public List<MenuDetail> queryAllMenuDetails() {
            return menuMapper.queryAllMenuDetails();
        }
    }
    

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 认证成功、失败回调

  • 认证成功回调

    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            httpServletResponse.setCharacterEncoding("utf-8");
            httpServletResponse.setContentType("application/json");
            ResultVO result = new ResultVO(ResultStatus.OK, "登录成功", authentication);
            objectMapper.writeValue(httpServletResponse.getOutputStream(), result);
        }
    }
    
  • 认证失败回调

    @Component
    public class LoginFailHandler implements AuthenticationFailureHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            httpServletResponse.setCharacterEncoding("utf-8");
            httpServletResponse.setContentType("application/json");
            ResultVO result = new ResultVO(ResultStatus.NO, "登录失败", null);
            objectMapper.writeValue(httpServletResponse.getOutputStream(), result);
        }
    }
    
  • 配置回调

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private LoginSuccessHandler loginSuccessHandler;
        @Autowired
        private LoginFailHandler loginFailHandler;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                        .formLogin()
                        .loginProcessingUrl("/login").permitAll()
                        .successHandler(loginSuccessHandler)
                        .failureHandler(loginFailHandler);
        }
    }
    

三、自定义动态授权

动态授权配置分为两步:

  • 实现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 授权异常回调

  • 未认证用户访问受限资源异常

    @Component
    public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Autowired
        private ObjectMapper objectMapper;
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            httpServletResponse.setCharacterEncoding("utf-8");
            httpServletResponse.setContentType("application/json");
            ResultVO result = new ResultVO(ResultStatus.NO, "该资源需要登录访问", null);
            objectMapper.writeValue(httpServletResponse.getOutputStream(), result);
        }
    }
    
  • 访问受限资源时权限不足异常

    @Component
    public class MyAccessDeniedHandler implements AccessDeniedHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
            httpServletResponse.setCharacterEncoding("utf-8");
            httpServletResponse.setContentType("application/json");
            ResultVO result = new ResultVO(ResultStatus.NO, "权限不足!", null);
            objectMapper.writeValue(httpServletResponse.getOutputStream(), result);
        }
    }
    
  • 配置回调

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
        @Autowired
        private MyAccessDeniedHandler myAccessDeniedHandler;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                        .exceptionHandling()
                        .authenticationEntryPoint(myAuthenticationEntryPoint)
                        .accessDeniedHandler(myAccessDeniedHandler);
        }
    }
    

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)
        );
    }

}


这篇关于理解Spring Security和实现动态授权的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程