Spring Security07 Spring Security核心API

前言

之前几个章节,基本都属于达到了会用的层面,但是为什么这么用,这又是另一个问题层面了,对于技术学习来说,我们追求的不仅要 "知其然" ,更要 "知其所以然" 。

本篇带大家来了解剖析Spring Security核心Api以及内部源码,实现认证授权的具体过程。

Spring Security认证请求完整流程图

先借用网上的一张图片,先让各位对认证授权有个大致的了解,下图展示了从发起一个web请求,到经过内存或数据库层面的查询,最后再得到整个用户认证信息的全过程。


image.png

在图中涉及到Spring Security关羽认证授权的众多核心API,本文就是对这些核心API进行分析讲解。

SecurityContextHolder、SecurityContext、Authentication

1.1、Authentication类关系
Authentication是spring-security-core核心包中的接口,直接继承自Principal接口,而Principal位于java.security包中,可以知道Authentication是Spring Security的核心接口。Authentication接口封装了用户信息,有个很重要的子类UsernamePasswordAuthenticationToken,先记住。
1.2、Authentication功能
用户登录认证之前,用户名密码等信息会被封装为一个Authentication的具体实现类对象。登录认证成功后,又会生成一个信息更全面的Authentication对象。
从这个对象可以得到用户的权限信息列表、密码、用户细节信息、用户身份信息、认证信息等。该Authentication对象会被保存在SecurityContextHolder所持有的SecurityContext中,方便后续程序调用。
SecurityContextHolder 、SecurityContext 、Authentication三者的关系
SecurityContextHolder 包含SecurityContext 包含Authentication

image.png

1.3、Authentication核心方法
image.png

利用上面这些方法就可以获取到关于用户相关的信息,比如用户名、密码、角色等。

2.1、SecurityContext
安全上下文,该类主要存储认证授权的相关信息,实际上就是存储"当前用户"的账号信息和相关权限,即代表当前用户信息的 Authentication 对象会被 SecurityContext 所持有(引用),SecurityContext上下文对象则被SecurityContextHolder 所持有(引用)。
SecurityContext源码如下

image.png

从源码可以看到,利用SecurityContext的getAuthentication()方法可以拿到用户信息。
2.2、获取当前用户信息
有个问题,如果在自己的项目中,如何获取当前已登录的用户信息呢?
因为Authentication身份信息与当前执行线程已经绑定在一起,所以可以使用以下代码在应用程序中获取当前已经认证用户的用户名。

public String getCurrentUsername() {
    //得到当前认证后的用户信息
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    
    if (principal instanceof UserDetails) {
        return ((UserDetails) principal).getUsername();
    }
    
    if (principal instanceof Principal) {
        return ((Principal) principal).getName();
    }
    
    return String.valueOf(principal);
}

3.1、SecurityContextHolder
由于一个请求从开始到结束都让一个线程处理,这个线程中途不会去处理其他的请求,所以在这段时间内,这个线程就相当于跟当前用户是一一对应的。SecurityContextHolder工具类就是用于把SecurityContext存储在当前线程中,在Web环境下,SecurityContextHolder是利用ThreadLocal来存储SecurityContext对象的。所以SecurityContextHolder可以用来设置和获取SecurityContext对象,该类主要是给框架内部用,我们可以利用它获取当前用户的SecurityContext对象,进而进行请求检查,和访问控制。
因为 SecurityContextHolder用于存储当前应用程序的SecurityContext安全上下文对象,而SecurityContext中则存储着当前 正在访问系统的 Authentication 用户的详细信息,如当前操作的用户是谁,该用户是否已经被认证,拥有哪些角色权限等。所以我们可以用下图来展示SecurityContextHolder、SecurityContext与Authentication三者之间的关系:

image.png

3.2、SecurityContextHolder的线程安全性保障
SecurityContextHolder 使用 ThreadLocal 来保存 SecurityContext,意味着只要是同一线程中的方法,都可以从 ThreadLocal 中获取到当前的 SecurityContext对象。
**因为Sevlet中的线程都是被池化复用的,一旦处理完当前的请求,这个线程可能马上就会被分配去处理其他的请求,而且也不能保证用户下次的请求会被分配到与上次相同的线程。 也就是 我们每次在请求完成后,Spring Security都会将 ThreadLocal 进行清除,即在每一次 请求 结束后都会自动清除当前线程的 ThreadLocal对象。
这时候如果我们的认证信息没有被保存,岂不是每次请求后都要重新认证登录?这明显不行,我们肯定要保存用户的认证信息!这个保存工作是由SecurityContextPersistenceFilter来完成的!
SecurityContextPersistenceFilter是Security中的一个拦截器,它的执行时机非常早,当请求来临时它会从SecurityContextRepository中把SecurityContext对象取出来,然后放入SecurityContextHolder的ThreadLocal中。在所有拦截器都处理完成后,再把SecurityContext存入SecurityContextRepository,并清除SecurityContextHolder内的SecurityContext引用 。 这样就实现了用户认证信息的保存,也保证了线程的安全性!

3.3、SecurityContextHolder中的方法及属性
在 SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法的内部逻辑基本上都是通过 SecurityContextHolder 所持有的 SecurityContextHolderStrategy 对象来实现的,如 getContext()、setContext()、clearContext()等。如下图所示:

image.png

3.4、SecurityContextHolder中的3种存储策略
SecurityContextHolder中定义了3种策略 ****来管理SecurityContext对象的存储,默认使用的 strategy 是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy,另外还有GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy 两种策略。

SecurityContextHolder可以通过设置来调整3种存储策略,三种策略详情如下:

MODE_THREADLOCAL:表示将 SecurityContext对象 存储在当前线程中;
MODE_INHERITABLETHREADLOCAL:表示将 SecurityContext对象 存储在线程中,但子线程可以获取到父线程中的 SecurityContext;
MODE_GLOBAL:表示 SecurityContext对象内容 在所有线程中都相同。

SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。 一般而言,我们使用默认的 strategy 就可以了。

Filter相关API

1.1、Filter概念
Spring Security 的底层是通过一系列的 Filter 来管理认证和授权请求的,形成了一组核心的过滤器链,每个 Filter 都有其自身的功能,而且各个 Filter 在功能上还有关联关系,项目启动后将会自动配置生效。其中最核心的就是 BasicAuthenticationFilter ,该Filter用来认证用户的身份。

1.2、Filter顺序
Spring Security 中定义的一些列Filter,不管实际应用中我们会用到哪些,它们都应当保持如下顺序。

1、ChannelProcessingFilter:如果你访问的 channel 错了,那首先就会在 channel 之间进行跳转,如 http 变为 https。
2、SecurityContextPersistenceFilter:在一开始进行 请求 的时候就会在 SecurityContextHolder 中建立一个 SecurityContext,然后在请求结束的时候,任何对 SecurityContext 的改变都会被 copy 到 HttpSession中。
3、ConcurrentSessionFilter:因为它需要使用 SecurityContextHolder 的功能,而且更新对应 session 的最后更新时间,以及通过 SessionRegistry 获取当前的 SessionInformation 以检查当前的 session 是否已经过期,过期则会调用 LogoutHandler。
4、认证处理机制:如 UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter 等,以至于 SecurityContextHolder 可以被更新为包含一个有效的 Authentication 请求。
5、SecurityContextHolderAwareRequestFilter:它将会把 HttpServletRequest 封装成一个继承自 HttpServletRequestWrapper 的 SecurityContextHolderAwareRequestWrapper,同时使用 SecurityContext 实现了 HttpServletRequest 中与安全相关的方法。
6、JaasApiIntegrationFilter:如果 SecurityContextHolder 中拥有的 Authentication 是一个 JaasAuthenticationToken,那么该 Filter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain。
7、RememberMeAuthenticationFilter:如果之前的认证处理机制没有更新 SecurityContextHolder,并且用户请求包含了一个 Remember-Me 对应的 cookie,那么一个对应的 Authentication 将会设给 SecurityContextHolder。
8、AnonymousAuthenticationFilter:如果之前的认证机制都没有更新 SecurityContextHolder 拥有的 Authentication,那么一个 AnonymousAuthenticationToken 将会设给 SecurityContextHolder。
9、ExceptionTransactionFilter:用于处理在 FilterChain 范围内抛出的 AccessDeniedException 和 AuthenticationException,并把它们转换为对应的 Http 错误码返回或者对应的页面。
10、FilterSecurityInterceptor:保护 Web URI,并且在访问被拒绝时抛出异常。

认证管理相关类

1.1、AuthenticationManager
AuthenticationManager的作用是校验Authentication,如果验证失败会抛出AuthenticationException异常。 AuthenticationException是抽象类,因此并不能实例化一个AuthenticationException异常并抛出,实际抛出的通常是其实现子类,如DisabledException、LockedException、BadCredentialsException等。AuthenticationManager的核心认证方法authenticate()如下所示:

image.png

AuthenticationManager是认证相关的核心接口,也是发起认证的起点 。在实际开发中,我们可能会允许用户使用 用户名 + 密码 登录,同时允许用户使用 邮箱 + 密码,手机号码 + 密码 登录,甚至可能允许用户使用 指纹 登录,所以要求认证系统支持多种认证方式。几种不同的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登录,可以分别对应三种 不同的 AuthenticationProvider 子类。

1.2、ProviderManager
Spring Security 中 AuthenticationManager 接口的默认实现类是 ProviderManager,但它本身并不直接处理身份认证请求,它会委托给内部配置的 AuthenticationProvider 列表providers。该列表会进行循环遍历,依次对比匹配以查看它是否可以执行身份验证,每个 Provider 验证程序在验证后,将会抛出异常或返回一个完全填充的 Authentication 对象。源码如下所示:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    //维护一个AuthenticationProvider列表
    private List providers = Collections.emptyList();
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
                // 遍历providers列表,判断是否支持当前authentication对象的认证方式
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                                // 执行provider的认证方式并获取返回结果
                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;
            }
        }
                // 若当前ProviderManager无法完成认证操作,且其包含父级认证器,则允许转交给父级认证器尝试进行认证
        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parentResult = 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 = parentException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // 完成认证,从authentication对象中移除私密数据
                ((CredentialsContainer) result).eraseCredentials();
            }

                        // 若父级AuthenticationManager认证成功,则派发AuthenticationSuccessEvent事件
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }

                // 未认证成功,抛出ProviderNotFoundException异常
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
        // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }
}

在ProviderManager进行认证的过程中,会遍历providers列表,判断是否支持对当前authentication对象的认证方式。若支持该认证方式时,就会调用所匹配的provider(AuthenticationProvider)对象的authenticate()方法进行认证操作;若认证失败则返回null,下一个 AuthenticationProvider 会继续尝试认证。如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。

到这里有人肯定有疑惑,ProviderManager会判断providers列表中的某个xxxProvider是否支持对当前authentication对象的认证方式,那到底是怎么判断的呢?

我们进入AuthenticationProvider看看


image.png

有两个方法authenticate和supports,关键就在supports这个方法,查看一下AuthenticationProvider的实现类


image.png

点开AnonymousAuthenticationProvider类查看
public class AnonymousAuthenticationProvider implements AuthenticationProvider,
        MessageSourceAware {
    public boolean supports(Class authentication) {
        return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

发现有一个supports方法,意味着AnonymousAuthenticationProvider 类重写了父类AuthenticationProvider的supports方法,其中该方法里将AnonymousAuthenticationToken的Class字节码和要认证的 authentication 对象的 Class字节码作比较,如果一样那就使用AnonymousAuthenticationProvider 来对authentication 进行认证处理,如果不是就返回。
那为什么要用AnonymousAuthenticationToken呢,仔细看有没有发现AnonymousAuthenticationToken和上文提到过的UsernamePasswordAuthenticationToken很像,上文有提到 "用户登录认证之前,用户名密码等信息会被封装为一个Authentication的具体实现类对象",所以我们可以这样理解,如果用户以用户名+密码的方式来登录系统,那就可以使用UsernamePasswordAuthenticationToken这个类封装(从名字也可以看出来是专门正对用户名和密码登录这种方式的),那如果换种登录方式,比如微信一键登录、手机短信验证登录、github登录或者邮箱登录等,还能使用UsernamePasswordAuthenticationToken吗,在这种情况下可能就不适用了,可能就要使用类似AnonymousAuthenticationToken这种别的xxxToken来封装登录信息了。
所以,对于不同的登录方式,我们可以使用不同的xxxToken,同时每个xxxToken都对应有属于自己的实现了AuthenticationProvider的xxxProvider,然后重写supports方法,在方法里用不同的xxxToken来判断该xxxProvider是否支持对当前authentication对象的认证方式。

认证实现类AuthenticationProvider

1.1、UsernamePasswordAuthenticationToken

image.png

UsernamePasswordAuthenticationToken继承自AbstractAuthenticationToken类,实现了Authentication接口。在 Spring Security 中,当我们使用用户名和密码进行登录认证的时候,用户在登录表单中提交的用户名和密码信息,都会被封装到 UsernamePasswordAuthenticationToken 对象 中。
这个生成的Token(Authentication)对象,接下来会被交由AuthenticationManager来进行管理,而AuthenticationManager内部又管理了一系列的AuthenticationProvider对象,每一个Provider都会调用UserDetailsService和UserDetail类,来查询返回一个Authentication对象,该对象中包含有用户名、密码、权限等信息。

1.2、AuthenticationProvider
AuthenticationManager是负责管理协调认证工作的,但并不负责认证功能的真正实现,认证功能的真正实现是由 AuthenticationManager 中引用的 AuthenticationProvider 类来完成的,通过源码我们可以看到AuthenticationManager 中引用了一个providers列表 , 如下所示:

image.png

providers集合的泛型是AuthenticationProvider接口, AuthenticationProvider接口有多个实现子类,如下图:
image.png

AuthenticationProvider接口的一个直接子类是AbstractUserDetailsAuthenticationProvider,同时该类又有一个直接子类DaoAuthenticationProvider,关系如下

image.png

Spring Security中默认就是使用DaoAuthenticationProvider来实现基于数据库模型认证授权工作的
DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证方式,可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。

1.3、authenticate()认证方法源码剖析
在实际项目中,最常见的认证方式是基于数据库模型,使用用户名和密码进行认证,具体是由DaoAuthenticationProvider类实现的。 对于已注册的用户,因为我们在数据库中已保存了正确的用户名和密码,所以认证就是对比用户在登录表单中提交的用户名、密码,与数据库中所保存的用户名、密码是否相同。这个认证的实现,主要是基于如下源码来实现的。

image.png

首先我们知道,在AuthenticationProvider接口中有个authenticate()方法,但是该方法并没有默认实现,如下所示:


image.png

这个authenticate()方法是在AuthenticationProvider的子类AbstractUserDetailsAuthenticationProvider中实现的,实现源码如下:

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        //从认证对象中得到用户名
        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()方法,根据用户名查询用户对象。
                //其中的retrieveUser()方法是个抽象方法,由子类DaoAuthenticationProvider来实现。
                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 {
            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;
            }
        }

        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

1.4、retrieveUser()检索用户方法源码剖析
在上面的authenticate()方法内部,调用了retrieveUser()方法,该方法是抽象方法,根据用户名查询用户信息,由子类DaoAuthenticationProvider来具体实现,如下所示:

image.png

当使用用户名和密码进行认证时,用户在登录页面表单中提交的用户名和密码,会被封装成UsernamePasswordAuthenticationToken对象。在DaoAuthenticationProvider 中,会对retrieveUser方法进行具体实现,根据用户名加载用户的信息。虽然有两个参数,但只有第一个参数起主要作用,该方法会返回一个 UserDetails对象。retrieveUser 方法的具体实现如下:
image.png

当执行this.getUserDetailsService().loadUserByUsername(username);时会去调用我们自己编写的UserDetailsService对象中的loadUserByUsername()方法加载用户。
image.png

1.5、完整的authenticate流程

1、在ProviderManager类中遍历List providers集合,判断AuthenticationProvider是否支持对当前对象进行认证。
2、如果支持,调用AuthenticationProvider接口的authenticate(authentication)方法,由对应的子类重写authenticate进行认证。
3、结合当前具体的认证实现模型,如果是基于数据库模型来进行表单认证,则执行AbstractUserDetailsAuthenticationProvider类中的authenticate(authentication)方法具体进行认证。
4、AbstractUserDetailsAuthenticationProvider类中的authenticate(authentication)方法内部会调用retrieveUser()抽象方法加载对象。
5、在DaoAuthenticationProvider类中具体执行retrieveUser()方法,在该方法中调用UserDetailsService对象的loadUserByUsername(username)方法,从而实现根据用户名从数据库中查询用户信息。

UserDetails 、GrantedAuthority、UserDetailsService与User

1.1、UserDetails接口
UserDetails 是 Spring Security 中的核心接口,它表示用户的详细信息,这个接口涵盖了一些必要的用户信息字段。我们可以将UserDetails视为自己的用户数据库和SecurityContextHolder所需的适配器,通过具体的实现类对它进行扩展。

image.png

另外我们在前面也介绍过另一个 Authentication 接口,它与 UserDetails 接口的定义如下:
image.png

虽然 Authentication 与 UserDetails 很类似,但它们之间是有区别的。Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 需要被区分对待,前者是用户从前端传递提交过来的密码凭证,后者是用户存储的正确的密码,认证器其实就是对这两者进行比对。

同时Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。 还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息就是经过 provider(AuthenticationProvider) 认证之后被填充的。在Spring Security中有多处引用这个UserDetails接口,比如在 DaoAuthenticationProvider 类中,retrieveUser 方法签名是这样的:

image.png

1.1、GrantedAuthority接口
我们在前面介绍的Authentication类中有一个getAuthorities()方法,该方法可以返回当前 Authentication 对象拥有的角色权限,即当前用户拥有的角色权限。该方法的返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种角色权限。GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后再赋值给 UserDetails。

GrantedAuthority 接口中只定义了一个 getAuthority() 方法,该方法会返回一个字符串,表示对应的角色权限,如果对应权限不能用字符串表示,则应当返回 null。


image.png

该接口的默认实现类是SimpleGrantedAuthority,该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象。如下图所示:


image.png

我们前面讲过的Authentication类中,提供的一个重要方法就是getAuthorities(),该方法会返回GrantedAuthority这个对象的数组。该对象代表的权限通常是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。我们可以将这些角色配置为 Web 授权、方法授权和域对象授权。 GrantedAuthority对象通常由UserDetailsService加载。

另外在Spring Security中,角色和权限共用这个GrantedAuthority接口,唯一的不同点在于角色多了"ROLE_"前缀,而且它没有Shiro中的那种从属关系,即一个角色包含哪些权限等。 在Spring Security看来角色和权限是一样的,它认证的时候,会把所有权限(角色、权限)都取出来,并不是分开验证的。所以,在Spring Security提供的UserDetailsService默认的实现类JdbcDaoImpl中,角色和权限都存储在auhtorities表中。而不是像Shiro那样,角色有个roles表,权限有个permissions表,以及相关的管理表等等。

1.2、UserDetailsService接口

image.png

在loadUserByUsername 方法中,可以通过 username 来加载匹配的用户,当找不到 username 对应的用户时,会抛出 UsernameNotFoundException 异常。

UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库中加载用户,后者从内存中加载用户。 JdbcDaoImpl 允许我们从数据库中来加载一个 UserDetails,其底层使用的是 Spring 的 JdbcTemplate 进行操作,所以我们需要给其指定一个数据源。InMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中。
当然我们也可以自己实现 UserDetailsService接口,实现其中的loadUserByUsername()方法,在该方法中可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails并返回。
另外UserDetailsService 和 AuthenticationProvider 两者的职责很像,常常被人们搞混,记住一点即可,UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。

总结

到目前为止,我们已经简单介绍了 Spring Security 的几个核心API:

1.SecurityContextHolder,提供对SecurityContext的访问权限。
2.SecurityContext,保存Authentication以及可能特定于请求的安全信息。
3.Authentication,Spring Security 的主体信息。
4.GrantedAuthority,代表授予主体的权限。
5.UserDetails,通过提供必要的信息,从应用程序的 DAO 或其他安全数据源来构建出 Authentication 对象。
6.UserDetailsService,通过传入String类型的用户名(或证书 ID 等),创建出对应的UserDetails。
7.AuthenticationProvider,负责进行用户的认证授权工作。

你可能感兴趣的:(Spring Security07 Spring Security核心API)