Ooooon

灯光照亮着我

【spring Security】认证授权

权限就是菜单(用户管理、订单管理、、、)和按钮(增、删、改、查、、、),还有数据权限就是针对不同的人可以操作不同的数据

权限管理指系统的安全规则及策略,使用户只能访问自己被授权的资源。在权限管理中涉及两个概念:认证、授权。

认证:用户使用账号密码或手机号验证码等方式进行登录,服务端对登录信息匹配认证。

授权:认证成功查询用户权限进项授权。

  1. 权限模型-RBAC0

RBAC基于角色的权限控制。就是在用户和权限之间增加角色概念,为用户分配角色,为角色分配权限,他们之间都是一对多或多对多的关系,用户的权限就是他所有角色的权限集合。

增加角色的好处:

    • 简化权限管理,为多个用户分配角色,只需要更改角色的权限配置就可以更改此角色所有用户的权限
    • 易于维护
  1. 权限模型-RBAC1

在RBAC0的基础上增加了子角色的概念,子角色的权限只能少于父角色的权限不能多于父角色权限。

  1. 权限模型-RBAC2

在RBAC0的基础上增加了对角色的限制,如:

  • 互斥角色 :同一用户只能分配到一组互斥角色集合中至多一个角色,支持责任分离的原则。互斥角色是指各自权限互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权。常举的例子:在审计活动中,一个角色不能同时被指派给会计角色和审计员角色。
  • 基数约束 :一个角色被分配的用户数量受限;一个用户可拥有的角色数目受限;同样一个角色对应的访问权限数目也应受限,以控制高级权限在系统中的分配。例如公司的领导人有限的;
  • 先决条件角色 :可以分配角色给用户仅当该用户已经是另一角色的成员;对应的可以分配访问权限给角色,仅当该角色已经拥有另一种访问权限。指要想获得较高的权限,要首先拥有低一级的权限。就像我们生活中,***是从副主席中选举的一样。
  • 运行时互斥 :例如,允许一个用户具有两个角色的成员资格,但在运行中不可同时激活这两个角色。
  1. 权限模型-RBAC3

RBAC3称为统一模型,它包含了RBAC1和RBAC2,利用传递性,也把RBAC0包括在内。

Spring Security

Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能。

Spring Security流程图

Spring Security认证流程分析

1、AbstractAuthenticationProcessingFilter:认证流程的抽象处理器

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {

	......
    
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
        // 判断需要拦截的请求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
        	// 执行认证逻辑,attemptAuthentication()方法需要在子类中重写
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			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) {
			// 认证失败
			unsuccessfulAuthentication(request, response, ex);
		}
	}
    
    // 认证成功
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
            
        // 在SecurityContextHolder中设置完成认证的认证信息
		SecurityContextHolder.getContext().setAuthentication(authResult);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
		}
        // 记住我功能,讲token写入数据库,并且放入cookie中。即使服务器重启也不需要重新登录
		this.rememberMeServices.loginSuccess(request, response, authResult);
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
        // 调用认证成功的处理器
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
	}
    
    // 认证失败
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		this.logger.trace("Failed to process authentication request", failed);
		this.logger.trace("Cleared SecurityContextHolder");
		this.logger.trace("Handling authentication failure");
		this.rememberMeServices.loginFail(request, response);
        // 调用认证失败的处理器
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}
    
    ......



}

2、UsernamePasswordAuthenticationFilter:用于拦截 /login 的POST请求做认证处理。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
			"POST");

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;

	......

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
        // 判断请求是否是 POST
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
        // 封装账号密码到 UsernamePasswordAuthenticationToken
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
        // 调用AuthenticationManager处理认证请求
		return this.getAuthenticationManager().authenticate(authRequest);
	}
    .......
        
}

3、AuthenticationManager的实现类ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

	......

    // 针对不同的认证逻辑(微信登录,QQ登录,手机号登录等),都需要去实现AuthenticationProvider
    // 处理各自的认真逻辑
	private List<AuthenticationProvider> providers = Collections.emptyList();

	......
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
            // 遍历所有的AuthenticationProvider 匹配当前的认证类型
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
                // 调用authenticate方法完成认证逻辑
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will
		// publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
		// parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

4、AuthenticationProvider实现类AbstractUserDetailsAuthenticationProvider:默认的账号密码认证处理器

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
	
    // 这里就是账号密码登录的认证逻辑
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                // !!!这里是获取spring Security定义的用户信息UserDetails。需要自定义实现类实现UserDetails封装用户信息及权限信息。retrieveUser在子类DaoAuthenticationProvider中
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		...... 省略一些用户信息校验 ......
        
        // 这里返回值的处理也在DaoAuthenticationProvider中
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
    
    
    // 查找对应认证处理器的方法
    @Override
	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
	}
    
    // 处理返回值---创建一个认证成功的身份证明。这里就是又封装了一下UsernamePasswordAuthenticationToken跟之前封装的区别就是,之前是两个参数的构造,现在是三个参数的构造。看一下UsernamePasswordAuthenticationToken
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		this.logger.debug("Authenticated user");
		return result;
	}

DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    ......

    // 这里是交互重点
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            // UserDetails 是spring Security定义的用户信息实体,UserDetailsService是spring Security定义的用户服务。我们需要分别实现 UserDetails 和 UserDetailsService中,自定义认证信息及认证逻辑,在UserDetailsService中重写loadUserByUsername(string username)方法完成认证逻辑,可以查询数据库、可以找缓存、可以啥都没有
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            // 如果用户信息为空抛异常,认证失败
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
    
    ......
    
    // 处理返回值---创建一个认证成功的身份证明。这里主要是可以进行用户的密码修改,然而我没有用过,也不知道这个意义是啥。。。然后又调回了父类的这个方法super.createSuccessAuthentication(),再看回去
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		boolean upgradeEncoding = this.userDetailsPasswordService != null
				&& this.passwordEncoder.upgradeEncoding(user.getPassword());
		if (upgradeEncoding) {
			String presentedPassword = authentication.getCredentials().toString();
			String newPassword = this.passwordEncoder.encode(presentedPassword);
			user = this.userDetailsPasswordService.updatePassword(user, newPassword);
		}
		return super.createSuccessAuthentication(principal, authentication, user);
	}
    
}

UsernamePasswordAuthenticationToken

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    ......
    
    // 认证前调用的
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	// 认证成功调用的
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); 
	}
    
    // 两个构造函数的区别有两个
    // super(authorities); 是设置权限信息
    // setAuthenticated(boolean b) 设置权限认证的标志,是否完成认证
    ......
    
}

自定义认证逻辑:

    1. 自定义过滤器,继承AuthenticationProvider。定义拦截的请求路径及请求方式,重写attemptAuthentication()方法调用对应的认证逻辑处理器
    2. 自定义Token实体,继承AbstractAuthenticationToken。作为过滤器寻找对应认证处理器的媒介
    1. 自定义认证逻辑的处理器,实现AuthenticationProvider。定义authenticate()方法完成认证逻辑,定义supports()方法用于过滤器与对应处理器的匹配媒介

实现流程大概是这样,具体的细节后面在说。

推荐文章:

RBAC用户、角色、权限、组设计方案 - 左新宇的文章 - 知乎 https://zhuanlan.zhihu.com/p/63769951

SpringSecurity---认证+授权代码实现 https://www.cnblogs.com/qdhxhz/p/13040560.html

posted @ 2021-10-18 17:34  Ooooon  阅读(630)  评论(0)    收藏  举报