Cet article vous apporte une introduction aux principes de Spring Security (avec code). Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il vous sera utile.
Ce n'est qu'en connaissant votre ennemi et en vous connaissant vous-même que vous pourrez gagner chaque bataille. Pour utiliser Spring Security pour répondre à nos besoins, il est préférable de comprendre ses principes afin de pouvoir développer à volonté. processus opérationnel de Spring Security.
Filtre
Spring Security utilise essentiellement des filtres pour compléter l'authentification d'identité configurée, l'authentification des autorisations et la déconnexion.
Spring Security enregistre un filtre FilterChainProxy dans la chaîne de filtres du servlet, qui transmettra la requête à plusieurs chaînes de filtres gérées par Spring Security lui-même. Chaque chaîne de filtres correspondra à certaines URL. S'il y a une correspondance, le filtre correspondant. est exécuté. Les chaînes de filtres sont séquentielles et seule la première chaîne de filtres correspondante sera exécutée pour une requête. La configuration de Spring Security consiste essentiellement à ajouter, supprimer et modifier des filtres.
Par défaut, les 15 filtres injectés par le système pour nous correspondent à différents besoins de configuration. Ensuite, nous nous concentrons sur l'analyse du filtre UsernamePasswordAuthenticationFilter. Il s'agit d'un filtre utilisé pour l'authentification de connexion à l'aide d'un nom d'utilisateur et d'un mot de passe. Cependant, dans de nombreux cas, notre connexion n'est pas simplement un simple nom d'utilisateur et un mot de passe, mais peut également utiliser une connexion autorisée par un tiers. , à ce stade, nous devons utiliser un filtre personnalisé. Bien sûr, je ne l'expliquerai pas en détail ici, je parlerai simplement de la façon d'injecter le filtre personnalisé.
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAfter(...); ... }
Nous devons comprendre quelques concepts de base avant de commencer le processus d'authentification d'identité
SecurityContextHolder stocke les objets SecurityContext. SecurityContextHolder est un agent de stockage avec trois modes de stockage :
SecurityContextHolder utilise le mode MODE_THREADLOCAL par défaut et SecurityContext est stocké dans le thread actuel. Il n'est pas nécessaire de transmettre des paramètres explicites lors de l'appel de SecurityContextHolder. L'objet SecurityContextHolder peut être obtenu directement dans le thread actuel.
SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication();
L'authentification est une vérification, indiquant qui est l'utilisateur actuel. Qu'est-ce que la vérification ? Par exemple, un ensemble de nom d'utilisateur et de mot de passe est une vérification. Bien sûr, un nom d'utilisateur et un mot de passe incorrects sont également une vérification, mais Spring Security échouera à la vérification.
Interface d'authentification
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); }
En fait, ces trois éléments sont faciles à distinguer. AuthenticationManager sert principalement à compléter le processus d'authentification d'identité. l'interface de AuthenticationManager Classe d'implémentation spécifique, ProviderManager a un fournisseur d'attributs de collection qui enregistre l'objet AuthenticationProvider. Il existe deux méthodes dans la classe d'interface AuthenticationProvider
public interface AuthenticationProvider { //实现具体的身份认证逻辑,认证失败抛出对应的异常 Authentication authenticate(Authentication authentication) throws AuthenticationException; //该认证类是否支持该 Authentication 的认证 boolean supports(Class> authentication); }
L'étape suivante consiste à parcourir la collection des fournisseurs dans ProviderManager pour. recherchez et complétez l’identité avec la certification AuthenticationProvider appropriée.
4.UserDetailsService UserDetails
Il n'y a qu'une seule méthode simple dans l'interface UserDetailsService
public interface UserDetailsService { //根据用户名查到对应的 UserDetails 对象 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
Pour ce qui précède. concepts, il y en a Si vous ne comprenez rien, nous l'analyserons lentement dans le processus suivant
Lors de l'exécution du filtre UsernamePasswordAuthenticationFilter, vous entrez d'abord la méthode doFilter() de sa classe parent 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); }
Le gestionnaire de gestion des échecs par défaut configuré ici est SimpleUrlAuthenticationFailureHandler, qui peut être personnalisé.
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); } } } }
Voici une petite extension : utilisez le gestionnaire de gestion des erreurs du système pour spécifier l'URL vers laquelle accéder si l'authentification échoue. Dans la méthode URL correspondante dans MVC, vous pouvez obtenir les informations d'erreur de la requête ou. session via la clé. , retour au 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); }
Le gestionnaire de traitement de réussite par défaut configuré ici est SavedRequestAwareAuthenticationSuccessHandler Le code à l'intérieur ne sera pas développé en détail. Quoi qu'il en soit, il passe à l'interface spécifiée. Une fois l'authentification réussie, elle peut être personnalisée.
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); } }
Il ressort du suivi des points d'arrêt du code source que l'analyse finale est effectuée par la classe d'implémentation de l'interface AuthenticationManager 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
Hérite de AbstractUserDetailsAuthenticationProvider
et implémente l'interface AuthenticationProvider
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教程视频栏目!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!