SpringSecurity初探之认证源码分析

来源:互联网 发布:淘宝订单贷款取消了么 编辑:程序博客网 时间:2024/06/09 20:07

首先,笔者初学SpringSecurity,因此本文章提供的观点不一定正确,笔者也没有具体求证(主要是能力有限),因此请读者带着批判质疑的眼光浏览。另:觉得分析过程枯燥凌乱的可以直接看总结,把过程写下来主要是我的个人乐趣,并且在一边写一边分析的时候我可以像脱缰野马一样不受束缚地进行分析(因为我不用一直记着上一步让我沸腾的发现,生怕它被我遗忘)。


大概的原理分析(其实是别人分析过,这边进行总结)

spring Security实现安全的级别有两种:web访问级别和方法调用级别。

首先我们需要知道的是:web访问级别通过一系列过滤器链实现安全,方法调用级别则是通过AOP实现安全。

然后,下面我们将着眼于web访问级别的安全。

上面我们说过web访问级别的安全通过一系列Filter链实现安全,而我们并不会在web配置文件里面定义一系列的Filter链,而是在web配置文件定义一个Filter代理,该代理负责过滤所有的URL并将URL交给spring容器中真正的Filter进行处理。在spring中,这些过滤器作为bean被spring所管理,主要的Filter有如下几个:

  • ChannelProcessingFilter 要求访问的链接必须是HTTPS协议
  • HttpSessionContextIntegrationFilter 获得登陆用户的认证信息
  • LogoutFilter 登出的过滤器
  • AuthenticationProcessingFilter 登陆
  • RememberMeProcessingFilter RememberMe功能实现
  • FilterSecurityInterceptor 定义访问规则

这些过滤器过滤链接并不会处理认证和授权,而是将认证和授权的工作交给认证管理器(AuthenticationMenager)和决策管理器(DecisionMenager)

这里认证管理器主要负责认证工作,决策管理器主要负责授权工作(这一点请抱着质疑的眼光)。

实际上,认证管理器和决策管理器都不实际处理认证和授权,而是将工作委托给下一级的Provider和Victer

spring针对Provider和Voter有多种实现,分别处理不同方式的授权和认证,这里我们通过使用数据库获得用户存储因此Provider我们一般用DaoAuthenticationProvider,而授权我们用RoleVoter

授权的工作大致就到这里,而认证还尚未停下脚步。

DaoAuthenticationProvider不实际调用数据库,而是将调用数据库的工作交给了其实现类UserDetailsService,该类是事实上与我们(使用Security的程序猿)交互的类,我们在这个类中实现loadUserByUsername(String username)并返回一个User对象给认证管理器进行认证工作。


稍微具体一点的认证原理分析(全内容请带上质疑的眼镜)

先说说我分析源码的方法:因为一开始拿到一整个security源码,我们很难去具体分析,我们甚至不知道我们在分析什么,哪些类有助你的分析。因此我往往先定义自己分析的目的。首先明确分析的目的是探寻登陆的认证过程。那么按照我已有的知识可以猜测(下面的猜测建立在初步进行分析的基础上):登陆请求传递到后台,被web容器捕获,web容器将该请求交给DelegatingFilterProxy处理,而DelegatingFilterProxy并不自己处理这些请求,它将请求交给具体的filter(这些filter存在spring容器中,在security-spring.xml的标签就是一个filter,如form-login就是一个AuthenticationProcessingFilter)。filter内部应该对请求对象封装成Authentication对象(这步猜测源于在AuthenticationManager的authenticate方法中要求传入一个Authentication对象),然后在filter内调用AuthenticationManager的authenticate方法。大致过程应该是这样,那么根据这个初步的过程去找寻具体实现的过程吧!

根据上面的分析,我们知道了,springSecurityFilterChain过滤器作为代理被定义在web容器中,其代码如下:

    <filter>        <filter-name>springSecurityFilterChain</filter-name>        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>    </filter>    <filter-mapping>        <filter-name>springSecurityFilterChain</filter-name>        <url-pattern>/*</url-pattern>    </filter-mapping>

事实上,对于过滤器的原理,我懂的并不多,按照我个人的理解,应该是声明filter-mapping标签之后,实际就对进入的请求进行match,match到的请求会被作为对象纳入filter进行进一步操作。

那么上面的filter过滤所有链接,并把所有的请求对象纳入到DelegatingFilterProxy这个类的管理范围。接下来让我们看看DelegatingFilterProxy源码。

首先进入DelegatingFilterProxy(该类位于org.springframework.web.filter)我们会看到下面这段话:

 * Proxy for a standard Servlet Filter, delegating to a Spring-managed bean that * implements the Filter interface. Supports a "targetBeanName" filter init-param * in {@code web.xml}, specifying the name of the target bean in the Spring * application context.

大概翻译过来就是:这是一个标准Servlet代理,代表了spring实例化的bean。通过指定的targetBeanName寻找实际存在spring上下文的bean(翻译有错莫怪).

然后让我们看一下它的init方法

    @Override    protected void initFilterBean() throws ServletException {        synchronized (this.delegateMonitor) {            if (this.delegate == null) {                // If no target bean name specified, use filter name.                if (this.targetBeanName == null) {                    this.targetBeanName = getFilterName();                }                // Fetch Spring root application context and initialize the delegate early,                // if possible. If the root application context will be started after this                // filter proxy, we'll have to resort to lazy initialization.                WebApplicationContext wac = findWebApplicationContext();                if (wac != null) {                    <!-- 初始化delegate -->                    this.delegate = initDelegate(wac);                }            }        }    }        protected Filter initDelegate(WebApplicationContext wac) throws ServletException {        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);        if (isTargetFilterLifecycle()) {            delegate.init(getFilterConfig());        }        return delegate;    }

关于init方法,我能力有限,并不能解读出太详细的信息,主要有两步操作:

this.targetBeanName = getFilterName()this.delegate = initDelegate(wac)

第一个方法,返回目标filter的名字,该方法通过调用FilterConfig的getFilterName获取目标Filter的名字,而FilterConfig是一个接口,我猜测具体实现应该是filter的配置类(至于具体如何我并不是很清楚)。然后initDelegate通过FilterName从spring上下文得到具体的bean。我们的目标是探寻登陆的认证过程。因此我们可以假定这个Filter就是AuthenticationProcessingFilter(它是处理登陆的Filter,对应form-login标签)。

接下来主要看看它的doFilter方法:

    @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)            throws ServletException, IOException {        // Lazily initialize the delegate if necessary.        Filter delegateToUse = this.delegate;        if (delegateToUse == null) {            synchronized (this.delegateMonitor) {                if (this.delegate == null) {                    WebApplicationContext wac = findWebApplicationContext();                    if (wac == null) {                        throw new IllegalStateException("No WebApplicationContext found: " +                                "no ContextLoaderListener or DispatcherServlet registered?");                    }                    this.delegate = initDelegate(wac);                }                delegateToUse = this.delegate;            }        }        <!-- 核心语句 -->        // Let the delegate perform the actual doFilter operation.        invokeDelegate(delegateToUse, request, response, filterChain);    }    protected void invokeDelegate(            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)            throws ServletException, IOException {        delegate.doFilter(request, response, filterChain);    }

通过doFilter方法,我们可以看到DelegatingFilterProxy的doFilter方法实际上是调用其他Filter的doFilter方法,这里就是调用了AuthenticationProcessingFilter的doFilter方法。那么接下来我们分析的目标转到AuthenticationProcessingFilter。

通过doFilter的实现类我们可以很快在MyEclipse上找到所有实现这个方法的类,但是我并没有找到AuthenticationProcessingFilter,而相对地我找到了AbstractAuthenticationProcessingFilter(该类位于org.springframework.security.web.authentication),且不论为什么与之前大牛分析的不同,我们先看看这个类。

首先我们看它的doFilter方法(让我收获巨大!):

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)            throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) req;        HttpServletResponse response = (HttpServletResponse) res;        <!-- 最神奇的一步 -->        <!-- 之前我一直不明白具体是怎么调用到哪个具体的Filter -->        <!-- 现在我突然就明白过来了 -->        <!-- 之所以称它是一系列的Filter链,是因为它们的执行过程就真的仿佛一个链条一样环环相扣 -->        <!-- 下面这句代码就是它们环环相扣的具体实现 -->        <!-- 通过这一步的判断,判定该请求是否该由此Filter处理 -->        <!-- requiresAuthentication方法体是一个匹配URL方法 -->        <!-- 匹配结果为true说明该请求该由此filter处理,不执行if的内容 -->        <!-- 匹配结果为false说明该方法不应该有此filter处理。执行if内的chain.doFilter方法 -->        <!-- 其实就是调用该filter链的下一个filter的doFilter方法 -->        if (!requiresAuthentication(request, response)) {            chain.doFilter(request, response);            return;        }        if (logger.isDebugEnabled()) {            logger.debug("Request is to process authentication");        }        Authentication authResult;        try {            <!-- 核心语句 -->            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) {            logger.error("An internal error occurred while trying to authenticate the user.", failed);            unsuccessfulAuthentication(request, response, failed);            return;        }        catch (AuthenticationException failed) {            // Authentication failed            unsuccessfulAuthentication(request, response, failed);            return;        }        // Authentication success        if (continueChainBeforeSuccessfulAuthentication) {            chain.doFilter(request, response);        }        successfulAuthentication(request, response, chain, authResult);    }

这样我们其实可以大胆的猜测大牛们都说在web容器上我们定义的Filter的名字最好是springSecurityFilterChain的原因:这一系列的Filter链由springSecurityFilterChain开始。当然,这依然只是我的猜想,毕竟尚未验证。

那么现在,让我们进入doFilter的核心语句authResult = attemptAuthentication(request, response),关于这个方法有一段说明如下:

Performs actual authentication. The implementation should do one of the following: Return a populated authentication token for the authenticated user, indicating successful authentication Return null, indicating that the authentication process is still in progress. Before returning, the implementation should perform any additional work required to complete the process. Throw an AuthenticationException if the authentication process fails 

翻译过来是:

执行认证过程,有三种可能的返回结果:验证成功的话返回填充了具体的请求信息和验证结果的Authentication对象返回null标志认证过程尚未结束。验证失败抛出AuthenticationExcepting

关于第二点返回null可能我们会感到疑惑,但我们不应该被这个小问题拖慢脚步。我们其实可以猜测认证过程是一系列的认证,可能并不包含单一一个认证方法,而是一系列的认证方法,认证方法并不需要返回认证对象,它只需要顺利地执行整个认证过程并不抛出异常,我们就可以人为该用户通过认证(同样,这是我的猜测,未求证),那么为什么还要返回认证对象的返回值呢?关于这一点,希望大家不要忘记我们之前提出的几个filter,其中有一个叫HttpSessionContextIntegrationFilter,它的作用就是返回认证对象的。

那么让我们来具体分析一下这个attemptAuthentication方法。

这个方法是该类定义的一个抽象方法,有几个类都实现了该方法,因为我们的目标是探寻登陆的认证过程。因此,接下来我们看到UsernamePasswordAuthenticationFilter(该类位于org.springframework.security.web.authentication)。

下面我们直接看它的attemptAuthentication方法:

    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();        <!-- 这一步验证了我们之前的猜想,在filter内将username和password进行封装 -->        <!-- UsernamePasswordAuthenticationToken内有两个属性 -->        <!-- principal对应这里的username,credentials(证书)对应这里的密码 -->        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);        // Allow subclasses to set the "details" property        setDetails(request, authRequest);        <!-- 核心语句,把认证工作交给认证管理器 -->        return this.getAuthenticationManager().authenticate(authRequest);    }

又是让我欣喜若狂的一步分析。这一步有两个关键的地方:

  1. 证实了我们之前的猜想,filter把请求进行封装成Authentication对象(上面是封装成UsernamePasswordAuthenticationToken对象,该类是AbstractAuthenticationToken的子类,AbstractAuthenticationToken实现了Authentication接口)
  2. 和我们之前的分析对应起来,filter不进行认证处理,而是把工作交给认证管理器处理。

那么我们的分析工作到了下一步了,在进行具体认证分析之前,我们先总结一下之前的分析成果。

初步总结

DelegatingFilterProxy,这个被定义在web容器上的filter代理,它过滤了所有的链接,然后把它交给spring容器的一系列filter链进行过滤,初始的filter也许就是springSecurityFilterChain,它可能完全不做任何事情,直接把请求交给filter链的下一个filter去处理,但我猜想有一个更有可能并且更高效的做法,那就是它并不是任何事都不做,它也许对应<security:http pattern="/**/*.jpg" security="none"/>标签,它负责判断URL是否应该被处理,假如URL被上面的标签标示,则表示security框架不应该干涉它,则不进入filter链,而是直接放行,若果URL并不被上面的标签标示,则代表它应该被filter链管理。进入filter链的请求经过一系列的filter,通过matches方法决定该filter是否应该对该请求处理,假设它现在来到了AbstractAuthenticationProcessingFilter,在这里的doFilter方法里调用了attemptAuthentication方法进行认证,该方法的具体实现(账号登陆的过程中)是UsernamePasswordAuthenticationFilter,它把请求封装成Authentication对象,并调用了AuthenticationManger的认证方法。


那么接下来,分析的工作进行到真正的认证过程的分析了。

在此之前,我要提出一个疑问,AuthenticationManager是从哪里被注入Filter的呢?
按照我的理解,filter应该将请求封装成Authentication对象,并且应该在这里注入具体的AuthenticationManager来处理我们的认证,可是我怎么也找不到注入的语句(这时候真心希望能找到一个可可以互相交流的人)。

抛开上一个问题(我已经把问题提交给知乎了),现在我们应该看到AuthenticationManager,它开启了整个认证过程。

AuthenticationManager是一个接口,在该接口里定义了一个authenticate的抽象方法。

Authentication authenticate(Authentication authentication) throws AuthenticationException;

对于AuthenticationManager Security框架有两个具体实现,分别是ProviderManager和OAuth2AuthenticationManager,对于OAuth2我不是很熟悉,只知道作用是对客户端授权,然后在对第三方服务商授权的时候就可以通过客户端授权而不是通过自己的账户(有错勿喷,关注OAuth2的可以尝试去研究这个Manager)。而我们研究的是登陆过程的认证,因此应该主要与ProviderManager相关。

之前有大牛分析过,AuthenticationManager不处理认证而是把认证的工作提交给具体的Provider(Provider != ProviderManager,ProviderManager是AuthenticationManager的具体实现)去实现,关注这一步也契合了之前大牛的研究,因此接下来我们分析ProviderManager(该类位于org.springframework.security.authentication)。下面是该类的签名:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

从类签名我们可以看出,该类被spring容器初始化为bean(看名称猜测,这一点其实我不肯定),并且该类实现了AuthenticationManager。

接下来看看它的构造器

    public ProviderManager(List<AuthenticationProvider> providers) {        this(providers, null);    }    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {        Assert.notNull(providers, "providers list cannot be null");        this.providers = providers;        this.parent = parent;        checkState();    }

ProviderManager有两个构造器,如上所述,但不论是哪个构造器,都被要求传入AuthenticationProvider对象的List,这一点和我们之前从大牛那里获知的衔接——AuthenticationManager不直接处理认证,而是交给Provider来执行,而providerManager就是Provider的管理器。在这里,所有需要对该请求认证的Provider(Provider翻译为提供商,从名字上看可以推测它也不是最终执行认证工作的人)会通过构造器传进来处理请求(至于从哪里被注入,怎么注入,这一点我也尚未得解,希望有人能指点一二)。

这里传入的providers是一个AuthenticationProvider(该类位于org.springframework.security.authentication)的列表,AuthenticationProvider是一个跟AuthenticationManager类似的接口,相对AuthenticationManager多了supports方法声明。

    public boolean supports(Class<?> authentication) {        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));    }

我们可以看到supports的具体实现大概像这样(该实现由AbstractUserDetailsAuthenticationProvider实现),isAssignableFrom当UsernamePasswordAuthenticationToken是authentication的父类或接口则返回true。关于这个方法的作用下面会提到。

接下来我们看看最主要的ProviderManager的authenticate方法(这是让我沸腾不已的发现,我们上面提出的问题在这里将得到解决!)。

下面是authenticate方法的部分代码,后面provider的认证不通过的时候还有进一步的处理,但是第一次探索源码的时候我们没必要关注太多,因为到目前为止我们已经遗漏了太多信息了,如果有回头去一点一点捡起来,估计我们要把整个Security框架的代码贴上来,这显然不符合我们的需求。记住我们的目标,我们只是想知道登陆的认证过程

    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;                }                ...

记得我们之前提出的疑问吗?我们不清楚在哪里注入了AuthenticationManager来处理这个Authentication的认证。事实上,AuthenticationManager从未注入过,因为filter根本没必要决定谁来处理这个Authentication的认证,这个认证应当由Authentication自己决定!

在上面的supports方法里面,(大概)会过滤所有的Provider。程序的表达比较简单,它说假如你是我(某一个Provider)的子类那么继续执行我提供的认证过程,否则,直接进行下一次循环,让其他的我的伙伴来决定你是否应该被它们处理。

通过这一步的验证之后,就有provider来具体执行authenticate的工作,只要有一个认证通过,那么后续就不需要继续认证了,我们可以认为它是合法用户了,那么就把详细的认证信息赋值到目标认证对象上就可以了,我们可以返回这个认证出去提供用户进一步的操作,当然,现在我们先不关心这个问题。我们先看看provider的authenticate方法。

因为我们假定的场景是登陆,所以这里应该会被AbstractUserDetailsAuthenticationProvider(该类位于org.springframework.security.authentication.dao)处理。

那么让我们直接来看看AbstractUserDetailsAuthenticationProvider的authenticate方法。

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {         <!-- 这一步着实让我见识到了安全框架的安全性能啊!(实际上我真不明白为什么要再判断一次) -->        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,            messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",                "Only UsernamePasswordAuthenticationToken is supported"));        // Determine username        <!-- 获得该对象的username,默认值是NONE_PROVIDED -->        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();        boolean cacheWasUsed = true;        <!-- 这一步我猜测首先先检索缓存是否存在该对象,存在则获取,不存在再通过其他手段(在我们的例子里是查询数据库)来获取UserDetails -->        <!-- 关于缓存方面,知识尚浅薄,因此不作思考 -->        UserDetails user = this.userCache.getUserFromCache(username);        if (user == null) {            cacheWasUsed = false;            try {                <!-- 这一步调用了该类声明的一个抽象方法,该方法由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 {            <!-- 在这里校验用户证书(即密码)与查询出来的UserDetails对象的证书是否一致 -->            <!-- 校验失败会抛出认证异常 -->            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);                <!-- 在这里校验用户证书(即密码)与查询出来的UserDetails对象的证书是否一致 -->                <!-- 校验失败会抛出认证异常 -->                additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);            } else {                throw exception;            }        }        postAuthenticationChecks.check(user);        if (!cacheWasUsed) {            this.userCache.putUserInCache(user);        }        Object principalToReturn = user;        if (forcePrincipalAsString) {            principalToReturn = user.getUsername();        }        <!-- 通过验证,返回验证成功的Authentication对象 -->        return createSuccessAuthentication(principalToReturn, authentication, user);    }

这是很神奇的转折点,通过上面的代码,我们明白Provider的作用,它下与查询关联,上承接着返回查询结果的工作,而且用户合法性的验证也在Provider中进行了操作。接下来我们继续看看retrieveUser方法的具体实现。

该方法的实现类是DaoAuthenticationProvider(它位于org.springframework.security.authentication.dao),这一点又和其他大牛的解读衔接了(我享受这种衔接的过程,它一次次告诉我,目前为止走过的路应该没有错)。现在让我们看看它的实现过程:

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)            throws AuthenticationException {        UserDetails loadedUser;        try {            <!-- 我们看到,这一步getUserDetailsService返回一个UserDetailsService对象(该对象由我们来写,主要重写loadUserByUsername) -->            <!-- 默认jdbc数据库有jdbcDaoImpl实现该方法,但是为了适应业务,我们往往自己实现该方法 -->            loadedUser = this.getUserDetailsService().loadUserByUsername(username);            <!-- 这里关于为什么查询不到用户还要进行密码的验证呢? -->            <!-- 我自己并不是特别清楚,他自己的解释如下 -->            <!--因为如果密码不是有效的格式,一些PasswordEncoder实现会短路。 -->        } 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;    }

这里我又要提出一个疑问了。UserDetailsService是什么时候注入的呢?这个问题先放着,说不定等等又有让我热血沸腾的发现。

代码注释已经把过程解释得比较清楚了。那么接下来让我们直接看看loadUserByUsername方法的具体实现,因为我们不知道具体是在哪里被注入的UserDetailsService,所以这里我们只能假定loadUserByUsername由JdbcDaoImpl实现,因为它是默认的UserDetailsService,我们也可以根据自己的业务表设计合适的UserDetailsService,因此我猜测上面注入UserDetailsService的工作由用户执行。

下面是它默认的查询语句:

org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl.DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?"org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl.DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?"org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl.DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities 

这就意味着,假如我们要用SpringSecurity框架默认的UserDetailsService那么我们就必须按照它的意志去实现我们的权限表,这显然太局限了。从外,我们还有自己实现UserDetailsService这条路可以走,让我们来看看UserDetailsService(该类位于org.springframework.security.core.userdetails)接口定义了哪些方法。

public interface UserDetailsService {    //~ Methods ========================================================================================================    /**     * Locates the user based on the username. In the actual implementation, the search may possibly be case     * sensitive, or case insensitive depending on how the implementation instance is configured. In this case, the     * <code>UserDetails</code> object that comes back may have a username that is of a different case than what was     * actually requested..     *     * @param username the username identifying the user whose data is required.     *     * @return a fully populated user record (never <code>null</code>)     *     * @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority     */    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}

可以看到它指定以了一个方法loadUserByUsername,该方法返回一个UserDetails对象,知道这一点任何人都可以随心所欲地指定自己的数据库表,而不被SpringSecurity框架所束缚。那么在自己实现UserDetailsService之前,我们应该先看看官方给我们的例子是怎么实现的。

下面是JdbcDaoImpl(该类位于org.springframework.security.core.userdetails.jdbc)对loadUserByUsername的实现。

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        List<UserDetails> users = loadUsersByUsername(username);        if (users.size() == 0) {            logger.debug("Query returned no results for user '" + username + "'");            <!-- 查询不到结果的话返回UsernameNotFoundException -->            throw new UsernameNotFoundException(                    messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"), username);        }        UserDetails user = users.get(0); // contains no GrantedAuthority[]        Set<GrantedAuthority> dbAuthsSet = new HashSet<GrantedAuthority>();        <!-- 获取用户的所有权限 -->        if (enableAuthorities) {            dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));        }        <!-- 获取用户的用户组权限 -->        if (enableGroups) {            dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));        }        <!-- 上一步用set存储典型作用就是去重 -->        List<GrantedAuthority> dbAuths = new ArrayList<GrantedAuthority>(dbAuthsSet);        <!-- 这一步用意如其名,就是自定义授权的方法,在该类中此方法是一个空方法体的方法 -->        <!-- 它的用意很明确,就是用户有选择授权或不授权的操作,一切自主性留给用户 -->        addCustomAuthorities(user.getUsername(), dbAuths);        if (dbAuths.size() == 0) {            logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");            throw new UsernameNotFoundException(                    messages.getMessage("JdbcDaoImpl.noAuthority",                            new Object[] {username}, "User {0} has no GrantedAuthority"), username);        }        <!-- 把用户名,密码和所拥有的的权限封装成User对象返回,User对象是UserDetails的具体实现 -->        return createUserDetails(username, user, dbAuths);    }    protected List<UserDetails> loadUsersByUsername(String username) {        <!-- 查询语句查询出来的结果通过RowMapper构造函数构造成List返回出去,它是通过匿名函数的形式实现的 -->        <!-- 这里为什么返回一个列表呢?用户名不是不能相同的吗? -->        <!-- 从业务的角度来想的确用户名不应该相同,但是从框架的角度来思考,它应该具有容错性 -->        <!-- 它不应该为使用者的错误承担责任(即是说,即使用户在存储用户的时候犯了错,存储了同样的用户,那么该错误应该由用户自己去发现,而不是框架来报告该错误) -->        return getJdbcTemplate().query(usersByUsernameQuery, new String[] {username}, new RowMapper<UserDetails>() {            public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {                String username = rs.getString(1);                String password = rs.getString(2);                boolean enabled = rs.getBoolean(3);                return new User(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);            }        });    }    <!-- 封装查询结果 -->    protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery,            List<GrantedAuthority> combinedAuthorities) {        String returnUsername = userFromUserQuery.getUsername();        if (!usernameBasedPrimaryKey) {            returnUsername = username;        }        return new User(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(),                true, true, true, combinedAuthorities);    }

到这一步我们的漫长的战斗结束了,并且找到了我们最后应当自己实现UserDetailsService的理由了。那么最后让我们高举战旗,作最后一次总结》

最后的总结

通过filter的时候请求被封装成Authentication对象,送到了AuthenticationManager的面前,它的实现类ProviderManager根据封装的对象的类的类型来决定由那个Provider去处理它(实际上也是Authentication对象自己决定由谁来执行authenticate方法)。我们假定实施authenticate方法的对象是AbstractUserDetailsAuthenticationProvider,这个类做了三件值得赞赏的事情,一是调用retrieveUser方法查询出用户存储;二是对用户的合法性进行了验证;三是验证通过后将查询出来的UserDetails对象封装成Authentication返回出去(该Authentication是经过认证的)。而它调用的retrieveUser方法由DaoAuthenticationProvider具体实现,该方法通过UserDetailsService的loadUserByUsername方法查询UserDetails并返回出去,而我们用户则是通过实现UserDetailsService来介入Spring Security框架。

2 0