文章目录
- 1. SecurityContextPersistenceFilter
- 2. UsernamePasswordAuthenticationFilter
- 3. ExceptionTranslationFilter
- 4. FilterSecurityInterceptor
SpringSecurity的核心是一条过滤器链(
1. SecurityContextPersistenceFilter
在用户请求成功认证后,会将认证成功后的上下文信息SecurityContext
通过该类存储在HttpSession
中。当用户下次进行请求时,可以直接从通过该类从HttpSession
中获取已经认证后的上下文信息SecurityContext
,从而避免重复认证。
先来看下该类的 doFilter() 方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 确保每个请求都要经过过滤器链认证
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
// 获取Spring Security上下文对象 SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
// 将 SecurityContext 中的信息保存到 HttpSession 中
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
最终调用的 HttpSessionSecurityContextRepository
的 saveContext()
方法
@Override
protected void saveContext(SecurityContext context) {
final Authentication authentication = context.getAuthentication();
HttpSession httpSession = request.getSession(false);
// See SEC-776
if (authentication == null || trustResolver.isAnonymous(authentication)) {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
}
if (httpSession != null && authBeforeExecution != null) {
// SEC-1587 A non-anonymous context may still be in the session
// SEC-1735 remove if the contextBeforeExecution was not anonymous
httpSession.removeAttribute(springSecurityContextKey);
}
return;
}
if (httpSession == null) {
httpSession = createNewSessionIfAllowed(context);
}
// If HttpSession exists, store current SecurityContext but only if it has
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
if (httpSession != null) {
// We may have a new session, so check also whether the context attribute
// is set SEC-1561
if (contextChanged(context)
|| httpSession.getAttribute(springSecurityContextKey) == null) {
// 将 SecurityContext 信息保存到 HttpSession 中
httpSession.setAttribute(springSecurityContextKey, context);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext '" + context
+ "' stored to HttpSession: '" + httpSession);
}
}
}
}
2. UsernamePasswordAuthenticationFilter
进行用户名和密码的认证。
UsernamePasswordAuthenticationFilter
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();
// 封装用户名和密码
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
// 调用 AuthenticationManager 接口的 authenticate()方法进行认证
// 实际上是调用它的实现类 ProviderManager类中的 authenticate()方法
return this.getAuthenticationManager().authenticate(authRequest);
}
主要内容
1.将用户名和密码封装成 UsernamePasswordAuthenticationToken
2.调用 ProviderManager 类中的 authenticate() 方法进行认证
ProviderManager
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();
/*
遍历所有的provider
SpringSecurity提供了许多AuthenticationProvider的实现类
用于处理各种认证
*/
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 找到处理该认证的provider类并进行认证
// 这里找到的是 AbstractUserDetailsAuthenticationProvider
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
...
}
主要内容
1.遍历所有的 AuthenticationProvider 接口的实现类,找到对应的provider类处理认证信息
AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从缓存中获取 username并封装成 UserDetails对象(实际上是它的实现类org.springframework.security.core.userdetails.User)
UserDetails user = this.userCache.getUserFromCache(username);
// 缓存中获取不到 UserDetails对象
if (user == null) {
cacheWasUsed = false;
try {
// 调用该方法获取 UserDetails对象,具体实现方法在
// DaoAuthenticationProvider中
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;
}
}
...
return createSuccessAuthentication(principalToReturn, authentication, user);
}
主要内容
1.先尝试从缓存中获取 UserDetails对象,UserDetails 接口有唯一实现类User
2.如果没有从缓存中获取到对象,则调用 DaoAuthenticationProvider类中的retrieveUser()获取 UserDetails对象
DaoAuthenticationProvider
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 调用 UserDetailsService接口中的 loadUserByUsername()方法获取 UserDetails 对象
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);
}
}
主要内容
1.调用了 UserDetailsService接口中的loadUserByUsername()方法实现认证
综上所述,我们只要自定义类实现UserDetailsService
接口,并重写其中的loadUserByUsername()
方法,就可以实现自己的认证逻辑。
再回到AbstractUserDetailsAuthenticationProvider
类的authenticate()
方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
...
// 创建 UsernamePasswordAuthenticationToken对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 使用构造器构造 UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
// 调用父类构造器
super(authorities);
// 用户名
this.principal = principal;
// 密码
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
return;
}
// 进行授权处理
for (GrantedAuthority a : authorities) {
if (a == null) {
throw new IllegalArgumentException(
"Authorities collection cannot contain any null elements");
}
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
}
总结:
最后获取到的 UsernamePasswordAuthenticationToken包含了用户名,密码,权限信息
注意:用户一定要被授予相关权限,否则会认证失败,后面的文章我还会提到
3. ExceptionTranslationFilter
用于在过滤器链中抛出AccessDeniedException
和AuthenticationException
异常,抛出的异常由AccessDeniedHandler
接口的实现类AccessDeniedHandlerImpl
处理
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
// 如果定义了异常页面
if (errorPage != null) {
request.setAttribute(WebAttributes.ACCESS_DENIED_403,
accessDeniedException);
// 设置响应码为403
response.setStatus(HttpStatus.FORBIDDEN.value());
// 跳转到错误页面
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
}
else {
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
}
主要内容
1.ExceptionTranslationFilter 用于在过滤器链中抛出AccessDeniedException 和 AuthenticationException异常,并交给 AccessDeniedHandler接口的实现类 AccessDeniedHandlerImpl处理
2.如果自定义了错误页面,AccessDeniedHandlerImpl会跳往该页面并设置响应码为403
4. FilterSecurityInterceptor
用于对http请求进行过滤
(引用了https://www.jb51.net/article/176217.htm)这里的解释
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//获取当前http请求的地址,比如说“/login”
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 这里做主要URL比对,将当前URL与securityMetadataSource(我们自己配置)中的URL过滤条件进行比对
// 首先判断当前URL是permit的还是需要验证的
// 若需要验证,尝试加载保存在SecurityContext类中的已登录信息
// 调用 AbstractSecurityInterceptor中的 AccessDecisionManager对象的decide方法
// 如果对于配置中需要登录才可访问的URL,已经查找到登录信息,则执行下一个Filter
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
这里大家可能有疑问,用户信息为什么保存在SecurityContext
类中,我们再回到UsernamePasswordAuthenticationFilter
类中,它是一个Filter
类,那么肯定有doFilter()
方法对请求进行过滤,该方法继承自它的父类AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 该方法在前面具体讲了
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 认证失败
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// 认证失败
unsuccessfulAuthentication(request, response, failed);
return;
}
// 认证成功,继续沿着Filter链执行
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 最终认证成功
successfulAuthentication(request, response, chain, authResult);
}
主要内容
1.UsernamePasswordAuthenticationFilter调用了父类AbstractAuthenticationProcessingFilter中的doFilter()方法进行过滤,如果认证失败,则调用unsuccessfulAuthentication()方法,如果认证成功,则调用successfulAuthentication()方法
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
// 将认证成功后的用户信息保存在SecurityContext类中
SecurityContextHolder.getContext().setAuthentication(authResult);
// 实现记住我功能
rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
主要内容
1.在successfulAuthentication()方法中,通过SecurityContextHolder.getContext().setAuthentication(authResult)这个方法将认证成功后的用户信息保存在了SecurityContext类中