Spring Security 架构与源码分析

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Spring Security 架构与源码分析

点击上方“Java知音”,选择“置顶公众号”

技术文章第一时间送达!

作者:JadePeng cnblogs.com/xiaoqi/p/spring-security.html

cnblogs.com/xiaoqi/p/spring-security.html

技术经验交流:

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定义:

Spring Security 架构与源码分析

在实际应用中,通常使用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? extends GrantedAuthority 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? extends Authentication 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,
                CollectionConfigAttribute 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:

    Spring Security 架构与源码分析

    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,
                CollectionConfigAttribute attributes) {
            if(authentication == null) {
                return ACCESS_DENIED;
            }
            int result = ACCESS_ABSTAIN;
    
            // 获取GrantedAuthority信息
            Collection? extends GrantedAuthority 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 架构与源码分析

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

    Spring Security 架构与源码分析

    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);
    
            ListFilter 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 ListFilter additionalFilters;
            private final FirewalledRequest firewalledRequest;
            private final int size;
            private int currentPosition = 0;
    
            private VirtualFilterChain(FirewalledRequest firewalledRequest,
                    FilterChain chain, ListFilter 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);
                }
            }
        }
    

    参考

    https://spring.io/guides/topicals/spring-security-architecture/ https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#overall-architecture

    阅****读

    1. 

    ****2. ****

    3. 

    ****4. ****

    看完本文有收获?请转发分享给更多人

    Spring Security 架构与源码分析

    原文始发于微信公众号(Java知音):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Spring Security 架构与源码分析


     上一篇
    【加精】别再问Spring Bean的生命周期了! 【加精】别再问Spring Bean的生命周期了!
    点击上方“后端技术精选”,选择“置顶公众号” 技术文章第一时间送达! 作者:sunshujie1990 www.jianshu.com/p/1dec08d290c1 www.jianshu.com/p/1dec08d290c
    2021-04-05
    下一篇 
    深入理解Spring中的各种注解 深入理解Spring中的各种注解
    点击上方“Java知音”,选择“置顶公众号” 技术文章第一时间送达! 作者:digdeep .cnblogs.com/digdeep/p/4525567.html .cnblogs.com/digdeep/
    2021-04-05