Spring Security (一) 核心组件

本文详细介绍了Spring Security的认证流程,包括SecurityContextHolder的作用、Authentication接口的功能、AuthenticationManager的工作原理、DaoAuthenticationProvider的具体实现以及UserDetails与UserDetailsService的职责。

1、SecurityContextHolder

SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。

获取用户信息

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!authentication.isAuthenticated()){
    return null;
}

Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof org.springframework.security.core.userdetails.UserDetails){
     username = ((UserDetails) principal).getUsername();
}else {
    username = principal.toString();
}
return username;

getAuthentication()返回认证信息getPrincipal()返回身份信息。UserDetails是Spring对身份信息封装的一个接口

2、Authentication

/**
 * Authentication在spring security中是最高级别的身份/认证的抽象
 */
public interface Authentication extends Principal, Serializable {
	/**
	 * 权限信息列表,GrantedAuthority的实现类
	 * @return
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
	 * @return
	 */
	Object getCredentials();
    
	/**
	 * 细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
	 * @return
	 */
	Object getDetails();

	/**
	 * 身份信息
	 * @return
	 */
	Object getPrincipal();

	/**
	 * 是否认证
	 * @return
	 */
	boolean isAuthenticated();

	/**
	 * 设置是否认证
	 * @param isAuthenticated
	 * @throws IllegalArgumentException
	 */
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Spring Security是如何完成身份认证的?
  1. 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. AuthenticationManager 身份管理器负责验证这个Authentication
  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
  4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

3、AuthenticationManager

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

AuthenticationManager接口是认证的核心接口,也是发起认证的出发点。在实现需求中,用户可能需要用户名/密码,手机/密码,或者开放平台unionId登录等等,所以AuthenticationManager并不直接认证。

ProviderManagerAuthenticationManager常用的实现类。ProviderManager维护着一个 List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。也就是说,核心的认证入口始终只有一个:AuthenticationManager

ProviderManager代码

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    //AuthenticationProvider列表
    private List<AuthenticationProvider> providers;

    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;
        boolean debug = logger.isDebugEnabled();
        
        //AuthenticationProvider列表的Iterator
        Iterator var8 = this.getProviders().iterator();

        //依次认证
        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            //是否支持
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    //认证
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (AuthenticationException var14) {
                    lastException = var14;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }

        // 如果有Authentication信息,则直接返回
        if (result != null) {
            ////移除密码
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            // //发布登录成功事件
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
            //认证失败,包装异常信息
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

ProviderManager 中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。

4、DaoAuthenticationProvider

DaoAuthenticationProvider最常用的一个实现类

在这里插入图片描述

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

	private PasswordEncoder passwordEncoder;
    
    //
    private UserDetailsService userDetailsService;
    
    //检索用户,
	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //UserDetailsService  通过用户名去检索用户,实现中一般从数据库中
			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);
		}
	}
}

DaoAuthenticationProvider.retrieveUser()只是检索用户,真正认证的逻辑在父类AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider中的认证代码

5、UserDetails与UserDetailsService

UserDetails接口,它代表了最详细的用户信息

public interface UserDetails extends Serializable {

	/**
	 * 权限
	 * @return
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 密码
	 * @return
	 */
	String getPassword();


	/**
	 * 用户名
	 * @return
	 */
	String getUsername();


	/**
	 * 是否账号过期
	 * @return
	 */
	boolean isAccountNonExpired();


	/**
	 * 是否锁定
	 * @return
	 */
	boolean isAccountNonLocked();


	/**
	 * 用户凭证(密码)是否过期
	 * @return
	 */
	boolean isCredentialsNonExpired();


	/**
	 * 启用禁用
	 * @return
	 */
	boolean isEnabled();
}

UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,UserDetailsService常见的实现类有JdbcDaoImplInMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

认证流程

常是数据库)加载用户信息,UserDetailsService常见的实现类有JdbcDaoImplInMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

认证流程

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值