SpringSecurity 原理初探(三) 1、HttpSecurity 常用配置
**authorizeRequests()**:这是配置请求授权规则的入口点。通常,你会以 authorizeRequests()
方法开始配置,然后定义哪些请求需要进行身份验证和授权。
**antMatchers()**:使用 antMatchers()
方法定义需要授权的 URL 模式。例如,.antMatchers("/admin/**").hasRole("ADMIN")
表示任何以 “/admin/“ 开头的 URL 需要用户具有 “ADMIN” 角色才能访问。
**permitAll()**:permitAll()
方法用于配置允许所有用户访问的请求,即不需要身份验证。
**authenticated()**:authenticated()
方法指示所有已经通过身份验证的用户都可以访问,无需特定的角色或权限。
hasRole() 和 **hasAuthority()**:这些方法用于配置需要具有特定角色或权限的用户才能访问的请求。例如,.hasRole("USER")
表示需要用户具有 “USER” 角色。
**formLogin()**:formLogin()
方法启用基于表单的身份验证。它会自动生成登录页面和处理登录请求。
**loginPage()**:loginPage()
方法允许你指定自定义的登录页面的 URL。
**loginProcessingUrl()**:loginProcessingUrl()
方法指定处理登录请求的 URL。
**failureUrl()**:failureUrl()
方法定义登录失败后重定向到的 URL。
**logout()**:logout()
方法配置登出功能,可以定义登出 URL 和登出成功后的重定向 URL。
**rememberMe()**:rememberMe()
方法配置 “记住我” 功能,允许用户在下一次访问时保持登录状态。
**csrf()**:csrf()
方法配置跨站请求伪造(CSRF)保护,可以启用或禁用 CSRF 保护。
**sessionManagement()**:sessionManagement()
方法用于配置会话管理策略,例如最大会话数和会话过期策略。
**exceptionHandling()**:exceptionHandling()
方法允许配置身份验证和授权失败时的处理方式,例如重定向到自定义错误页面或返回特定错误响应。
2、登录认证流程
通过自定义SpringSecurity,在内存中存放一个用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override protected void configure (HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and().formLogin(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin" ) .password("{noop}123456" ) .authorities(new ArrayList <>()); }
大致流程概述:在登录认证时,请求会先进入过滤器,再使用Provider认证处理,最后进行成功/失败处理器处理
2.1、AbstractAuthenticationProcessingFilter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { .... private void doFilter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!this .requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { try { 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); } } } .... }
==UsernamePasswordAuthenticationFilter== 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 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 authRequest = new UsernamePasswordAuthenticationToken (username, password); this .setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest); } } } public UsernamePasswordAuthenticationToken (Object principal, Object credentials) { super (null ); this .principal = principal; this .credentials = credentials; setAuthenticated(false ); }
2.2、ProviderManager AuthenticationManager 身份验证管理器 接口的实现类,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 public class ProviderManager implements AuthenticationManager { @Override 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(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue ; } if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)" , provider.getClass().getSimpleName(), ++currentPosition, size)); } try { result = provider.authenticate(authentication); if (result != null ) { copyDetails(authentication, result); break ; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); throw ex; } catch (AuthenticationException ex) { lastException = ex; } } if (result == null && this .parent != null ) { try { parentResult = this .parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException ex) { } catch (AuthenticationException ex) { parentException = ex; lastException = ex; } } if (result != null ) { if (this .eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { ((CredentialsContainer) result).eraseCredentials(); } if (parentResult == null ) { this .eventPublisher.publishAuthenticationSuccess(result); } return result; } if (lastException == null ) { lastException = new ProviderNotFoundException (this .messages.getMessage("ProviderManager.providerNotFound" , new Object [] { toTest.getName() }, "No AuthenticationProvider found for {0}" )); } if (parentException == null ) { prepareException(lastException, authentication); } throw lastException; } }
这里循环所有的 provider
,每个provider都调用自己的 supports()
方法判断是否支持认证,能支持认证的话就执行自己的 authenticate()
方法进行认证
左边的 ProviderManager
就是这些策略的委托类,所有的provider都会被收集到该类的 providers
属性中,然后认证的时候由委托类循环所有策略,支持认证的话再调用对应的策略去认证。
最后会调用DaoAuthenticationProvider中的authenticate认证方法
2.3、AbstractUserDetailsAuthenticationProvider DaoAuthenticationProvider的父类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider , InitializingBean, MessageSourceAware { @Override public Authentication authenticate (Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this .messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports" , "Only UsernamePasswordAuthenticationToken is supported" )); String username = determineUsername(authentication); boolean cacheWasUsed = true ; UserDetails user = this .userCache.getUserFromCache(username); if (user == null ) { cacheWasUsed = false ; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this .logger.debug("Failed to find user '" + username + "'" ); if (!this .hideUserNotFoundExceptions) { throw ex; } 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); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } cacheWasUsed = false ; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); this .preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } this .postAuthenticationChecks.check(user); if (!cacheWasUsed) { this .userCache.putUserInCache(user); } Object principalToReturn = user; if (this .forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); } }
校验密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected void additionalAuthenticationChecks (UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null ) { this .logger.debug("Failed to authenticate since no credentials provided" ); throw new BadCredentialsException (this .messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } String presentedPassword = authentication.getCredentials().toString(); if (!this .passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this .logger.debug("Failed to authenticate since password does not match stored value" ); throw new BadCredentialsException (this .messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } }
最终会调用DelegatingPasswordEncoder中的matches方法校验
封装返回值,并更新内存中的密码,在第二次请求中内存中的密码返回的是加密后的密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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); }
==DaoAuthenticationProvider== 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @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); } } }
2.4、InMemoryUserDetailsManager 1 2 3 4 5 6 7 8 9 10 public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { UserDetails user = this .users.get(username.toLowerCase()); if (user == null ) { throw new UsernameNotFoundException (username); }对象返回 return new User (user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities()); }
内存中的用户信息
2.5、总结 通过fromLogin这种方式登录时会在UsernamePasswordAuthenticationFilter 过滤器中进行身份认证
在处理登录认证时会将前端的用户信息封装成一个UsernamePasswordAuthenticationToken对象传给ProviderManager中的authenticate()方法
在这个方法中会循环每provider,每个provider都调用自己的 supports()
方法判断是否支持认证,能支持认证的话就执行自己的 authenticate()
方法进行认证
通过循环后找到DaoAuthenticationProvider策略类
然后会调用AbstractUserDetailsAuthenticationProvider(DaoAuthenticationProvider的父类)的authenticate方法在这个方法中会将封装的用户信息传递给子类的retrieveUser方法,调用子类的方法进行处理
调用DaoAuthenticationProvider类的retrieveUser方法,然后调用InMemoryUserDetailsManager类中loadUserByUsername方法,通过username从内存中查询用户信息,
从内存中获取的用户信息会封装成一个UserDetails对象最后会返回给AbstractUserDetailsAuthenticationProvider
在AbstractUserDetailsAuthenticationProvider的authenticatn()方法中,会对UserDetails进行检查,判断用户是否被锁定,是否被禁用,调用additionalAuthenticationChecks方法对密码进行校验,最后会把返回得UserDetails放入缓存中,最后调用createSuccessAuthentication方法,如果内存中得密码是明文的话,会将密码进行加密处理修改到内存中,最后封装成一个UsernamePasswordAuthenticationToken对象返回
DelegatingPasswordEncoder默认的密码编码器,委托密码编码器,在matches方法中会匹配内存中密码带有{ 前缀 },根据前缀获得对应的编码器进行处理,比如在内存中的密码是以{noop}的前缀,则会从map集合中根据noop 这个关键字得到NoOpPasswordEncoder编码器,这个编码器不会对密码进行处理
最后如果认证成功就会调用成功处理器将内存中的用户信息放到SecurityContextHolder中,并进行页面重定向