This article brings you an introduction to the principles of Spring Security (with code). It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
Only by knowing your enemy and knowing yourself can you be victorious in every battle. To use Spring Security to meet our needs, it is best to understand its principles so that you can expand at will. This article mainly records the basic operating process of Spring Security.
Filter
Spring Security basically uses filters to complete the configured identity authentication, permission authentication and logout.
Spring Security registers a filter FilterChainProxy in the Servlet's filter chain, which will proxy the request to multiple filter chains maintained by Spring Security itself. Each filter chain will match some URLs. If there is a match, the corresponding filter is executed. The filter chains are sequential, and only the first matching filter chain will be executed for a request. The configuration of Spring Security is essentially adding, deleting, and modifying filters.
By default, the 15 filters injected by the system for us correspond to different configuration requirements. Next, we focus on analyzing the UsernamePasswordAuthenticationFilter filter. It is a filter used for login authentication using username and password. However, in many cases, our login is not just a simple username and password, but may also use a third-party authorized login. , at this time we need to use a custom filter. Of course, I will not give a detailed explanation here. I will just talk about how to inject the custom filter.
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAfter(...); ... }
We need to understand a few basic concepts before starting the identity authentication process
SecurityContextHolder stores SecurityContext objects. SecurityContextHolder is a storage agent with three storage modes:
SecurityContextHolder uses MODE_THREADLOCAL mode by default, and SecurityContext is stored in the current thread. There is no need to pass explicit parameters when calling SecurityContextHolder. The SecurityContextHolder object can be obtained directly in the current thread.
SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication();
Authentication is verification, indicating who the current user is. What is verification? For example, a set of username and password is verification. Of course, wrong username and password are also verification, but Spring Security will fail the verification.
Authentication interface
public interface Authentication extends Principal, Serializable { //获取用户权限,一般情况下获取到的是用户的角色信息 Collection extends GrantedAuthority> getAuthorities(); //获取证明用户认证的信息,通常情况下获取到的是密码等信息,不过登录成功就会被移除 Object getCredentials(); //获取用户的额外信息,比如 IP 地址、经纬度等 Object getDetails(); //获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展) Object getPrincipal(); //获取当前 Authentication 是否已认证 boolean isAuthenticated(); //设置当前 Authentication 是否已认证 void setAuthenticated(boolean isAuthenticated); }
In fact, these three are easy to distinguish. AuthenticationManager is mainly to complete the identity authentication process. ProviderManager is the specific implementation of the AuthenticationManager interface. class, ProviderManager has a collection attribute provider that records the AuthenticationProvider object, and there are two methods in the AuthenticationProvider interface class
public interface AuthenticationProvider { //实现具体的身份认证逻辑,认证失败抛出对应的异常 Authentication authenticate(Authentication authentication) throws AuthenticationException; //该认证类是否支持该 Authentication 的认证 boolean supports(Class> authentication); }
The next step is to traverse the providers collection in ProviderManager to find and complete the identity authentication with the appropriate AuthenticationProvider.
4.UserDetailsService UserDetails
There is only one simple method in the UserDetailsService interface
public interface UserDetailsService { //根据用户名查到对应的 UserDetails 对象 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
What’s wrong with the above concept? To understand, we will analyze it slowly in the next process
When running to the UsernamePasswordAuthenticationFilter filter, first enter the doFilter() method of its parent class AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { ... //首先配对是不是配置的身份认证的URI,是则执行下面的认证,不是则跳过 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } ... Authentication authResult; try { //关键方法, 实现认证逻辑并返回 Authentication, 由其子类 UsernamePasswordAuthenticationFilter 实现, 由下面 5.3 详解 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //认证失败调用...由下面 5.1 详解 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //认证失败调用...由下面 5.1 详解 unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //认证成功调用...由下面 5.2 详解 successfulAuthentication(request, response, chain, authResult); }
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); ... rememberMeServices.loginFail(request, response); //该 handler 处理失败界面跳转和响应逻辑 failureHandler.onAuthenticationFailure(request, response, failed); }
The default failure handling handler configured here is SimpleUrlAuthenticationFailureHandler, which can be customized.
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler { ... public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //没有配置失败跳转的URL则直接响应错误 if (defaultFailureUrl == null) { logger.debug("No failure URL set, sending 401 Unauthorized error"); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { //否则 //缓存异常 saveException(request, exception); //根据配置的异常页面是重定向还是转发进行不同方式跳转 if (forwardToDestination) { logger.debug("Forwarding to " + defaultFailureUrl); request.getRequestDispatcher(defaultFailureUrl) .forward(request, response); } else { logger.debug("Redirecting to " + defaultFailureUrl); redirectStrategy.sendRedirect(request, response, defaultFailureUrl); } } } //缓存异常,转发则保存在request里面,重定向则保存在session里面 protected final void saveException(HttpServletRequest request, AuthenticationException exception) { if (forwardToDestination) { request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } else { HttpSession session = request.getSession(false); if (session != null || allowSessionCreation) { request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } } } }
Let’s do a small expansion here: use the system’s error handling handler to specify the URL to jump to when authentication fails. In the corresponding URL method in MVC, you can get the error information and feedback from the request or session through the key. For the front end
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { ... //这里要注意很重要,将认证完成返回的 Authentication 保存到线程对应的 `SecurityContext` 中 SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //该 handler 就是为了完成页面跳转 successHandler.onAuthenticationSuccess(request, response, authResult); }
The default success processing handler configured here is SavedRequestAwareAuthenticationSuccessHandler. The code inside will not be expanded in detail. Anyway, it jumps to the specified authentication success. The interface is customizable.
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 String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; ... //开始身份认证逻辑 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 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(); //先用前端提交过来的 username 和 password 封装一个简易的 AuthenticationToken UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //具体的认证逻辑还是交给 AuthenticationManager 对象的 authenticate(..) 方法完成,接着往下看 return this.getAuthenticationManager().authenticate(authRequest); } }
It is known from the source code breakpoint tracking that the final resolution is completed by the AuthenticationManager interface implementation class ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... private List<authenticationprovider> providers = Collections.emptyList(); ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { .... //遍历所有的 AuthenticationProvider, 找到合适的完成身份验证 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } ... try { //进行具体的身份验证逻辑, 这里使用到的是 DaoAuthenticationProvider, 具体逻辑记着往下看 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch ... } ... throw lastException; } }</authenticationprovider>
DaoAuthenticationProvider
Inherited from AbstractUserDetailsAuthenticationProvider
and implemented the AuthenticationProvider
interface
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { ... private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks(); private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks(); ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... // 获得提交过来的用户名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); //根据用户名从缓存中查找 UserDetails boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //缓存中没有则通过 retrieveUser(..) 方法查找 (看下面 DaoAuthenticationProvider 的实现) user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch ... } try { //比对前的检查,例如账户以一些状态信息(是否锁定, 过期...) preAuthenticationChecks.check(user); //子类实现比对规则 (看下面 DaoAuthenticationProvider 的实现) additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //根据最终user的一些信息重新生成具体详细的 Authentication 对象并返回 return createSuccessAuthentication(principalToReturn, authentication, user); } //具体生成还是看子类实现 protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // Ensure we return the original credentials the user supplied, // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } }
接下来我们来看下 DaoAuthenticationProvider 里面的三个重要的方法,比对方式、获取需要比对的 UserDetails 对象以及生产最终返回 Authentication 的方法。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { ... //密码比对 @SuppressWarnings("deprecation") protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); //通过 PasswordEncoder 进行密码比对, 注: 可自定义 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } //通过 UserDetailsService 获取 UserDetails protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //通过 UserDetailsService 获取 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); } } //生成身份认证通过后最终返回的 Authentication, 记录认证的身份信息 @Override 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); } }
本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的Java教程视频栏目!
The above is the detailed content of Introduction to Spring Security principles (with code). For more information, please follow other related articles on the PHP Chinese website!