Spring Security
使用Spring Security 3 来实现多种用户类型的登录方式,在我看来,大致分为两大步,第一步是控制多种用户登录类型的登录界面的展示,第二步是控制多种用户登录的验证方式,而第二大步又可分为三个小步来展开,第一小步是验证过滤器,第二小步是验证凭证的制作,第三小步是验证用户凭证,在这里我将就这几步的分析与实现一步一步展开来进行描述。
这里我假定有这样一个系统,分为前台用户和后台用户两种用户类型,针对这两种不同的用户,相应的验证是不一样的,前台用户登录需要提供邮箱地址和电话号码,而后台用户需要提供我们在登录方式中最为常见的用户名,密码和验证码,两种登录类型的登录界面和登录的验证处理流程都不相同,拥有自己的验证处理,验证成功的处理,验证失败的处理等。这里我会用四篇文章来详细阐述整个验证的流程实现,每一篇文章后都会附有我的项目压缩文件,有兴趣的朋友可以下载来进行尝试,我使用的是Maven来管理项目,项目的框架是Spring+Hibernate,使用到的开发工具是IntelliJ,这里我要给IntelliJ打个广告,当然是无偿的,呵呵,在这之前我曾使用过NetBeans,Eclipse等开发工具,IntelliJ是我使用过的最为优秀的开发工具,有兴趣的同学可以尝试下。
Spring Security 3多用户登录实现之二 多登录界面展示
接前讲,首先针对一个多种用户类型的登录需求,需要先实现多种用户类型的登录界面的展示,Spring Security提供了这样一个接口来帮助我们实现多种用户类型的登录界面的展示,这个接口就是AuthenticationEntryPoint, 实现这样一个接口,我们就可以随心所欲的控制登录界面的展示了,当我们访问一个受权限的资源,而当前又没有权限访问时,Spring Security就会将处理导向这个接口的实现。针对前讲我所提到的需求,在这里我将实现前台用户和后台用户登录界面的展示,先来看看我的源码实现吧,在这里为了实现多用户类型的登录,很多场景我都需要根据相应的请求参数或地址来判断我需要导向哪个URL地址,我在这里特实现了一个共用的接口和类,接口名为DirectUrlResolver。
package com.template.security.shared; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午7:11 */ public interface DirectUrlResolver { boolean support(HttpServletRequest request); String directUrl(); }
package com.template.security.shared; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午7:12 */ public abstract class AbstractDirectUrlResolver implements DirectUrlResolver { protected String pattern; protected String directUrl; @Override public abstract boolean support(HttpServletRequest request); @Override public String directUrl() { return this.directUrl; } public void setPattern(String pattern) { this.pattern = pattern; } public void setDirectUrl(String directUrl) { this.directUrl = directUrl; } }
package com.template.security.shared; import com.template.utils.StringUtils; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午7:13 */ public class RequestParameterDirectUrlResolver extends AbstractDirectUrlResolver { private String parameterName; @Override public boolean support(HttpServletRequest request) { String parameterValue = request.getParameter(parameterName); if (StringUtils.isEmpty(parameterValue)) { return false; } return parameterValue.equals(this.pattern); } public void setParameterName(String parameterName) { this.parameterName = parameterName; } }
package com.template.security.shared; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午7:13 */ public class RequestUriDirectUrlResolver extends AbstractDirectUrlResolver { @Override public boolean support(HttpServletRequest request) { String requestURI = request.getRequestURI(); return requestURI.contains(this.pattern); } }
RequestParameterDirectUrlResolver和RequestUriDirectUrlResolver都实现了DirectUrlResolver这样一个接口,前者的实现是根据相应请求中的参数来判断, 而后者的实现是根据相应的请求地址来判断。
现在让我们来看看如何通过实现AuthenticationEntryPoint接口来控制什么时候展示前台登录界面,什么时候展示后台登录界面的吧。
package com.template.security.login; import com.template.security.shared.DirectUrlResolver; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午7:40 */ public class MultipleAuthenticationLoginEntry implements AuthenticationEntryPoint { private String defaultLoginUrl; private List<DirectUrlResolver> directUrlResolvers = new ArrayList<DirectUrlResolver>(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { for (DirectUrlResolver directUrlResolver : directUrlResolvers) { if (directUrlResolver.support(request)) { String loginUrl = directUrlResolver.directUrl(); response.sendRedirect(loginUrl); return; } } response.sendRedirect(defaultLoginUrl); } public void setDefaultLoginUrl(String defaultLoginUrl) { this.defaultLoginUrl = defaultLoginUrl; } public void setDirectUrlResolvers(List<DirectUrlResolver> directUrlResolvers) { this.directUrlResolvers = directUrlResolvers; } }
再来看看在Spring配置文件中是如何对相应的登录入口进行配置的吧
package com.template.security.filter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午10:00 */ public class MultipleAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { private List<AuthenticationTokenResolver> tokenResolvers = new ArrayList<AuthenticationTokenResolver>(); /** * @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>. */ protected MultipleAuthenticationProcessingFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { for (AuthenticationTokenResolver tokenResolver : tokenResolvers) { if (tokenResolver.support(request)) { Authentication authentication = tokenResolver.resolve(request); return this.getAuthenticationManager().authenticate(authentication); } } throw new UnsupportedOperationException("No authentication token resolver found!"); } public void setTokenResolvers(List<AuthenticationTokenResolver> tokenResolvers) { this.tokenResolvers = tokenResolvers; } }
package com.template.security.filter; import org.springframework.security.core.Authentication; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午10:08 */ public interface AuthenticationTokenResolver { boolean support(HttpServletRequest request); Authentication resolve(HttpServletRequest request); }
package com.template.security.filter; import com.template.utils.StringUtils; import org.springframework.security.core.Authentication; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午10:27 */ public abstract class AbstractAuthenticationTokenResolver implements AuthenticationTokenResolver { protected String parameterName; protected String parameterValue; protected AbstractAuthenticationTokenResolver() { } protected AbstractAuthenticationTokenResolver(String parameterName) { this.parameterName = parameterName; } @Override public boolean support(HttpServletRequest request) { String parameterValue = request.getParameter(parameterName); if (StringUtils.isEmpty(parameterValue)) { return false; } return parameterValue.equals(this.parameterValue); } @Override public abstract Authentication resolve(HttpServletRequest request); public void setParameterName(String parameterName) { this.parameterName = parameterName; } public void setParameterValue(String parameterValue) { this.parameterValue = parameterValue; } }
package com.template.security.filter; import com.template.security.authentication.token.BackendAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午10:29 */ public class BackendAuthenticationTokenResolver extends AbstractAuthenticationTokenResolver { protected BackendAuthenticationTokenResolver() { super(); } @Override public Authentication resolve(HttpServletRequest request) { String username = request.getParameter("username"); String password = request.getParameter("password"); String captcha = request.getParameter("captcha"); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); return new BackendAuthenticationToken(username, password, authorities, captcha); } }
package com.template.security.filter; import com.template.security.authentication.token.ForendAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午10:29 */ public class ForendAuthenticationTokenResolver extends AbstractAuthenticationTokenResolver { protected ForendAuthenticationTokenResolver() { super(); } @Override public Authentication resolve(HttpServletRequest request) { String email = request.getParameter("email"); String phone = request.getParameter("phone"); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); return new ForendAuthenticationToken(email, phone, authorities); } }
这里实现AbstractAuthenticationProcessingFilter接口的类MultipleAuthenticationProcessingFilter,用来根据相应的登录表单提交信息构造相应的登录用户凭证。为了实现根据前台登录表单信息构造前台用户凭证,根据后台登录表单信息构造后台用户凭证,使用了策略模式来实现,实现AbstractAuthenticationTokenResolver接口的BackendAuthenticationTokenResolver和ForendAuthenticationTokenResolver分别用来构造后台用户凭证和前台用户凭证,再来看看配置文件是如何进行配置的吧。
package com.template.security.authentication.token; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-4 * Time: 下午11:23 */ public class ForendAuthenticationToken extends AbstractAuthenticationToken { private String email; private String phone; public ForendAuthenticationToken(String email, String phone, List<GrantedAuthority> grantedAuthorities) { super(grantedAuthorities); this.email = email; this.phone = phone; } public String getEmail() { return email; } public String getPhone() { return phone; } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return null; } }
package com.template.security.authentication.token; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-4 * Time: 下午11:24 */ public class BackendAuthenticationToken extends UsernamePasswordAuthenticationToken { private String captcha; public BackendAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String captcha) { super(principal, credentials, authorities); this.captcha = captcha; } public String getCaptcha() { return captcha; } }
为了标志我们创建的类为一个用户凭证并且能够被Spring Security识别,我们需要实现Authentication接口,因为Spring Security为我们提供了一个抽象的凭证,所以这里我的前台凭证就继承了这样一个抽象的凭证类,名为AbstractAuthenticationToken, 根据前台表单的提交信息有邮件地址和电话号码,所以实现的ForendAuthenticationToken包含这样两个属性,而Spring Security为我们提供了常见的用户名和密码的登录凭证实现,我的后台登录表单信息只是多了一个验证码,所以我直接继承了UsernamePasswordAuthenticationToken这样一个类。
Spring Security 3多用户登录实现之五 验证用户凭证
有了用户凭证后, 如何验证用户的凭证是否正确呢, 这就需要借助AuthenticationManager了, AuthenticationManager可以包含多个AuthenticationProvider, 每个AuthenticationProvider都会针对特定的AuthenticationToken, 也就是用户凭证来验证相应的用户凭证是否正确。
来看看我为了实现验证前台用户凭证和后台用户凭证而实现的AuthenticationProvider吧。
package com.template.security.authentication.provider; import com.template.security.authentication.token.BackendAuthenticationToken; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-4 * Time: 下午11:16 */ public class BackendAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { BackendAuthenticationToken authenticationToken = (BackendAuthenticationToken) authentication; // String captcha = authenticationToken.getCaptcha(); // if (captcha.startsWith("ZZ")) { // throw new AuthenticationServiceException("The captcha is wrong!"); // } String username = (String) authenticationToken.getPrincipal(); String password = (String) authenticationToken.getCredentials(); if (username.equalsIgnoreCase("ZHONGGANG") && password.equalsIgnoreCase("123")) { SecurityContextHolder.getContext().setAuthentication(authenticationToken); return authenticationToken; } throw new AuthenticationServiceException("The username or password is not correct!"); } @Override public boolean supports(Class<?> authentication) { return BackendAuthenticationToken.class.isAssignableFrom(authentication); } }
package com.template.security.authentication.provider; import com.template.security.authentication.token.ForendAuthenticationToken; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-4 * Time: 下午11:16 */ public class ForendAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { ForendAuthenticationToken authenticationToken = (ForendAuthenticationToken) authentication; String email = authenticationToken.getEmail(); String phone = authenticationToken.getPhone(); if (email.endsWith("@qq.com") && phone.startsWith("139")) { authenticationToken.setAuthenticated(true); SecurityContextHolder.getContext().setAuthentication(authenticationToken); return authenticationToken; } throw new AuthenticationServiceException("The email or phone is not correct!"); } @Override public boolean supports(Class<?> authentication) { return ForendAuthenticationToken.class.isAssignableFrom(authentication); } }
不论是前台用户凭证验证还是后台用户凭证验证,都实现了AuthenticationProvider接口,其中的supports方法表明这个AuthenticationProvider需要对哪个类型的用户凭证进行验证。这里我只是进行了一个简单的验证,没有什么实际意义,如果你的验证需要与数据库打交道,你可以在AuthenticationProvider中注入你的服务。来看看配置文件中的相应配置信息吧。
package com.template.security.authentication.handler; import com.template.security.shared.DirectUrlResolver; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午11:20 */ public class MultipleAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private List<DirectUrlResolver> resolvers = new ArrayList<DirectUrlResolver>(); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { for (DirectUrlResolver resolver : resolvers) { if (resolver.support(request)) { String directUrl = resolver.directUrl(); setDefaultFailureUrl(directUrl); } } super.onAuthenticationFailure(request, response, exception); } public void setResolvers(List<DirectUrlResolver> resolvers) { this.resolvers = resolvers; } }
验证成功的处理需要实现AuthenticationSuccessHandler接口,我的后台验证成功处理是这样的
package com.template.security.authentication.handler; import com.template.security.shared.DirectUrlResolver; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Created by IntelliJ IDEA. * User: Zhong Gang * Date: 12-11-9 * Time: 下午11:20 */ public class MultipleAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private List<DirectUrlResolver> resolvers = new ArrayList<DirectUrlResolver>(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { for (DirectUrlResolver resolver : resolvers) { if (resolver.support(request)) { String directUrl = resolver.directUrl(); setDefaultTargetUrl(directUrl); } } super.onAuthenticationSuccess(request, response, authentication); } public void setResolvers(List<DirectUrlResolver> resolvers) { this.resolvers = resolvers; } }
不论是前台验证成功还是后台验证成功,前台验证失败还是后台验证失败我都有不同的处理,前台验证成功导向前台验证成功界面,后台验证成功导向后台验证成功界面, 前台验证失败导向前台登录界面, 后台验证失败导向后台登录界面,所以这里我使用了前面我书写的一个通用接口,也就是DirectUrlResolver。来看看验证处理成功或失败的配置信息。
- <beans:bean id="multipleAuthenticationSuccessHandler"
- class="com.template.security.authentication.handler.MultipleAuthenticationSuccessHandler">
- <beans:property name="alwaysUseDefaultTargetUrl" value="true"/>
- <beans:property name="resolvers">
- <beans:list>
- <beans:ref bean="backendAuthenticationSuccessUrlResolver"/>
- <beans:ref bean="forendAuthenticationSuccessUrlResolver"/>
- </beans:list>
- </beans:property>
- </beans:bean>
- <beans:bean id="backendAuthenticationSuccessUrlResolver"
- class="com.template.security.shared.RequestParameterDirectUrlResolver">
- <beans:property name="parameterName" value="token"/>
- <beans:property name="pattern" value="backend"/>
- <beans:property name="directUrl" value="/backend/login/success"/>
- </beans:bean>
- <beans:bean id="forendAuthenticationSuccessUrlResolver"
- class="com.template.security.shared.RequestParameterDirectUrlResolver">
- <beans:property name="parameterName" value="token"/>
- <beans:property name="pattern" value="forend"/>
- <beans:property name="directUrl" value="/forend/login/success"/>
- </beans:bean>
- <beans:bean id="multipleAuthenticationFailureHandler"
- class="com.template.security.authentication.handler.MultipleAuthenticationFailureHandler">
- <beans:property name="resolvers">
- <beans:list>
- <beans:ref bean="backendAuthenticationFailureUrlResolver"/>
- <beans:ref bean="forendAuthenticationFailureUrlResolver"/>
- </beans:list>
- </beans:property>
- </beans:bean>
- <beans:bean id="backendAuthenticationFailureUrlResolver"
- class="com.template.security.shared.RequestParameterDirectUrlResolver">
- <beans:property name="parameterName" value="token"/>
- <beans:property name="pattern" value="backend"/>
- <beans:property name="directUrl" value="/backend/login?error=1"/>
- </beans:bean>
- <beans:bean id="forendAuthenticationFailureUrlResolver"
- class="com.template.security.shared.RequestParameterDirectUrlResolver">
- <beans:property name="parameterName" value="token"/>
- <beans:property name="pattern" value="forend"/>
- <beans:property name="directUrl" value="/forend/login?error=1"/>
- </beans:bean>
这里还需要将相应的验证Handler注入到前讲的认证处理Filter中。