千家信息网

Spring Security架构以及源码的示例分析

发表于:2025-01-18 作者:千家信息网编辑
千家信息网最后更新 2025年01月18日,这篇文章给大家分享的是有关Spring Security架构以及源码的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。前言现在流行的通用授权框架有apache的shi
千家信息网最后更新 2025年01月18日Spring Security架构以及源码的示例分析

这篇文章给大家分享的是有关Spring Security架构以及源码的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

前言

现在流行的通用授权框架有apache的shiro和Spring家族的Spring Security,在涉及今天的微服务鉴权时,需要利用我们的授权框架搭建自己的鉴权服务,今天总理了Spring Security。

Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。

核心对象

主要代码在spring-security-core包下面。要了解Spring Security,需要先关注里面的核心对象。

SecurityContextHolder, SecurityContext 和 Authentication

SecurityContextHolder 是 SecurityContext的存放容器,默认使用ThreadLocal 存储,意味SecurityContext在相同线程中的方法都可用。

SecurityContext主要是存储应用的principal信息,在Spring Security中用Authentication 来表示。

获取principal:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();if (principal instanceof UserDetails) {String username = ((UserDetails)principal).getUsername();} else {String username = principal.toString();}

在Spring Security中,可以看一下Authentication定义:

public interface Authentication extends Principal, Serializable { Collection getAuthorities(); /** * 通常是密码 */ Object getCredentials(); /** * Stores additional details about the authentication request. These might be an IP * address, certificate serial number etc. */ Object getDetails(); /** * 用来标识是否已认证,如果使用用户名和密码登录,通常是用户名  */ Object getPrincipal(); /** * 是否已认证 */ boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}

在实际应用中,通常使用UsernamePasswordAuthenticationToken:

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { }public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {}

一个常见的认证过程通常是这样的,创建一个UsernamePasswordAuthenticationToken,然后交给authenticationManager认证(后面详细说明),认证通过则通过SecurityContextHolder存放Authentication信息。

 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());Authentication authentication = this.authenticationManager.authenticate(authenticationToken);SecurityContextHolder.getContext().setAuthentication(authentication);

UserDetails与UserDetailsService

UserDetails 是Spring Security里的一个关键接口,他用来表示一个principal。

public interface UserDetails extends Serializable { /** * 用户的授权信息,可以理解为角色 */ Collection getAuthorities(); /** * 用户密码 * * @return the password */ String getPassword(); /** * 用户名  * */ String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();}

UserDetails提供了认证所需的必要信息,在实际使用里,可以自己实现UserDetails,并增加额外的信息,比如email、mobile等信息。

在Authentication中的principal通常是用户名,我们可以通过UserDetailsService来通过principal获取UserDetails:

public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}

GrantedAuthority

在UserDetails里说了,GrantedAuthority可以理解为角色,例如 ROLE_ADMINISTRATOR or ROLE_HR_SUPERVISOR。

小结

  • SecurityContextHolder, 用来访问 SecurityContext.

  • SecurityContext, 用来存储Authentication .

  • Authentication, 代表凭证.

  • GrantedAuthority, 代表权限.

  • UserDetails, 用户信息.

  • UserDetailsService,获取用户信息.

Authentication认证

AuthenticationManager

实现认证主要是通过AuthenticationManager接口,它只包含了一个方法:

public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException;}

authenticate()方法主要做三件事:

  • 如果验证通过,返回Authentication(通常带上authenticated=true)。

  • 认证失败抛出AuthenticationException

  • 如果无法确定,则返回null

AuthenticationException是运行时异常,它通常由应用程序按通用方式处理,用户代码通常不用特意被捕获和处理这个异常。

AuthenticationManager的默认实现是ProviderManager,它委托一组AuthenticationProvider实例来实现认证。
AuthenticationProvider和AuthenticationManager类似,都包含authenticate,但它有一个额外的方法supports,以允许查询调用方是否支持给定Authentication类型:

public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class authentication);}

ProviderManager包含一组AuthenticationProvider,执行authenticate时,遍历Providers,然后调用supports,如果支持,则执行遍历当前provider的authenticate方法,如果一个provider认证成功,则break。

public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using "  + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) {  copyDetails(authentication, result);  break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException e) { lastException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication  && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } eventPublisher.publishAuthenticationSuccess(result); return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage(  "ProviderManager.providerNotFound",  new Object[] { toTest.getName() },  "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; }

从上面的代码可以看出, ProviderManager有一个可选parent,如果parent不为空,则调用parent.authenticate(authentication)

AuthenticationProvider

AuthenticationProvider有多种实现,大家最关注的通常是DaoAuthenticationProvider,继承于AbstractUserDetailsAuthenticationProvider,核心是通过UserDetails来实现认证,DaoAuthenticationProvider默认会自动加载,不用手动配。

先来看AbstractUserDetailsAuthenticationProvider,看最核心的authenticate:

public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 必须是UsernamePasswordAuthenticationToken Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage(  "AbstractUserDetailsAuthenticationProvider.onlySupports",  "Only UsernamePasswordAuthenticationToken is supported")); // 获取用户名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 从缓存获取 UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // retrieveUser 抽象方法,获取用户 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; } }  Assert.notNull(user,  "retrieveUser returned null - a violation of the interface contract"); } try { // 预先检查,DefaultPreAuthenticationChecks,检查用户是否被lock或者账号是否可用 preAuthenticationChecks.check(user);  // 抽象方法,自定义检验 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; } }  // 后置检查 DefaultPostAuthenticationChecks,检查isCredentialsNonExpired postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); }  return createSuccessAuthentication(principalToReturn, authentication, user); }

上面的检验主要基于UserDetails实现,其中获取用户和检验逻辑由具体的类去实现,默认实现是DaoAuthenticationProvider,这个类的核心是让开发者提供UserDetailsService来获取UserDetails以及 PasswordEncoder来检验密码是否有效:

private UserDetailsService userDetailsService;private PasswordEncoder passwordEncoder;

看具体的实现,retrieveUser,直接调用userDetailsService获取用户:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,  presentedPassword, null); } throw notFound; } catch (Exception repositoryProblem) { throw new InternalAuthenticationServiceException(  repositoryProblem.getMessage(), repositoryProblem); } if (loadedUser == null) { throw new InternalAuthenticationServiceException(  "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }

再来看验证:

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } 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后的密码是否和userdetails的密码一致 if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage(  "AbstractUserDetailsAuthenticationProvider.badCredentials",  "Bad credentials")); } }

小结:要自定义认证,使用DaoAuthenticationProvider,只需要为其提供PasswordEncoder和UserDetailsService就可以了。

定制 Authentication Managers

Spring Security提供了一个Builder类AuthenticationManagerBuilder,借助它可以快速实现自定义认证。

看官方源码说明:

SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider's.

AuthenticationManagerBuilder可以用来Build一个AuthenticationManager,可以创建基于内存的认证、LDAP认证、 JDBC认证,以及添加UserDetailsService和AuthenticationProvider。

简单使用:

@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)public class ApplicationSecurity extends WebSecurityConfigurerAdapter { public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userDetailsService = userDetailsService; this.tokenProvider = tokenProvider; this.corsFilter = corsFilter; this.problemSupport = problemSupport; } @PostConstruct public void init() { try { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } catch (Exception e) { throw new BeanInitializationException("Security configuration failed", e); } } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(problemSupport) .accessDeniedHandler(problemSupport) .and() .csrf() .disable() .headers() .frameOptions() .disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/register").permitAll() .antMatchers("/api/activate").permitAll() .antMatchers("/api/authenticate").permitAll() .antMatchers("/api/account/reset-password/init").permitAll() .antMatchers("/api/account/reset-password/finish").permitAll() .antMatchers("/api/profile-info").permitAll() .antMatchers("/api/**").authenticated() .antMatchers("/management/health").permitAll() .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/v2/api-docs/**").permitAll() .antMatchers("/swagger-resources/configuration/ui").permitAll() .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN) .and() .apply(securityConfigurerAdapter()); }}

授权与访问控制

一旦认证成功,我们可以继续进行授权,授权是通过AccessDecisionManager来实现的。框架有三种实现,默认是AffirmativeBased,通过AccessDecisionVoter决策,有点像ProviderManager委托给AuthenticationProviders来认证。

public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException { int deny = 0; // 遍历DecisionVoter  for (AccessDecisionVoter voter : getDecisionVoters()) { // 投票 int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } }  // 一票否决 if (deny > 0) { throw new AccessDeniedException(messages.getMessage(  "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }

来看AccessDecisionVoter:

boolean supports(ConfigAttribute attribute);boolean supports(Class clazz);int vote(Authentication authentication, S object, Collection attributes);

object是用户要访问的资源,ConfigAttribute则是访问object要满足的条件,通常payload是字符串,比如ROLE_ADMIN 。所以我们来看下RoleVoter的实现,其核心就是从authentication提取出GrantedAuthority,然后和ConfigAttribute比较是否满足条件。

public boolean supports(ConfigAttribute attribute) { if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getRolePrefix())) { return true; } else { return false; } } public boolean supports(Class clazz) { return true; }public int vote(Authentication authentication, Object object, Collection attributes) { if(authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN;  // 获取GrantedAuthority信息 Collection authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { // 默认拒绝访问 result = ACCESS_DENIED; // Attempt to find a matching granted authority for (GrantedAuthority authority : authorities) {  // 判断是否有匹配的 authority  if (attribute.getAttribute().equals(authority.getAuthority())) {  // 可访问  return ACCESS_GRANTED;  } } } } return result; }

这里要疑问,ConfigAttribute哪来的?其实就是上面ApplicationSecurity的configure里的。

web security 如何实现

Web层中的Spring Security(用于UI和HTTP后端)基于Servlet Filters,下图显示了单个HTTP请求的处理程序的典型分层。

Spring Security通过FilterChainProxy作为单一的Filter注册到web层,Proxy内部的Filter。

FilterChainProxy相当于一个filter的容器,通过VirtualFilterChain来依次调用各个内部filter

public void doFilter(ServletRequest request, ServletResponse response,  FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) {  try {  request.setAttribute(FILTER_APPLIED, Boolean.TRUE);  doFilterInternal(request, response, chain);  }  finally {  SecurityContextHolder.clearContext();  request.removeAttribute(FILTER_APPLIED);  } } else {  doFilterInternal(request, response, chain); } } private void doFilterInternal(ServletRequest request, ServletResponse response,  FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall  .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall  .getFirewalledResponse((HttpServletResponse) response); List filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) {  if (logger.isDebugEnabled()) {  logger.debug(UrlUtils.buildRequestUrl(fwRequest)   + (filters == null ? " has no matching filters"    : " has an empty filter list"));  }  fwRequest.reset();  chain.doFilter(fwRequest, fwResponse);  return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); }  private static class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List additionalFilters; private final FirewalledRequest firewalledRequest; private final int size; private int currentPosition = 0; private VirtualFilterChain(FirewalledRequest firewalledRequest,  FilterChain chain, List additionalFilters) {  this.originalChain = chain;  this.additionalFilters = additionalFilters;  this.size = additionalFilters.size();  this.firewalledRequest = firewalledRequest; } public void doFilter(ServletRequest request, ServletResponse response)  throws IOException, ServletException {  if (currentPosition == size) {  if (logger.isDebugEnabled()) {   logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)    + " reached end of additional filter chain; proceeding with original chain");  }  // Deactivate path stripping as we exit the security filter chain  this.firewalledRequest.reset();  originalChain.doFilter(request, response);  }  else {  currentPosition++;  Filter nextFilter = additionalFilters.get(currentPosition - 1);  if (logger.isDebugEnabled()) {   logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)    + " at position " + currentPosition + " of " + size    + " in additional filter chain; firing Filter: '"    + nextFilter.getClass().getSimpleName() + "'");  }  nextFilter.doFilter(request, response, this);  } } }

感谢各位的阅读!关于"Spring Security架构以及源码的示例分析"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

0