spring security2之后namespace方式使得在配置上大大降低了门槛,一个简单的基础<http auto-config='true'/>就可以搞定一个demo,如果你的系统接受那个单调而古板的ss默认的登录页面和英文的错误提示,那对于你而言,ss只是一个几句话的配置而已。
但是实际应用中,我们并不会有那么简单的场景,不管是从用户体验页面交互还是从系统架构,都几乎不太可能使用那个可怜巴巴的默认登陆页面。
一个 auto-config='true',ss默认加载了一系列的过滤器,并执行逐个过滤身份验证,如果验证成功,则注册信息到session,如果校验失败,则进入下一个filter,如果全部都检查失败,则抛出异常。通常在这种情况下,你看到的只有一个 bad Credentials,你并不能清楚的指导,到底是密码错了,还是用户压根儿不存在,是因为这种方式下ss隐藏了具体的异常信息。言归正传到本章内容,旨在完成以下几个目标:
目标
-------------------------------------------------------------------------------------------
1、系统前台和后台分别登陆并定位到不同的成功页面和失败页面;
--------------------------------------------------------------------------------------------
2、控制session单次登陆,后一次的登录将强迫前一次的session失效。
--------------------------------------------------------------------------------------------
3、使用cookie自动登陆,默认为ss的2个周;
--------------------------------------------------------------------------------------------
4、使用自己系统的权限数据库模型,不采用ss默认的user - authorities表的结构;
--------------------------------------------------------------------------------------------
5、优化处理异常信息,增强用户体验;
--------------------------------------------------------------------------------------------
部分问题在上一章节中已经提到,但是上一个章节在凌晨写的很是仓促,个人感觉总结的不好,所以希望通过这次的总结加深印象,巩固一下收获。
第一个问题,通常而言,现在大部分的系统都采用前后台分开登录的方式,所以这里我们跟风也实现这样的方式。要实现这个目标头脑中第一个反应就是想先判断下url是后台的还是前台的,如果是后台的则定位到后台登陆页面,如果是前台的,则定位到前台登录页面。很简单的思路。好了,现在想想我们遇到了什么问题,首先,如何将我们的这样的逻辑判断逻辑插入到ss中;其次,如何去分别定位不同的登陆页面;
<http>标签提供了一个参数:entry-point-ref 顾名思义,这个参数允许你制定过滤器链Entry,你可以制定一个任何实现了AuthenticationEntryPoint接口的子类,以在其内部实现自己的定位逻辑,那么这个是如何被使用的呢?
首先摘一段ss文档中的话:
-----------------------------------------------------------------------------------------------------------
讨论一个典型的web应用验证过程:
-
你访问首页,点击一个链接。
-
向服务器发送一个请求,服务器判断你是否在访问一个受保护的资源。
-
如果你还没有进行过认证,服务器发回一个响应,提示你必须进行认证。 响应可能是HTTP响应代码,或者是重新定向到一个特定的web页面。
-
依据验证机制,你的浏览器将重定向到特定的web页面,这样你可以添加表单, 或者浏览器使用其他方式校验你的身份(比如,一个基本校验对话框,cookie,或者X509证书,或者其他)。
-
浏览器会发回一个响应给服务器。 这将是HTTP POST包含你填写的表单内容,或者是HTTP头部,包含你的验证信息。
-
下一步,服务器会判断当前的证书是否是有效的, 如果他们是有效的,下一步会执行。 如果他们是非法的,通常你的浏览器会再尝试一次(所以你返回的步骤二)。
-
你发送的原始请求,会导致重新尝试验证过程。 有希望的是,你会通过验证,得到足够的授权,访问被保护的资源。 如果你有足够的权限,请求会成功。否则,你会收到一个HTTP错误代码403,意思是访问被拒绝。
Spring Security使用鲜明的类负责上面提到的每个步骤。 主要的部分是(为了使用他们)是ExceptionTranslationFilter, 一个AuthenticationEntryPoint, 一个验证机制,一个AuthenticationProvider。
ExceptionTranslationFilter 是一个Spring Security过滤器,用来检测是否抛出了Spring Security异常。这些异常会被AbstractSecurityInterceptor抛出,它主要用来提供验证服务。我们会在下一节讨论AbstractSecurityInterceptor,但是现在,我们只需要知道,它是用来生成Java异常,和知道跟HTTP没啥关系,或者如何验证一个主体。而ExceptionTranslationFilter提供这些服务,使用特点那个的响应,返回错误代码403(如果主题被验证了,但是权限不足-在上边的步骤七),或者启动一个AuthenticationEntryPoint(如果主体没有认证,然后我们需要进入步骤三)。
-----------------------------------------------------------------------------------------------------------------
其中讲到了一个ExceptionTranslationFilter,我们提供的AuthenticationEntryPoint也主要是提供给其使用,在容器初始化的时候,如下:
可以看到,上述生成了一个ExceptionTranslationFilter etf并且
这样讲我们的EntryPoint注入其中,那么在过滤器链启动开始权限检查的时候,下面的代码很清晰的展示了我们注入的EntryPoint将会被怎么样使用:(ExceptionTranslationFilter.java类)
可以看到,其中调用commence方法来执行内部逻辑,所以到了现在我们可以知道如何去讲自己的EntryPoint逻辑插入其中,只需要实现AuthenticationEntryPoint接口并在commence方法中填写自己的逻辑即可。
我们按照之前的思路编写如下的逻辑:
其中:
这样我们实现了自动定位登陆页面。
但是这样只完成了一半工作,还有就是我们要在这两个不同的登陆页面中,进行分别的处理,比如登陆成功和失败的动作等。我们配置如下前台登录、前台注销、后台登录、后台注销四个过滤器,并分别指定其处理的url地址,这样我们在页面中只要指定相关的action的地址就可以定位到不同的过滤器执行处理。
其中的filterProcessesUrl参数即制定了当前的过滤器处理的url。以区分了前后台,这样在前台后台的form中,你的前台的action和后台的action只要分别指定到相应的路径即可,即上述的/j_spring_security_logout和/admin/j_spring_security_logout等。
注意一个参数alwaysUseDefaultTargetUrl,其指定的含义是登录成功是否总是定位到指定的成功页面,通常而言,我们拦截了一个url提示用户登录,在用户登录完成之后希望立即跳转到用户希望的页面,所以这里需要设置成false,很不幸的是,由于登录操作使用的是post方式的提交,所以只有第一次的登录成功才会跳到相应的页面,如果失败,则后续的成功登陆之后,将跳转到默认的成功页面。这里后续可以改造一下。
完成了这个工作,看看目前的配置情况:
既然我们制定了entry-point-ref,也就自然去掉了auto-config来手动控制完成更多的事情。
第二个目标:实现session的单次登录,后一次的登录将强迫前一次登录失效;
如果你使用session标签,也可以很方便完成,但是这里我们尽可能的少用http内部的标签,因为后续的配置,这些内容都是关联的,虽然配置很方便也可以奏效,但是配置代码的交织将让我们思路混乱,所以这里使用bean配置的方式。
至于authenticationManager,后面我们会提到。
注意的是,这里配置完成之后,首次登陆会成功,但是第二次的登录将会抛出异常:
java.lang.IllegalArgumentException: SessionIdentifierAware did not return a Session ID
这里需要像上面一样,将forceEagerSessionCreation配置成true,这样如果session存在了,将不会重复创建,只是英文字面意思有点让人捉摸不透。
其实现机制是怎样的呢,其实就是一个请求过来之后,便利服务器的session列表,如果发现有相同sessionID的存在,那么就强制上一个session过期。所以说,这个配置在用户量非常大的情况下是具有一定性能代价的。
第三个目标,自动登陆,使用cookie
<http>同样提供了一个<remember/>标签来一句话搞定这个需求,比如
<remember-me key="NORMANDYPOSITION_ADMIN" user-service-ref="norUserDetailsService"/>
后面user-service-ref指明了具体的UserDetailsService,注意如果你的xml配置中配置过多个UserDetailsService,如果此处不显示制定一个,则会跑出more than one UserDetailsService....之类的异常;
当然你也可以像下面一样的配置来实现:
你可以看到其中我们制定的userDetailsService等参数皆为我们系统自定义开发的相关内容,具体信息后面我们会涉及这里暂且略过,同时我们制定了这个自定义filter在过滤器链中的位置,以便让其发生作用。
这里自动登录使用到的TokenBasedRememberMeServices,是保存在cookie中的基于散列标记的方式,还有一种是PersistentTokenBasedRememberMeServices,可以将其存放在数据库中。
http://www.family168.com/tutorial/springsecurity/html/remember-me.html#d4e1748
第四个目标:使用自己系统的权限数据模型
ss提供了一个默认的表结构,你只需要按照其字段和名称建立就可以使用了,同时ss提供了一个jdbcDAOImpl类的子类JdbcUserDetailsManager来实现了增删查改操作,但是通常而言对于一个系统有着自己的数据库模型,自然这样的方式不能满足需求,这里我们通过继承UserDetailsService来实现使用自己的数据库表结构以及重新定义UserDetails。
前面我们涉及到了一个authenticationManager配置,他托管着providers和sessionController,后面一个sessionController使我们之前使用到的单次session登录控制使用到的变量,前一个呢,是一个list,维护者当前的“数据提供者”,所谓的数据提供者即提供给ss进行身份判断依据的数据来源,比如database,比如cookie比如内存中等,再贴一下完整的authenticationManager的相关配置:
上面通过providers我们设置了三个数据提供者,分别是dbAuthenticationProvider 数据库,memoryAuthenticationProvider 内存,rememberMeAuthenticationProvider cookie,后面的两个配置很简单,其使用的类也是ss提供的,主要看第一个,我们说要使用自己的数据库结构,那么肯定需要在这个上做文章,我们在其中
指定了使用我们自己开发的UserDetailsService子类,而不是系统默认数据结构封装的JdbcDAOImpl(你可以对比注视掉的那段内容)。
我们都做了什么?
我们使用自己系统定义的nor_customer表和nor_authority表,来保存权限,所以重写如下:
对应的数据结构不影响这里的代码,略过。
现在可以使用自己的数据进行登录和自动登录了。
第五个目标,优化一下异常处理
这个是目前还在进行的工作,一种方式是通过国际化ss的message文件,来实现中文提示,另一个方面,如果我们不想那么麻烦的国际化,并且希望在发生异常的时候做一些事情,可以简单的如下处理一下:
当发生错误的时候会定位的error页面,在error的渲染类中我们捕获异常然后简单分分类就可以了(如果没有其他要求)
这里还在优化进行中,暂时这样子吧。