首页 > 代码库 > S - 使用SpringSecurity3用户验证几点体会(异常信息,验证码)

S - 使用SpringSecurity3用户验证几点体会(异常信息,验证码)

1. 自定义user-service后,封装自定义异常信息返回

 

通常情况下,抛UsernameNotFoundException异常信息是捕捉不了,跟踪源码后发现

 

 

Java代码  收藏代码

  1. try {  

  2.     user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);  

  3. catch (UsernameNotFoundException notFound) {  

  4.     logger.debug("User ‘" + username + "‘ not found");  

  5.   

  6.     if (hideUserNotFoundExceptions) {  

  7.         throw new BadCredentialsException(messages.getMessage(  

  8.                 "AbstractUserDetailsAuthenticationProvider.badCredentials""Bad credentials"));  

  9.     } else {  

  10.         throw notFound;  

  11.     }  

  12. }  

 

 

而默认情况下,hideUserNotFoundExceptions为true。所以就会导致明明抛UsernameNotFoundException,但前台还是只能捕获Bad credentials的问题。

 

解决办法我们可以直接覆盖org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider的类,然后修改hideUserNotFoundExceptions为false。

 

当然,这样的解决办法并不好。所以,我们还是走正规的途径,自定义org.springframework.security.authentication.dao.DaoAuthenticationProvider来替换默认的即可,即修改配置文件并定义provider,这就是IoC的伟大之处。

 

原来authentication-manager中简单的定义user-service-ref

 

 

Xml代码  收藏代码

  1. <authentication-manager alias="authenticationManager">  

  2.     <authentication-provider user-service-ref="myUserDetailsService">  

  3.         <!-- 密码加密方式  -->  

  4.         <password-encoder hash="md5" />  

  5.     </authentication-provider>  

  6. </authentication-manager>  

 

 

 

现在修改如下:

 

 

 

Xml代码  收藏代码

  1. <authentication-manager alias="authenticationManager">  

  2.     <authentication-provider ref="authenticationProvider" />  

  3. </authentication-manager>  

  4.   

  5. <b:bean id="authenticationProvider"  

  6.     class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">  

  7.     <b:property name="userDetailsService" ref="myUserDetailsService" />  

  8.     <b:property name="hideUserNotFoundExceptions" value="false" />  

  9.     <b:property name="passwordEncoder" ref="passwordEncoder"></b:property>  

  10. </b:bean>  

  11.   

  12. <b:bean  

  13.     class="org.springframework.security.authentication.encoding.Md5PasswordEncoder"  

  14.     id="passwordEncoder"></b:bean>  

 

 

这样修改后,在登录页面获取的异常已经是自己抛出去的UsernameNotFoundException了。

 

(注:这里保留了md5加密方式,但是原始的加密,没加salt,之后会继续修改为安全性高一些的md5+salt加密。现在这世道普通的md5加密和明文没多大区别。)

 

2. 国际化资源i18n信息

 

若想封装国际化资源信息到页面(不想打硬编码信息到代码内),又不想自己构造Properties对象的话,可以参考SpringSecurity3中的获取资源文件方法。(也是看源码的时候学习到的)

 

在SpringSecurity3中的message都是通过这样的方式得到的:

 

 

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

通过提供的静态方法,我们很方便的得到国际化资源信息。但无奈SpringSecurityMessageSource硬编码写死了只是获取org.springframework.security.messages的资源文件(英文信息)。如下:

Java代码  收藏代码

  1. public SpringSecurityMessageSource() {  

  2.     setBasename("org.springframework.security.messages");  

  3. }  

 

通常情况下,这个并不符合我们的使用,并且很多情况下,使用SpringSecurity3自定义抛出的异常信息的话,也会出现不符合语言习惯的信息。

所以,这里是建议覆盖org.springframework.security.core.SpringSecurityMessageSource类,并指定获取应用中的默认国际化资源文件。

不过,你还是不想覆盖别人的类的话,也还可以自己模仿SpringSecurityMessageSource编写自己的获取MessageSourceAccessor的类,例如我就是这么做....

Java代码  收藏代码

  1. public class SpringMessageSource extends ResourceBundleMessageSource {  

  2.     // ~ Constructors  

  3.     // ===================================================================================================  

  4.   

  5.     public SpringMessageSource() {  

  6.         setBasename("com.foo.resources.messages_zh_CN");  

  7.     }  

  8.   

  9.     // ~ Methods  

  10.     // ========================================================================================================  

  11.   

  12.     public static MessageSourceAccessor getAccessor() {  

  13.         return new MessageSourceAccessor(new SpringMessageSource());  

  14.     }  

  15. }  

 

这样,我们就可以在自定义的userDetailsService类中,像SpringSecurity3那样方便的使用国际化资源文件了。

如:

Java代码  收藏代码

  1.     private MessageSourceAccessor messages = SpringMessageSource.getAccessor();  

  2.   

  3. ....  

  4.   

  5.     public UserDetails loadUserByUsername(String username)  

  6.             throws UsernameNotFoundException, DataAccessException {  

  7.         if (StringUtils.isBlank(username)) {  

  8.             throw new UsernameNotFoundException(  

  9.                     messages.getMessage("PasswordComparisonAuthenticator.badCredentials"),  

  10.                     username);  

  11.         }  

  12.   

  13. ...  

  14.   

  15. }  

 

3.添加验证码

在实际应用中,其实验证码是少不了的,不然很容易就被暴力破解了。添加验证码起码也可以增加一点安全性,而且添加验证码也比较简单。

添加自定义UsernamePasswordAuthenticationFilter,在验证username和password之前,我们加入验证码的判定。

在spring-security配置文件中的<http>代码块中添加

Xml代码  收藏代码

  1. <custom-filter before="FORM_LOGIN_FILTER" ref="validateCodeAuthenticationFilter" />  

然后就是在beans内添加定义validateCodeAuthenticationFilter的bean代码

Xml代码  收藏代码

  1. <b:bean id="validateCodeAuthenticationFilter"  

  2.     class="com.foo.security.ValidateCodeAuthenticationFilter">  

  3.     <b:property name="postOnly" value="false"></b:property>  

  4.     <b:property name="authenticationSuccessHandler" ref="loginLogAuthenticationSuccessHandler"></b:property>  

  5.     <b:property name="authenticationFailureHandler" ref="simpleUrlAuthenticationFailureHandler"></b:property>  

  6.     <b:property name="authenticationManager" ref="authenticationManager"></b:property>  

  7. </b:bean>  

  8.   

  9. <b:bean id="loginLogAuthenticationSuccessHandler"  

  10.     class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">  

  11.     <b:property name="defaultTargetUrl" value="/index.do"></b:property>  

  12. </b:bean>  

  13. <b:bean id="simpleUrlAuthenticationFailureHandler"  

  14.     class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">  

  15.     <b:property name="defaultFailureUrl" value="/login.jsp?login_error=1"></b:property>  

  16. </b:bean>  

 

最后是ValidateCodeAuthenticationFilter的源码:

Java代码  收藏代码

  1. public class ValidateCodeAuthenticationFilter extends  

  2.         UsernamePasswordAuthenticationFilter {  

  3.   

  4.     private boolean postOnly = true;  

  5.     private boolean allowEmptyValidateCode = false;  

  6.     private String sessionvalidateCodeField = DEFAULT_SESSION_VALIDATE_CODE_FIELD;  

  7.     private String validateCodeParameter = DEFAULT_VALIDATE_CODE_PARAMETER;  

  8.     public static final String DEFAULT_SESSION_VALIDATE_CODE_FIELD = "validateCode";  

  9.     public static final String DEFAULT_VALIDATE_CODE_PARAMETER = "validateCode";  

  10.     public static final String VALIDATE_CODE_FAILED_MSG_KEY = "validateCode.notEquals";  

  11.   

  12.     @Override  

  13.     public Authentication attemptAuthentication(HttpServletRequest request,  

  14.             HttpServletResponse response) throws AuthenticationException {  

  15.         if (postOnly && !request.getMethod().equals("POST")) {  

  16.             throw new AuthenticationServiceException(  

  17.                     "Authentication method not supported: "  

  18.                             + request.getMethod());  

  19.         }  

  20.   

  21.         String username = StringUtils.trimToEmpty(obtainUsername(request));  

  22.         String password = obtainPassword(request);  

  23.         if (password == null) {  

  24.             password = StringUtils.EMPTY;  

  25.         }  

  26.   

  27.         UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(  

  28.                 username, password);  

  29.   

  30.         // Place the last username attempted into HttpSession for views  

  31.         HttpSession session = request.getSession(false);  

  32.   

  33.         if (session != null || getAllowSessionCreation()) {  

  34.             request.getSession().setAttribute(  

  35.                     SPRING_SECURITY_LAST_USERNAME_KEY,  

  36.                     TextEscapeUtils.escapeEntities(username));  

  37.         }  

  38.   

  39.         // Allow subclasses to set the "details" property  

  40.         setDetails(request, authRequest);  

  41.         // check validate code  

  42.         if (!isAllowEmptyValidateCode())  

  43.             checkValidateCode(request);  

  44.         return this.getAuthenticationManager().authenticate(authRequest);  

  45.     }  

  46.   

  47.     /** 

  48.      *  

  49.      * <li>比较session中的验证码和用户输入的验证码是否相等</li> 

  50.      *  

  51.      */  

  52.     protected void checkValidateCode(HttpServletRequest request) {  

  53.         String sessionValidateCode = obtainSessionValidateCode(request);  

  54.         String validateCodeParameter = obtainValidateCodeParameter(request);  

  55.         if (StringUtils.isEmpty(validateCodeParameter)  

  56.                 || !sessionValidateCode.equalsIgnoreCase(validateCodeParameter)) {  

  57.             throw new AuthenticationServiceException(  

  58.                     messages.getMessage(VALIDATE_CODE_FAILED_MSG_KEY));  

  59.         }  

  60.     }  

  61.   

  62.     private String obtainValidateCodeParameter(HttpServletRequest request) {  

  63.         return request.getParameter(validateCodeParameter);  

  64.     }  

  65.   

  66.     protected String obtainSessionValidateCode(HttpServletRequest request) {  

  67.         Object obj = request.getSession()  

  68.                 .getAttribute(sessionvalidateCodeField);  

  69.         return null == obj ? "" : obj.toString();  

  70.     }  

  71.   

  72.     public boolean isPostOnly() {  

  73.         return postOnly;  

  74.     }  

  75.   

  76.     @Override  

  77.     public void setPostOnly(boolean postOnly) {  

  78.         this.postOnly = postOnly;  

  79.     }  

  80.   

  81.     public String getValidateCodeName() {  

  82.         return sessionvalidateCodeField;  

  83.     }  

  84.   

  85.     public void setValidateCodeName(String validateCodeName) {  

  86.         this.sessionvalidateCodeField = validateCodeName;  

  87.     }  

  88.   

  89.     public boolean isAllowEmptyValidateCode() {  

  90.         return allowEmptyValidateCode;  

  91.     }  

  92.   

  93.     public void setAllowEmptyValidateCode(boolean allowEmptyValidateCode) {  

  94.         this.allowEmptyValidateCode = allowEmptyValidateCode;  

  95.     }  

  96.   

  97. }  

 

附件中有生成CODE图片的JSP(相对比较简单的,但基本可以满足应用),还有文章中用到的一些关键配置文件与源码。

生成验证码的jsp页面调用时直接<img src="http://www.mamicode.com/validateCode.jsp"  />即可,但刷新时,记得在URL上增加随机数的参数,不然会有缓存导致刷新失败。

添加验证码部分有参考:http://www.iteye.com/topic/720867


S - 使用SpringSecurity3用户验证几点体会(异常信息,验证码)