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也主要是提供给其使用,在容器初始化的时候,如下:
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (!BeanIds.EXCEPTION_TRANSLATION_FILTER.equals(beanName)) { return bean; } logger.info("Selecting AuthenticationEntryPoint for use in ExceptionTranslationFilter"); ExceptionTranslationFilter etf = (ExceptionTranslationFilter) beanFactory.getBean(BeanIds.EXCEPTION_TRANSLATION_FILTER); Object entryPoint = null; if (beanFactory.containsBean(BeanIds.MAIN_ENTRY_POINT)) { entryPoint = beanFactory.getBean(BeanIds.MAIN_ENTRY_POINT); logger.info("Using main configured AuthenticationEntryPoint."); } else { Map entryPoints = beanFactory.getBeansOfType(AuthenticationEntryPoint.class); Assert.isTrue(entryPoints.size() != 0, "No AuthenticationEntryPoint instances defined"); Assert.isTrue(entryPoints.size() == 1, "More than one AuthenticationEntryPoint defined in context"); entryPoint = entryPoints.values().toArray()[0]; } logger.info("Using bean '" + entryPoint + "' as the entry point."); etf.setAuthenticationEntryPoint((AuthenticationEntryPoint) entryPoint); return bean; }
可以看到,上述生成了一个ExceptionTranslationFilter etf并且
etf.setAuthenticationEntryPoint((AuthenticationEntryPoint) entryPoint);
这样讲我们的EntryPoint注入其中,那么在过滤器链启动开始权限检查的时候,下面的代码很清晰的展示了我们注入的EntryPoint将会被怎么样使用:(ExceptionTranslationFilter.java类)
protected void sendStartAuthentication(ServletRequest request, ServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { HttpServletRequest httpRequest = (HttpServletRequest) request; SavedRequest savedRequest = new SavedRequest(httpRequest, portResolver); if (logger.isDebugEnabled()) { logger.debug("Authentication entry point being called; SavedRequest added to Session: " + savedRequest); } if (createSessionAllowed) { // Store the HTTP request itself. Used by AbstractProcessingFilter // for redirection after successful authentication (SEC-29) httpRequest.getSession().setAttribute(AbstractProcessingFilter.SPRING_SECURITY_SAVED_REQUEST_KEY, savedRequest); } // SEC-112: Clear the SecurityContextHolder's Authentication, as the // existing Authentication is no longer considered valid SecurityContextHolder.getContext().setAuthentication(null); authenticationEntryPoint.commence(httpRequest, response, reason); }
可以看到,其中调用commence方法来执行内部逻辑,所以到了现在我们可以知道如何去讲自己的EntryPoint逻辑插入其中,只需要实现AuthenticationEntryPoint接口并在commence方法中填写自己的逻辑即可。
我们按照之前的思路编写如下的逻辑:
/** * 权限过滤器链EntryPoint,区分前后台分别定位不同页面 * @author: quzishen * @class_type: NorPageEntryPoint * @version: v1.0 * @create_time:2010-9-3 下午03:24:42 * @project_name:NormandyPosition * @description: * <p> * * </p> */ public class NorPageEntryPoint implements AuthenticationEntryPoint, InitializingBean{ private NorUrlMappingLoginPageStrategy norUrlMappingLoginPageStrategy; public void afterPropertiesSet() throws Exception { Assert.notNull(norUrlMappingLoginPageStrategy,"NorUrlMappingLoginPageStrategy can not be null!"); } public void commence(ServletRequest request, ServletResponse response, AuthenticationException authException) throws IOException, ServletException { // 在其内部实现url判断并直接定位请求 norUrlMappingLoginPageStrategy.process(request, response, authException); } public NorUrlMappingLoginPageStrategy getNorUrlMappingLoginPageStrategy() { return norUrlMappingLoginPageStrategy; } public void setNorUrlMappingLoginPageStrategy( NorUrlMappingLoginPageStrategy norUrlMappingLoginPageStrategy) { this.norUrlMappingLoginPageStrategy = norUrlMappingLoginPageStrategy; } }
其中:
/** * 前后台区分url定位逻辑 * @author: quzishen * @class_type: NorUrlMappingLoginPageStrategy * @version: v1.0 * @create_time:2010-9-6 下午03:24:02 * @project_name:NormandyPosition * @description: * <p> * * </p> */ public interface NorUrlMappingLoginPageStrategy { /** * 页面访问策略 * @param request * @param response * @param authException * @throws IOException * @throws ServletException */ public void process(ServletRequest request, ServletResponse response, AuthenticationException authException) throws IOException, ServletException; }
/** * 自定义页面访问策略,如果是后台则跳转到后台登陆页面,如果是前台,则跳转到前台页面 * @author quzishen * */ public class NorUrlMappingLoginPageStrategyImpl implements NorUrlMappingLoginPageStrategy { protected final Logger logger = Logger.getLogger(NorUrlMappingLoginPageStrategyImpl.class); private static final String FRONT_LOGIN_PAGE_NAME = "login.do"; private static final String BACK_LOGIN_PAGE_NAME = "adminlogin.do"; // 后台页面的列表 private Set<String> backSysPageName = new HashSet<String>(); public void process(ServletRequest request, ServletResponse response, AuthenticationException authException) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String redirectUrl = ""; String url = httpRequest.getRequestURI(); String pageName = NorStringUtils.getPageNameFromUrl(url); if(backSysPageName.contains(pageName)){ if(logger.isDebugEnabled()){ logger.debug("redirect to page:adminlogin.do.pageName="+pageName); } redirectUrl = BACK_LOGIN_PAGE_NAME; } else { redirectUrl = FRONT_LOGIN_PAGE_NAME; } RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); urlBuilder.setContextPath(httpRequest.getContextPath()); urlBuilder.setScheme(httpRequest.getScheme()); urlBuilder.setServerName(httpRequest.getServerName()); urlBuilder.setServletPath("/"); urlBuilder.setPort(httpRequest.getLocalPort()); urlBuilder.setPathInfo(redirectUrl); String loginurl = urlBuilder.getUrl(); if(logger.isDebugEnabled()){ logger.debug("loginurl:"+loginurl); } httpResponse.sendRedirect(httpResponse.encodeRedirectURL(loginurl)); } public void setBackSysPageName(Set<String> backSysPageName) { this.backSysPageName = backSysPageName; } }
<!-- 权限处理过滤器入口,用于决策访问的资源是否是受保护的资源 --> <beans:bean id="authenticationProcessingFilterEntryPoint" class="com.normandy.position.web.security.NorPageEntryPoint"> <beans:property name="norUrlMappingLoginPageStrategy" ref="norUrlMappingLoginPageStrategy"/> </beans:bean> <!-- 访问策略 --> <beans:bean id="norUrlMappingLoginPageStrategy" class="com.normandy.position.web.security.NorUrlMappingLoginPageStrategyImpl"> <beans:property name="backSysPageName"> <beans:list> <beans:value>admin.do</beans:value> <beans:value>queryGood.do</beans:value> <beans:value>viewGood.do</beans:value> </beans:list> </beans:property> </beans:bean>
<http entry-point-ref="authenticationProcessingFilterEntryPoint">
这样我们实现了自动定位登陆页面。
但是这样只完成了一半工作,还有就是我们要在这两个不同的登陆页面中,进行分别的处理,比如登陆成功和失败的动作等。我们配置如下前台登录、前台注销、后台登录、后台注销四个过滤器,并分别指定其处理的url地址,这样我们在页面中只要指定相关的action的地址就可以定位到不同的过滤器执行处理。
<!-- 基于表单的认证 - 前台 --> <beans:bean id="front_authenticationProcessingFilter" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter"> <!-- 将过滤器加入过滤器链 --> <security:custom-filter before="AUTHENTICATION_PROCESSING_FILTER" /> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationFailureUrl" value="/login.do" /> <beans:property name="defaultTargetUrl" value="/index.do" /> <beans:property name="alwaysUseDefaultTargetUrl" value="false" /> <beans:property name="filterProcessesUrl" value="/j_spring_security_check" /> </beans:bean> <!-- 前台注销 --> <beans:bean id="front_logoutProcessingFilter" class="org.springframework.security.ui.logout.LogoutFilter"> <security:custom-filter before="LOGOUT_FILTER"/> <beans:property name="filterProcessesUrl" value="/j_spring_security_logout" /> <beans:constructor-arg value="/index.do"/> <beans:constructor-arg> <beans:list> <beans:bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler" /> </beans:list> </beans:constructor-arg> </beans:bean> <!-- 基于表单的认证 - 后台 --> <beans:bean id="admin_authenticationProcessingFilter" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter"> <security:custom-filter position="AUTHENTICATION_PROCESSING_FILTER" /> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationFailureUrl" value="/error.do" /> <beans:property name="defaultTargetUrl" value="/admin.do" /> <beans:property name="alwaysUseDefaultTargetUrl" value="false" /> <beans:property name="filterProcessesUrl" value="/admin/j_spring_security_check" /><!-- 与页面统一 --> </beans:bean> <!-- 后台注销 --> <beans:bean id="back_logoutProcessingFilter" class="org.springframework.security.ui.logout.LogoutFilter"> <security:custom-filter position="LOGOUT_FILTER"/> <beans:property name="filterProcessesUrl" value="/admin/j_spring_security_logout" /><!-- 与页面统一 --> <beans:constructor-arg value="/adminlogin.do"/> <beans:constructor-arg> <beans:list> <beans:bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler" /> </beans:list> </beans:constructor-arg> </beans:bean>
其中的filterProcessesUrl参数即制定了当前的过滤器处理的url。以区分了前后台,这样在前台后台的form中,你的前台的action和后台的action只要分别指定到相应的路径即可,即上述的/j_spring_security_logout和/admin/j_spring_security_logout等。
注意一个参数alwaysUseDefaultTargetUrl,其指定的含义是登录成功是否总是定位到指定的成功页面,通常而言,我们拦截了一个url提示用户登录,在用户登录完成之后希望立即跳转到用户希望的页面,所以这里需要设置成false,很不幸的是,由于登录操作使用的是post方式的提交,所以只有第一次的登录成功才会跳到相应的页面,如果失败,则后续的成功登陆之后,将跳转到默认的成功页面。这里后续可以改造一下。
完成了这个工作,看看目前的配置情况:
<http entry-point-ref="authenticationProcessingFilterEntryPoint"> <intercept-url pattern="/index.do*" filters="none" /> <intercept-url pattern="/freemarker/**" filters="none" /> <intercept-url pattern="/dwr/**" filters="none" /> <intercept-url pattern="/login.do*" filters="none" /> <intercept-url pattern="/adminlogin.do*" filters="none" /> <intercept-url pattern="/error.do*" filters="none" /> <intercept-url pattern="/admin.do*" access="ROLE_ADMIN" /> <intercept-url pattern="/queryGood.do*" access="ROLE_ADMIN" /> <intercept-url pattern="/viewGood.do*" access="ROLE_ADMIN" /> <intercept-url pattern="/**" access="ROLE_USER" /> <remember-me key="NORMANDYPOSITION_ADMIN" user-service-ref="norUserDetailsService"/><!-- 默认有效时间是两周,制定了userDetailService,所有只有数据库的用户才有效,内存的默认超级用户不会保存,这是我们希望的 --> </http>
既然我们制定了entry-point-ref,也就自然去掉了auto-config来手动控制完成更多的事情。
第二个目标:实现session的单次登录,后一次的登录将强迫前一次登录失效;
如果你使用session标签,也可以很方便完成,但是这里我们尽可能的少用http内部的标签,因为后续的配置,这些内容都是关联的,虽然配置很方便也可以奏效,但是配置代码的交织将让我们思路混乱,所以这里使用bean配置的方式。
<!-- 控制session的单次登录 --> <beans:bean id="concurrentSessionController" class="org.springframework.security.concurrent.ConcurrentSessionControllerImpl"> <beans:property name="maximumSessions"> <beans:value>1</beans:value> </beans:property> <beans:property name="sessionRegistry"> <beans:bean id="sessionRegistry" class="org.springframework.security.concurrent.SessionRegistryImpl"></beans:bean> </beans:property> </beans:bean>
<!-- 认证管理器,托管了providers / sessionController --> <beans:bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager"> <beans:property name="providers"> <beans:list> <beans:ref local="rememberMeAuthenticationProvider" /> <beans:ref local="memoryAuthenticationProvider" /> <beans:ref local="dbAuthenticationProvider" /> </beans:list> </beans:property> <beans:property name="sessionController"> <beans:ref local="concurrentSessionController" /> </beans:property> </beans: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....之类的异常;
当然你也可以像下面一样的配置来实现:
<!-- remember me filter --> <beans:bean id="rememberMeProcessingFilter" class="org.springframework.security.ui.rememberme.RememberMeProcessingFilter"> <security:custom-filter before="REMEMBER_ME_FILTER"/> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="rememberMeServices" ref="rememberMeServices" /> </beans:bean> <beans:bean id="rememberMeServices" class="org.springframework.security.ui.rememberme.TokenBasedRememberMeServices"> <beans:property name="userDetailsService" ref="norUserDetailsService"/> <beans:property name="key" value="NORMANDYPOSITION_ADMIN"/> </beans:bean>
你可以看到其中我们制定的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 / sessionController --> <beans:bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager"> <beans:property name="providers"> <beans:list> <beans:ref local="rememberMeAuthenticationProvider" /> <beans:ref local="memoryAuthenticationProvider" /> <beans:ref local="dbAuthenticationProvider" /> </beans:list> </beans:property> <beans:property name="sessionController"> <beans:ref local="concurrentSessionController" /> </beans:property> </beans:bean> <!-- 配置数据库的provider,托管了JdbcDaoImpl --> <beans:bean id="dbAuthenticationProvider" class="org.springframework.security.providers.dao.DaoAuthenticationProvider"> <beans:property name="userDetailsService" ref="norUserDetailsService" /> <beans:property name="passwordEncoder" ref="passwordEncoder" /> <beans:property name="saltSource" ref="reflectionSaltSource" /> <beans:property name="hideUserNotFoundExceptions" value="false" /> </beans:bean> <!-- 配置内存的provider,托管了InMemoryDaoImpl --> <beans:bean id="memoryAuthenticationProvider" class="org.springframework.security.providers.dao.DaoAuthenticationProvider"> <beans:property name="userDetailsService" ref="memoryUserDetailService" /> <beans:property name="passwordEncoder" ref="passwordEncoder" /> <beans:property name="saltSource" ref="reflectionSaltSource" /> <beans:property name="hideUserNotFoundExceptions" value="false" /> </beans:bean> <!-- 提供cookie的provider --> <beans:bean id="rememberMeAuthenticationProvider" class="org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider"> <beans:property name="key" value="NORMANDYPOSITION_ADMIN"/> </beans:bean> <!-- ss默认的数据库的UserDetailService --> <!-- <beans:bean id="dbUserDetailsService"--> <!-- class="org.springframework.security.userdetails.jdbc.JdbcDaoImpl">--> <!-- <beans:property name="dataSource" ref="datasource" />--> <!-- </beans:bean>--> <!-- 采用本系统权限模型的UserDetailService --> <beans:bean id="norUserDetailsService" class="com.normandy.position.web.security.NorUserDetailsService"> </beans:bean> <!-- 内存的UserDetailService --> <beans:bean id="memoryUserDetailService" class="org.springframework.security.userdetails.memory.InMemoryDaoImpl"> <beans:property name="userMap"> <beans:value> mikko=120a91479f8d471ff50a501e150b7737,ROLE_USER,ROLE_ADMIN,ROLE_SUPER jimi=a3ec91753463eae243740e9bbf1a895a,ROLE_USE </beans:value> </beans:property> </beans:bean>
上面通过providers我们设置了三个数据提供者,分别是dbAuthenticationProvider 数据库,memoryAuthenticationProvider 内存,rememberMeAuthenticationProvider cookie,后面的两个配置很简单,其使用的类也是ss提供的,主要看第一个,我们说要使用自己的数据库结构,那么肯定需要在这个上做文章,我们在其中
<beans:property name="userDetailsService" ref="norUserDetailsService" />
指定了使用我们自己开发的UserDetailsService子类,而不是系统默认数据结构封装的JdbcDAOImpl(你可以对比注视掉的那段内容)。
我们都做了什么?
我们使用自己系统定义的nor_customer表和nor_authority表,来保存权限,所以重写如下:
package com.normandy.position.web.security; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.dao.DataAccessException; import org.springframework.security.GrantedAuthority; import org.springframework.security.GrantedAuthorityImpl; import org.springframework.security.SpringSecurityMessageSource; import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetailsService; import org.springframework.security.userdetails.UsernameNotFoundException; import com.normandy.position.dao.NorAuthoritiesDAO; import com.normandy.position.dao.NorCustomerDAO; import com.normandy.position.domain.NorAuthoritiesDO; /** * 采用系统自定义权限结构模型的UserDetailsService * @author: quzishen * @class_type: NorUserDetailsService * @version: v1.0 * @create_time:2010-9-6 下午01:39:15 * @project_name:NormandyPosition * @description: * <p> * * </p> */ public class NorUserDetailsService implements UserDetailsService { protected final Logger logger = Logger.getLogger(NorUserDetailsService.class); private NorCustomerDAO norCustomerDAO; private NorAuthoritiesDAO norAuthoritiesDAO; protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { // get user information Map<String, Object> resultMap = getUserInfoByName(username); if (null == resultMap) { throw new UsernameNotFoundException(messages.getMessage( "JdbcDaoImpl.notFound", new Object[] { username }, "Username {0} not found"), username); } String userId = (String) resultMap.get("user_id"); String userName = (String) resultMap.get("user_name"); String password = (String) resultMap.get("password"); boolean enabled = (Boolean) resultMap.get("enabled"); // get user authorities List<NorAuthoritiesDO> authorities = getAuthoritiesListByUserId(userId); if (null == authorities || 0 == authorities.size()) { throw new UsernameNotFoundException(messages.getMessage( "JdbcDaoImpl.noAuthority", new Object[] { username }, "User {0} has no GrantedAuthority"), username); } List<String> dbAuths = getAuthoritiesList(authorities); GrantedAuthority[] arrayAuths = getGrantedAuthority(dbAuths); UserDetails userDetails = createUserDetails(userId,password,userName,arrayAuths,enabled); return userDetails; } private UserDetails createUserDetails(String userId, String password, String userName,GrantedAuthority[] arrayAuths,boolean enabled){ UserDetails ud = new NorUserDetails(userId,password,userName,arrayAuths,true,true,true,enabled); return ud; } private GrantedAuthority[] getGrantedAuthority(List<String> dbAuths){ GrantedAuthority[] arrayAuths = new GrantedAuthorityImpl[dbAuths.size()]; Iterator<String> index = dbAuths.iterator(); int k = 0; while(index.hasNext()){ String svalue = index.next(); GrantedAuthority temp = new GrantedAuthorityImpl(svalue); arrayAuths[k] = temp; k++; } return arrayAuths; } /** * 根据用户名获取用户信息 * * @param userName * @return */ private Map<String, Object> getUserInfoByName(String userName) { try { Map<String, Object> resultMap = norCustomerDAO.getLoginInfoByUserName(userName); return resultMap; } catch (IllegalAccessException e) { logger.error("get userInfo by username exception!", e); return null; } } /** * 根据userId获取权限列表 * * @param userId * @return */ private List<NorAuthoritiesDO> getAuthoritiesListByUserId(String userId) { try { List<NorAuthoritiesDO> authorities = norAuthoritiesDAO.getNorAuthoritiesByUserId(userId); return authorities; } catch (IllegalAccessException e) { logger.error("get Authority by userId exception!", e); return null; } } /** * 将权限do转换成权限字符串列表 * * @param authorities * @return */ private List<String> getAuthoritiesList(List<NorAuthoritiesDO> authorities) { List<String> dbAuths = new ArrayList<String>(); Iterator<NorAuthoritiesDO> authoritiesIndex = authorities.iterator(); while (authoritiesIndex.hasNext()) { NorAuthoritiesDO na = authoritiesIndex.next(); dbAuths.add(na.getAuthority()); } return dbAuths; } public NorCustomerDAO getNorCustomerDAO() { return norCustomerDAO; } public void setNorCustomerDAO(NorCustomerDAO norCustomerDAO) { this.norCustomerDAO = norCustomerDAO; } public NorAuthoritiesDAO getNorAuthoritiesDAO() { return norAuthoritiesDAO; } public void setNorAuthoritiesDAO(NorAuthoritiesDAO norAuthoritiesDAO) { this.norAuthoritiesDAO = norAuthoritiesDAO; } }
package com.normandy.position.web.security; import org.apache.commons.lang.StringUtils; import org.springframework.security.GrantedAuthority; import org.springframework.security.userdetails.UserDetails; public class NorUserDetails implements UserDetails{ //============Fields start~~~================================ private static final long serialVersionUID = 1L; private String userId; private String password; private String username; private GrantedAuthority[] authorities; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private boolean enabled; //============Fields end~~~==================================== //============Method start ~~~================================= /** * @param userId * @param password * @param username * @param authorities * @param accountNonExpired * @param credentialsNonExpired * @param enabled * @throws IllegalArgumentException */ NorUserDetails(String userId, String password, String username, GrantedAuthority[] authorities, boolean accountNonExpired, boolean credentialsNonExpired, boolean enabled) throws IllegalArgumentException { if (((username == null) || "".equals(username)) || (password == null)) { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } this.userId = userId; this.password = password; this.username = username; this.authorities = authorities; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.enabled = enabled; } /** * @param userId * @param password * @param username * @param authorities * @param accountNonExpired * @param enabled * @throws IllegalArgumentException */ NorUserDetails(String userId, String password, String username, GrantedAuthority[] authorities, boolean accountNonExpired, boolean enabled) throws IllegalArgumentException { if (((username == null) || "".equals(username)) || (password == null)) { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } this.userId = userId; this.password = password; this.username = username; this.authorities = authorities; this.accountNonExpired = accountNonExpired; this.enabled = enabled; } /** * @param userId * @param password * @param username * @param authorities * @param enabled * @throws IllegalArgumentException */ NorUserDetails(String userId, String password, String username, GrantedAuthority[] authorities, boolean enabled) throws IllegalArgumentException { if (((username == null) || "".equals(username)) || (password == null)) { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } this.userId = userId; this.password = password; this.username = username; this.authorities = authorities; this.enabled = enabled; } /** * @param userId * @param password * @param username * @param authorities * @param accountNonExpired * @param accountNonLocked * @param credentialsNonExpired * @param enabled * @throws IllegalArgumentException */ NorUserDetails(String userId, String password, String username, GrantedAuthority[] authorities, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) throws IllegalArgumentException { if (((username == null) || "".equals(username)) || (password == null)) { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } this.userId = userId; this.password = password; this.username = username; this.authorities = authorities; this.accountNonExpired = accountNonExpired; this.accountNonLocked = accountNonLocked; this.credentialsNonExpired = credentialsNonExpired; this.enabled = enabled; } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object usr) { if(null == usr || !(usr instanceof NorUserDetails)){ return false; } NorUserDetails user = (NorUserDetails)usr; if (user.getAuthorities().length != this.getAuthorities().length) { return false; } for (int i = 0; i < this.getAuthorities().length; i++) { if (!this.getAuthorities()[i].equals(user.getAuthorities()[i])) { return false; } } if(StringUtils.equals(user.getUserId(), this.getUserId())){ return true; } return (this.getPassword().equals(user.getPassword()) && this.getUsername().equals(user.getUsername()) && (this.isAccountNonExpired() == user.isAccountNonExpired()) && (this.isAccountNonLocked() == user.isAccountNonLocked()) && (this.isCredentialsNonExpired() == user.isCredentialsNonExpired()) && (this.isEnabled() == user.isEnabled())); } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ public int hashCode() { int code = 9792; if (this.getAuthorities() != null) { for (int i = 0; i < this.getAuthorities().length; i++) { code = code * (this.getAuthorities()[i].hashCode() % 7); } } if (this.getPassword() != null) { code = code * (this.getPassword().hashCode() % 7); } if (this.getUsername() != null) { code = code * (this.getUsername().hashCode() % 7); } if (this.isAccountNonExpired()) { code = code * -2; } if (this.isAccountNonLocked()) { code = code * -3; } if (this.isCredentialsNonExpired()) { code = code * -5; } if (this.isEnabled()) { code = code * -7; } return code; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#getAuthorities() */ public GrantedAuthority[] getAuthorities() { return authorities; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#getPassword() */ public String getPassword() { return password; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#getUsername() */ public String getUsername() { return username; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#isAccountNonExpired() */ public boolean isAccountNonExpired() { return accountNonExpired; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#isAccountNonLocked() */ public boolean isAccountNonLocked() { return accountNonLocked; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#isCredentialsNonExpired() */ public boolean isCredentialsNonExpired() { return credentialsNonExpired; } /* (non-Javadoc) * @see org.springframework.security.userdetails.UserDetails#isEnabled() */ public boolean isEnabled() { return enabled; } //=======================================~~~ setter ======================================= public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public void setPassword(String password) { this.password = password; } public void setUsername(String username) { this.username = username; } public void setAuthorities(GrantedAuthority[] authorities) { this.authorities = authorities; } public void setAccountNonExpired(boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public void setCredentialsNonExpired(boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public void setEnabled(boolean enabled) { this.enabled = enabled; } }
对应的数据结构不影响这里的代码,略过。
现在可以使用自己的数据进行登录和自动登录了。
第五个目标,优化一下异常处理
这个是目前还在进行的工作,一种方式是通过国际化ss的message文件,来实现中文提示,另一个方面,如果我们不想那么麻烦的国际化,并且希望在发生异常的时候做一些事情,可以简单的如下处理一下:
当发生错误的时候会定位的error页面,在error的渲染类中我们捕获异常然后简单分分类就可以了(如果没有其他要求)
/** * 出错页面渲染 * @param modelMap * @param request */ @RequestMapping(value="/error.do",method = RequestMethod.GET) public void processErrorPage(ModelMap modelMap, HttpServletRequest request) { String code = request.getParameter("errorCode"); modelMap.addAttribute("message", code); AuthenticationException ae = (AuthenticationException)request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION"); if(ae instanceof UsernameNotFoundException){ modelMap.addAttribute("errMsg","用户不存在"); } else if(ae instanceof BadCredentialsException){ modelMap.addAttribute("errMsg","身份检查不通过"); } return; }
这里还在优化进行中,暂时这样子吧。