原理
Spring Security是一个提供了认证、授权和防止常见攻击的框架
提供了很多基础的过滤器,当客户端发起请求的时候,经过容器已经创建的过滤器。下面是图片举例
再来看一下流程
源码分析
- 用户发起http请求首先会登陆,先来配置登陆
- 第一个过滤器 UsernamePasswordAuthenticationFilter 继承AbstractAuthenticationProcessingFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 如果不是登陆验证的请求直接放行
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
try {
// 登录验证的核心方法,具体实现是在UsernamePasswordAuthenticationFilter
Authentication authenticationResult = this.attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 验证成功后的处理逻辑
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
this.unsuccessfulAuthentication(request, response, var5);
} catch (AuthenticationException var6) {
// 验证失败后的处理逻辑
this.unsuccessfulAuthentication(request, response, var6);
}
}
}
- 说下验证成功之后的逻辑
// 登陆成功
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
// 登录失败
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// SecurityContextHolder 他创建了一个线程池保存用户信息,后面可以直接获取用户信息
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// 这里是记住登录状态的功能
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 登录成功后的相关处理,例如跳转到首页
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
- 成功和失败的逻辑
看一下登录成功SavedRequestAwareAuthenticationSuccessHandler类中的onAuthenticationSuccess方法
它在用户成功完成身份验证后被调用。这个方法有两个参数:request 和 response,分别代表 HTTP 请求和响应。在 onAuthenticationSuccess 方法中,可以让redis保存用户信息以及生产一串token返回给前端。
可以通过实现 AuthenticationSuccessHandler 接口并覆盖 onAuthenticationSuccess 方法来自定义成功身份验证后的行为。
可以通过实现 AuthenticationFailureHandler 接口并覆盖 onAuthenticationFailure 方法来自定义身份验证失败后的行为。
- 再来看一下身份验证的处理逻辑
在UsernamePasswordAuthenticationFilter 类中继承AbstractAuthenticationProcessingFilter
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());
} else {
String username = this.obtainUsername(request);
username = username != null ? username.trim() : "";
String password = this.obtainPassword(request);
password = password != null ? password : "";
// 根据用户名和密码生成一个身份认证令牌,其实就是保存一些用户登录的信息
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
// 调用这个AuthenticationManager接口类中的authenticate验证接口
// 把令牌传到 AuthenticationManager 进行验证,我们
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- 来看一下验证的
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 用户信息
private final Object principal;
// 用户凭证
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
// 下面两行是权限信息
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
// 第一次验证时调用此方法
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
// 已经验证过了,可以调用此方法
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
- 返回到下面这里,继续看getAuthenticationManager
// 调用这个AuthenticationManager接口类中的authenticate验证接口
// 把令牌传到 AuthenticationManager 进行验证,我们
return this.getAuthenticationManager().authenticate(authRequest);
发现只是个接口,实现是在ProviderManager
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
springsecurity会提供许多AuthenticationProvider,而ProviderManager这个类就处理这些AuthenticationProvider
- 用于管理身份验证(Authentication)和授权(Authorization)提供者的集合。
- 它可以帮助组织、调用和协调多个身份验证和授权提供者,以便完成身份验证和授权的过程。
- ProviderManager 可以根据配置决策选择哪个身份验证或授权提供者
- 如果一个提供者无法成功完成认证或授权,它会尝试使用其他提供者来处理请求。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
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();
Iterator var9 = this.getProviders().iterator();
//在容器启动时,springsecurity已经初始化了一些provider,其中就有DaoAuthenticationProvider,其主要是用户名和密码验证的
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
//如果provider不支持UsernamePasswordAuthenticationToken,就跳到下一个provider
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
// DaoAuthenticationProvider就是这个provider
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
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;
}
}
}
上面这个类的内容不是重点,重点是这个DaoAuthenticationProvider,而它又是继承AbstractUserDetailsAuthenticationProvider这个抽象类,因此我们先来了解一下它
- AbstractUserDetailsAuthenticationProvider提供了一些通用的身份验证逻辑
- 但是需要具体的子类来实现其中的一些关键功能。
- 通常会使用DaoAuthenticationProvider等具体的子类来完成身份验证任务。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
// 根据用户名从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 调用实现类的DaoAuthenticationProvider的retrieveUser,这里会得到保存在内存中的用户信息,如密码,账号状态等
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw var6;
}
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// 判断用户状态,是否是启用
this.preAuthenticationChecks.check(user);
// 密码比较
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建一个成功的Authentication对象
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
}
身份验证 DaoAuthenticationProvider
- 它提供了一种基于用户名和密码进行身份验证的机制。
- 它通过UserDetailsService从数据源(如数据库)中获取用户信息
- 使用PasswordEncoder对用户提供的密码进行加密后与数据库中存储的密码进行比较
- 如果验证成功,则DaoAuthenticationProvider会返回一个包含用户详细信息的Authentication对象;
- 否则会抛出相应的异常。
- 我们可以根据需要配置DaoAuthenticationProvider的各个方面,如加密算法、是否启用账户锁定等。
因此后面我们在做手机登录的时候,就可以自定义一个provider,然后调用一个根据手机号查询用户信息的接口即可。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
// 最终实现查询用户信息的代码在这里
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
}
UserDetailsService 配置查询逻辑
public interface UserDetailsService {
// 通过用户名获取用户信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
通过用户名获取用户信息的方法是被下面这个实现类所实现的
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从缓存中获取用户信息
UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
// 返回一个UserDetails
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
}
}
springSecurity获取用户信息,默认从缓存中换取的,然后返回一个UserDetails,我们看看其定义的User类,下面只是列出用部分
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
@Override
//权限
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
}
}
用户标识
当登录成功后,再次发送请求时,springsecurity时如何知道该用户已经通过验证的?
这时我们就要认识一下SecurityContextPersistenceFilter
它的作用主要是: 创建SecurityContext安全上下文信息和请求结束时清空SecurityContextHolder
- 用于在每个请求之间持久化 SecurityContext。
- 在用户进行身份验证后,Spring Security 将创建一个包含认证信息的 SecurityContext 对象。
- 该对象会被存储在当前线程的 ThreadLocal 中。
- 如果不使用 SecurityContextPersistenceFilter 这个过滤器,则在调用链中传递 SecurityContext 是非常困难的,并且可能导致安全问题。
SecurityContextPersistenceFilter 的作用是将当前线程中的 SecurityContext 对象持久化到适当的存储介质中(比如 HttpSession 或者数据库),以便在下一次请求时可以重新加载并再次使用。这样,在整个调用链中,即使跨多个请求处理程序,也可以始终访问相同的 SecurityContext 对象,从而实现身份验证和授权功能。
public class SecurityContextPersistenceFilter extends GenericFilterBean {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
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 {
request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
if (this.forceEagerSessionCreation) {
// 创建一个HttpSession
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 获取登录用户的相关信息,如果没有登录,用户信息为空
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var10 = false;
try {
var10 = true;
// SecurityContextHolder绑定SecurityContext对象
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
this.logger.debug("Set SecurityContextHolder to empty SecurityContext");
} else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
// 下一个过滤器处理
chain.doFilter(holder.getRequest(), holder.getResponse());
var10 = false;
} finally {
if (var10) {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 在任何其他操作之前删除SecurityContextHolder内容
SecurityContextHolder.clearContext();
// 重新保存用户信息
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
}