前言
本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接
学习Spring Security
有一段时间了,今天就大致理一理Spring Security
核心重要对象,有问题欢迎指出,互相学习👏🏻
SecurityContextHolder
SecurityContextHolder
是Spring Security
中最核心的组件之一,内部封装了保存应用程序中的安全上下文SecurityContext
的逻辑,提供了核心的静态方法暴露getContext
,setContext
用来设置和获取当前请求线程的安全上下文,主要在SecurityContextPersistenceFilter
拦截器中SecurityContextHolder
和SecurityContext
建立链接
SecurityContextHolder和SecurityContext的关联
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//是否执行过
if (request.getAttribute("__spring_security_scpf_applied") != null) {
chain.doFilter(request, response);
} else {
...
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//获取SpringSecurity,默认从HttpSession中获取,获取不到则自动新生成一个SecurityContext
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var10 = false;
try {
var10 = true;
//SecurityContextHolder
SecurityContextHolder.setContext(contextBeforeChainExecution);
...
chain.doFilter(holder.getRequest(), holder.getResponse());
var10 = false;
} finally {
if (var10) {
...
}
}
//获取SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
//清空上下文中的SecurityContext
SecurityContextHolder.clearContext();
//将经过拦截器处理后的SecurityContext重新设置到session中,便于下次请求再次获取
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
核心逻辑实现
- 默认从
HttpSession
中获取SecurityContext
,没有则自动生成一个新的SecurityContext
,设置到SecurityContextHolder
中,继续执行后续拦截器 - 其他拦截器以及核心逻辑执行完成后,获取
SecurityContext
,清空上下文信息,并且将经过拦截器处理后的SecurityContext
重新设置到session
中,便于下次请求再次获取
内部如何保存SecurityContext
SecurityContextHolder
保存SecurityContext
内部默认有三种模式
MODE_THREADLOCAL
ThreadLocal模式,通过ThreadLocal方式,将上下文保存到当前请求线程MODE_INHERITABLETHREADLOCAL
父子线程ThreadLocal模式,本质上也是通过ThreadLocal方式,将上下文保存到当前请求线程MODE_GLOBAL
全局模式,适用于全局唯一类型
默认在初始化在initialize
方法中缺省实现为ThreadLocal
模式
public class SecurityContextHolder {
//ThreadLocal模式
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
//父子线程ThreadLocal模式
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
//全局模式
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
//存储策略
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default ,默认ThreadLocal模式
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
Authentication
Authentication本质上是一个抽象接口,定义了身份验证的一系列服务抽象
- getAuthorities 当前授权的权限列表
- getCredentials 密码信息
- getDetails 登录信息。访问者的ip地址和sessionId的值等
- getPrincipal 登录之后返回UserDetails的实现,一般是
org.springframework.security.core.userdetails.User
public interface Authentication extends Principal, Serializable {
/**
* Set by an <code>AuthenticationManager</code> to indicate the authorities that the
* principal has been granted. Note that classes should not rely on this value as
* being valid unless it has been set by a trusted <code>AuthenticationManager</code>.
* <p>
* Implementations should ensure that modifications to the returned collection array
* do not affect the state of the Authentication object, or use an unmodifiable
* instance.
* </p>
* @return the authorities granted to the principal, or an empty collection if the
* token has not been authenticated. Never null.
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* The credentials that prove the principal is correct. This is usually a password,
* but could be anything relevant to the <code>AuthenticationManager</code>. Callers
* are expected to populate the credentials.
* @return the credentials that prove the identity of the <code>Principal</code>
*/
Object getCredentials();
/**
* Stores additional details about the authentication request. These might be an IP
* address, certificate serial number etc.
* @return additional details about the authentication request, or <code>null</code>
* if not used
*/
Object getDetails();
/**
* The identity of the principal being authenticated. In the case of an authentication
* request with username and password, this would be the username. Callers are
* expected to populate the principal for an authentication request.
* <p>
* The <tt>AuthenticationManager</tt> implementation will often return an
* <tt>Authentication</tt> containing richer information as the principal for use by
* the application. Many of the authentication providers will create a
* {@code UserDetails} object as the principal.
* @return the <code>Principal</code> being authenticated or the authenticated
* principal after authentication.
*/
Object getPrincipal();
/**
* Used to indicate to {@code AbstractSecurityInterceptor} whether it should present
* the authentication token to the <code>AuthenticationManager</code>. Typically an
* <code>AuthenticationManager</code> (or, more often, one of its
* <code>AuthenticationProvider</code>s) will return an immutable authentication token
* after successful authentication, in which case that token can safely return
* <code>true</code> to this method. Returning <code>true</code> will improve
* performance, as calling the <code>AuthenticationManager</code> for every request
* will no longer be necessary.
* <p>
* For security reasons, implementations of this interface should be very careful
* about returning <code>true</code> from this method unless they are either
* immutable, or have some way of ensuring the properties have not been changed since
* original creation.
* @return true if the token has been authenticated and the
* <code>AbstractSecurityInterceptor</code> does not need to present the token to the
* <code>AuthenticationManager</code> again for re-authentication.
*/
boolean isAuthenticated();
/**
* See {@link #isAuthenticated()} for a full description.
* <p>
* Implementations should <b>always</b> allow this method to be called with a
* <code>false</code> parameter, as this is used by various classes to specify the
* authentication token should not be trusted. If an implementation wishes to reject
* an invocation with a <code>true</code> parameter (which would indicate the
* authentication token is trusted - a potential security risk) the implementation
* should throw an {@link IllegalArgumentException}.
* @param isAuthenticated <code>true</code> if the token should be trusted (which may
* result in an exception) or <code>false</code> if the token should not be trusted
* @throws IllegalArgumentException if an attempt to make the authentication token
* trusted (by passing <code>true</code> as the argument) is rejected due to the
* implementation being immutable or implementing its own alternative approach to
* {@link #isAuthenticated()}
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication
的抽象实现为AbstractAuthenticationToken
,主要定义了抽象中的实体变量定义,比如
private final Collection<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
AbstractAuthenticationToken
默认的实现为UsernamePasswordAuthenticationToken
,本质上跟AbstractAuthenticationToken
没太大差别,是其具体实现
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
是SpringSecurity在web应用中重要的Filter,其主要功能用来通过拿到request
请求中的userName
和password
,生成对应的UsernamePasswordAuthenticationToken
认真实体对象,通过AuthenticationManager
认证管理器来进行认证登录,并且返回一个填充了授权信息的Authentication
对象
UsernamePasswordAuthenticationFilter
核心实现在于其attemptAuthentication
,尝试认证并返回一个填充了授权信息的Authentication
对象
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//获取用户名,密码信息
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
//生成UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- 非Post请求不支持,抛出异常
- 从
request
对象中获取用户名密码,生成UsernamePasswordAuthenticationToken
认证对象, - 通过认证管理器AuthenticationManager进行认证
AuthenticationManager
AuthenticationManager
只有authenticate
一个方法,authenticate
方法定义了认证的基本服务,authenticate
方法的入参和出参都是同一个对象,从authenticate
方法的描述中可以得知,尝试去认证一个Authentication
对象,返回一个被填充了granted authorities
的对象,也是就说需要在authenticate
方法的实现中填充granted authorities
对象的其他信息包含授权和用户个人信息等等
authenticate
可能抛出以下异常AuthenticationException
* DisabledException 账户禁用
* LockedException 账户锁定
* BadCredentialsException 密码错误
* UsernameNotFoundException 账户不存在
* 等等...
public interface AuthenticationManager {
/**
* Attempts to authenticate the passed {@link Authentication} object, returning a
* fully populated <code>Authentication</code> object (including granted authorities)
* if successful.
* <p>
* An <code>AuthenticationManager</code> must honour the following contract concerning
* exceptions:
* <ul>
* <li>A {@link DisabledException} must be thrown if an account is disabled and the
* <code>AuthenticationManager</code> can test for this state.</li>
* <li>A {@link LockedException} must be thrown if an account is locked and the
* <code>AuthenticationManager</code> can test for account locking.</li>
* <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are
* presented. Whilst the above exceptions are optional, an
* <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>
* </ul>
* Exceptions should be tested for and if applicable thrown in the order expressed
* above (i.e. if an account is disabled or locked, the authentication request is
* immediately rejected and the credentials testing process is not performed). This
* prevents credentials being tested against disabled or locked accounts.
* @param authentication the authentication request object
* @return a fully authenticated object including credentials
* @throws AuthenticationException if authentication fails
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
ProviderManager
providerManager
是AuthenticationManager
的具体实现,在providerManager
的authenticate
方法实现中,主要是遍历所有的AuthenticationProvider
的实现,通过provider.supports
方法识别当前传入的authentication
对象实现是否是当前provider
所支持的,如果不支持则跳过,直到找到一个匹配的
Class<? extends Authentication> toTest = authentication.getClass();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
......
try {
result = provider.authenticate(authentication);
if (result != null) {
....
}
}
catch (){
....
}
}
AuthenticationProvider
在Spring Security中,提供了多种认证方式,包括DAO认证,简单用户密码认证,三方认证,匿名认证等,AuthenticationProvider表示认证方式,可选多种认证方式,默认Spring Security中已经提供了多种实现,包括
DaoAuthenticationProvider
RemoteAuthenticationProvider
AnonymousAuthenticationProvider
TestingAuthenticationProvider
- 等等
在扩展DB
验证的时候我们常使用DaoAuthenticationProvider
,DaoAuthenticationProvider
看名字就知道跟数据库打交道用来校验识别登录认证信息的
DaoAuthenticationProvider
DaoAuthenticationProvider
中retrieveUser
方法主要通过
其最终目的,就是根据 UsernamePasswordAuthenticationToken
,获取到 username
,然后调用 UserDetailsService
检索用户详细信息。这里面就是我们熟悉的UserDetailsService
扩展服务
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//扩展实现
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);
}
}
肝就完事了!