Spring Security2.04+OpenID配置攻略
目 录
1.QQ互联中的OpenID
QQ 互联接口中,在QQ登陆成功后,会返回一个OpenID,但是此OpenID是一个大写字母和数字的组合。与一般的OpenID的格式不同,正常的OpenID应该是http开头的URL格式,例如http://zhangsan.openid.org.cn/ ,本文介绍的是国际通用的OpenID认证协议+Spring Security配置过程。
2.配置环境
本文的介绍的配置过程是在OpenJWeb2.63环境下配置的,OpenJWeb2.63集成了S2SH和Spring Security2.0.4,大家也可以自己搭建一个S2SH+Spring Security2.04的环境,在环境搭建好以后,我们需要从网上找到openid4java-0.9.8.jar,放到WEB-INF\lib目录下。有了这个驱动,我们就可以在spring security中进行openID的认证登陆。
另外,我们需要将spring-security-openid-2.0.4.jar放到lib目录中。
在web.xml文件中,我们需要增加一段配置以支持openID认证过滤器:
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/j_spring_openid_security_check</url-pattern>
</filter-mapping>
3、Spring Security的xml配置
特别注意Spring Security2.0.4的头部配置,因为网上版本众多,有的是以<bean>作为bean标签,有的是以<beans:bean> 作为bean标签,如果不设置正确的xml头部,则xml中无法使用像
<http>
<intercept-url pattern="/test/**" access="ROLE_USER"/>
<openid-login/>
</http>
这种标签。可以参考下面的xml头:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"
default-autowire="byType" default-lazy-init="true">
网上有很多pdf格式的spring security配置文档,里面介绍了
<http>
<intercept-url pattern="/test/**" access="ROLE_USER"/>
<openid-login/>
</http>
使用 <openid-login/>就可以引入OpenID认证。
但是经过试验发现,使用 <openid-login/>以后,通过spring_openid_security_check进行OpenID认证,则无法使用j_spring_security_check做最常用的本地数据库认证。显然我们希望是两者同时支持。所以我们就不能使用缺省的方式,因此上面的<http>那段配置可以在xml中删除。
参考现有的authenticationProcessingFilter的配置方式,我们需要增加以下配置来满足openID认证授权:
<beans:bean id="springSecurityFilterChain"
class="org.springframework.security.util.FilterChainProxy">
<beans:property name="filterInvocationDefinitionSource">
<beans:value><![CDATA[
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,openIDAuthenticationFilter,securityContextHolderAwareRequestFilter,exceptionTranslationFilter,filterSecurityInterceptor
]]></beans:value>
</beans:property>
</beans:bean>
上面蓝字部分是增加了一个openIDAuthenticationFilter ,下面是增加的这个bean的配置:
<beans:bean id="openIDAuthenticationFilter"
class="org.springframework.security.ui.openid.OpenIDAuthenticationProcessingFilter"
p:authenticationManager-ref="authenticationManager"
p:authenticationFailureUrl="/secure/openid.jsp"
p:defaultTargetUrl="/apps/v22/ui/frame/main.jsp"
p:filterProcessesUrl="/j_spring_openid_security_check"
/>
p:authenticationFailureUrl指明认证失败跳转的页面。p:defaultTargetUrl指明认证成功跳转的页面。p:filterProcessesUrl指明openID的认证过滤器URL.
在authenticationManager Bean中,原来有<beans:ref bean="daoAuthenticationProvider"/> ,需要增加一个针对OpenID的Provider,见下文(蓝字是针对OpenID增加的Provider):
<beans:bean id="authenticationManager"
class="org.springframework.security.providers.ProviderManager"
p:sessionController-ref="concurrentSessionController">
<beans:property name="providers">
<beans:list>
<beans:ref bean="daoAuthenticationProvider"/>
<beans:ref bean="openidAuthenticationProvider"/>
</beans:list>
</beans:property>
</beans:bean>
下面我们要设置openidAuthenticationProvider Bean:
<beans:bean id="openidAuthenticationProvider"
class="org.springframework.security.providers.openid.OpenIDAuthenticationProvider">
</beans:bean>
完整的配置(openjweb中此配置文件为D:\project\openjweb\src\java\applicationContext-security-new.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd"
default-autowire="byType" default-lazy-init="true">
<!--去掉rememberme-->
<beans:bean id="springSecurityFilterChain"
class="org.springframework.security.util.FilterChainProxy">
<beans:property name="filterInvocationDefinitionSource">
<beans:value><![CDATA[
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,openIDAuthenticationFilter,securityContextHolderAwareRequestFilter,exceptionTranslationFilter,filterSecurityInterceptor
]]></beans:value>
</beans:property>
</beans:bean>
<beans:bean id="openIDAuthenticationFilter"
class="org.springframework.security.ui.openid.OpenIDAuthenticationProcessingFilter"
p:authenticationManager-ref="authenticationManager"
p:authenticationFailureUrl="/secure/openid.jsp"
p:defaultTargetUrl="/apps/v22/ui/frame/main.jsp"
p:filterProcessesUrl="/j_spring_openid_security_check"
/>
<beans:bean id="httpSessionContextIntegrationFilter"
class="org.springframework.security.context.HttpSessionContextIntegrationFilter"/>
<!--
退出(Logout)过滤器 退出登录操作
-->
<beans:bean id="logoutFilter"
class="org.springframework.security.ui.logout.LogoutFilter">
<!-- 退出系统后系统跳转到此URL -->
<beans:constructor-arg value="/common/chooseLogin.jsp"/>
<!-- 退出系统后的操作(调用logout方法) -->
<beans:constructor-arg>
<beans:list>
<!-- 实现了LogoutHandler接口(logout方法) -->
<beans:ref bean="rememberMeServices"/>
<beans:bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler"/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="authenticationProcessingFilter"
class="org.openjweb.core.springsecurity.UserAuthenticationProcessingFilter"
p:authenticationManager-ref="authenticationManager"
p:authenticationFailureUrl="/common/chooseLogin.jsp?login_error=1"
p:defaultTargetUrl="/apps/v22/redirect.jsp"
p:filterProcessesUrl="/j_spring_security_check"
p:rememberMeServices-ref="rememberMeServices"/>
<beans:bean id="authenticationManager"
class="org.springframework.security.providers.ProviderManager"
p:sessionController-ref="concurrentSessionController">
<beans:property name="providers">
<beans:list>
<beans:ref bean="daoAuthenticationProvider"/>
<beans:ref bean="openidAuthenticationProvider"/>
<!-- 去掉rememberme 查看是否还有超时问题
<bean class="org.springframework.security.providers.anonymous.AnonymousAuthenticationProvider"
p:key="springsecurity"/>
<bean class="org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider"
p:key="springsecurity"/> -->
</beans:list>
</beans:property>
</beans:bean>
<!-- 阻止用户在成功登录之后再进行一次成功登录 -->
<beans:bean id="concurrentSessionController"
class="org.springframework.security.concurrent.ConcurrentSessionControllerImpl"
p:maximumSessions="1000"
p:exceptionIfMaximumExceeded="true"
p:sessionRegistry-ref="sessionRegistry"
p:messageSource-ref="messageSource"/>
<beans:bean id="sessionRegistry"
class="org.springframework.security.concurrent.SessionRegistryImpl"/>
<beans:bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource"
p:basename="/WEB-INF/classes/messageResource_zh_CN"/>
<beans:bean id="securityContextHolderAwareRequestFilter"
class="org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter"/>
<beans:bean id="rememberMeProcessingFilter"
class="org.springframework.security.ui.rememberme.RememberMeProcessingFilter"
p:authenticationManager-ref="authenticationManager"
p:rememberMeServices-ref="rememberMeServices"/>
<!--<http>
<intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
</http>-->
<beans:bean id="anonymousProcessingFilter"
class="org.springframework.security.providers.anonymous.AnonymousProcessingFilter"
p:key="springsecurity"
p:userAttribute="anonymousUser,ROLE_ANONYMOUS"/>
<beans:bean id="exceptionTranslationFilter"
class="org.springframework.security.ui.ExceptionTranslationFilter"
p:accessDeniedHandler-ref="accessDeniedHandler"
p:authenticationEntryPoint-ref="authenticationEntryPoint"/>
<beans:bean id="accessDeniedHandler"
class="org.springframework.security.ui.AccessDeniedHandlerImpl"
p:errorPage="/accessDenied.jsp"/>
<beans:bean id="authenticationEntryPoint"
class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint"
p:loginFormUrl="/common/chooseLogin.jsp"
p:forceHttps="false"/>
<beans:bean id="userCache"
class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache">
<beans:property name="cache" ref="userCacheBacked" />
</beans:bean>
<beans:bean id="userCacheBacked"
class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<beans:property name="cacheManager" ref="cacheManager" />
<beans:property name="cacheName" value="userCache" />
</beans:bean>
<beans:bean id="cacheManager"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<beans:property name="configLocation"
value="classpath:ehcache-security.xml" />
</beans:bean>
<beans:bean id="filterSecurityInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
<security:custom-filter before="FILTER_SECURITY_INTERCEPTOR" />
<beans:property name="authenticationManager"
ref="authenticationManager" />
<beans:property name="accessDecisionManager"
ref="accessDecisionManager" />
<beans:property name="alwaysReauthenticate" value="true" />
<beans:property name="objectDefinitionSource"
ref="databaseFilterInvocationDefinitionSource" />
</beans:bean>
<beans:bean id="accessDecisionManager"
class="org.springframework.security.vote.AffirmativeBased">
<beans:property name="decisionVoters">
<beans:list>
<beans:bean
class="org.springframework.security.vote.RoleVoter">
<beans:property name="rolePrefix" value="" />
</beans:bean>
<!-- <bean class="org.springframework.security.vote.AuthenticatedVoter"/> --> <!--解决权限不足出现异常信息-->
</beans:list>
</beans:property>
</beans:bean>
<beans:bean id="rememberMeServices"
class="org.springframework.security.ui.rememberme.TokenBasedRememberMeServices"
p:key="springsecurity"
p:userDetailsService-ref="userDetailsService"/>
<!-- <property name="tokenValiditySeconds" value="86400" /> -->
<beans:bean id="daoAuthenticationProvider"
class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="userDetailsService" />
<beans:property name="userCache" ref="userCache" />
<beans:property name="passwordEncoder" ref="passwordEncoder" />
</beans:bean>
<beans:bean id="openidAuthenticationProvider"
class="org.springframework.security.providers.openid.OpenIDAuthenticationProvider">
<!--<beans:property name="userDetailsService" ref="userDetailsService" />
<beans:property name="userCache" ref="userCache" />
<beans:property name="passwordEncoder" ref="passwordEncoder" />-->
</beans:bean>
<beans:bean id="passwordEncoder"
class="org.springframework.security.providers.encoding.Md5PasswordEncoder"/>
<beans:bean id="userDetailsService"
class="org.openjweb.core.springsecurity.UserDetailsServiceImpl">
<beans:constructor-arg>
<beans:ref bean="IBaseDao3" />
</beans:constructor-arg>
</beans:bean>
<beans:bean id="databaseFilterInvocationDefinitionSource"
class="org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource">
<!-- 匹配url的matcher -->
<beans:constructor-arg
type="org.springframework.security.util.UrlMatcher"
ref="antUrlPathMatcher" />
<!-- url对应authority的map -->
<beans:constructor-arg type="java.util.LinkedHashMap" ref="requestMap" />
</beans:bean>
<beans:bean id="antUrlPathMatcher"
class="org.springframework.security.util.AntUrlPathMatcher" />
<beans:bean id="requestMap"
class="org.openjweb.core.springsecurity.RequestMapFactoryBean"
init-method="init">
</beans:bean>
<beans:bean id="loggerListener"
class="org.springframework.security.event.authentication.LoggerListener"/>
</beans:beans>
注意配置文件中的这段代码:
<beans:bean id="userDetailsService"
class="org.openjweb.core.springsecurity.UserDetailsServiceImpl">
<beans:constructor-arg>
<beans:ref bean="IBaseDao3" />
</beans:constructor-arg>
</beans:bean>
org.openjweb.core.springsecurity.UserDetailsServiceImpl是OpenJWeb中实现Spring Security 的UserDetailsService接口的实现类。我们 此实现类中增加一段代码,此段代码获取openID认证以后的OpenID账号,检查数据库表comm_user中是否存在此账号,如果不存在,则认证成功后自动向数据库表添加一条账号记录,并且为此账号分配一个网站会员角色。下面是org.openjweb.core.springsecurity.UserDetailsServiceImpl的代码:
package org.openjweb.core.springsecurity;
import org.apache.log4j.Logger;
import org.openjweb.core.dao.IBaseDao;
import org.openjweb.core.entity.CommUser;
import org.openjweb.core.entity.CommUserRole;
import org.openjweb.core.service.IDBSupportService;
import org.openjweb.core.service.ServiceLocator;
import org.openjweb.core.util.StringUtil;
import org.springframework.dao.DataAccessException;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UsernameNotFoundException;
public class UserDetailsServiceImpl implements IUserService
{
private static final Logger logger = Logger.getLogger(UserDetailsServiceImpl.class);
private IBaseDao defaultDao;
public UserDetailsServiceImpl (IBaseDao dao)
{
this.defaultDao = dao;
}
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException
{
CommUser user = null;
try
{
user = (CommUser)this.defaultDao.findById(CommUser.class.getName(), "loginId", userName);
if(user.getIsInUse()!=null&&user.getIsInUse().equals("Y"))
{
}
else
{
logger.info("用户被锁定......");
throw new UsernameNotFoundException(userName); //被锁定
}
}
catch(Exception ex)
{
//检查如果是openid格式的,则生成一个对应的openid用户--必填项先默认生成,用户再自己修改
if(userName.startsWith("http://")||userName.startsWith("https://"))
{
IDBSupportService service = (IDBSupportService)ServiceLocator.getBean("IDBSupportService3");
user = new CommUser();
Long serial = null;
try
{
serial = service.getSerial();
}
catch(Exception ex1)
{
//
}
user.setUserId(serial);
user.setRowId(StringUtil.getUUID());
user.setLoginId(userName);
user.setComId("C0001");
user.setIsPortalMember("Y");//设置为网站会员,这种情况一般是网站会员
user.setUsername(userName);//暂时以用户id作为用户名
user.setUserEmail(serial.toString()+"openjweb.com");//生成一个默认的
user.setPassword("unused");
user.setUserMobile(serial.toString());//生成一个默认的由用户自己修改
user.setIsInUse("Y");
user.setEmpNo(serial.toString());//工号
user.setUpdateUid("system");
user.setCreateUid("system");
user.setCreateDt(StringUtil.getCurrentDateTime());
user.setUpdateDt(user.getCreateDt());
user.setCurrFrameCode("06");
user.setCurrSkinCode("sky");
try
{
service.saveOrUpdate(user);
//然后分配门户网站会员角色
CommUserRole roleEnt = new CommUserRole();
roleEnt.setCreateDt(StringUtil.getCurrentDateTime());
roleEnt.setSerialNo(service.getSerial());
roleEnt.setRoleId(new Long(505715));
roleEnt.setUserId(user.getUserId());
service.saveOrUpdate(roleEnt);
}
catch(Exception ex2)
{
}
}
else
{
throw new UsernameNotFoundException(userName);
}
}
return user;
}
}
4、OpenID登录页
编写一个jsp,例如openid.jsp,下面是此jsp中的一段form内容:
<form action="/portal/j_spring_openid_security_check" method="post">
<label for="openid_identifier">Login</label>:
<input id="openid_identifier1" name="j_username" size="20" maxlength="100" type="text"/>
<input type="submit" value="Login"/>
</form>
说明:/portal/指的是openjweb的应用路径,如果在webapps根目录可使用/j_spring_openid_security_check ,OpenID输入框的name必须为j_username。
5、申请OpenID 账号
我们可以到谷歌、微软等openID提供商注册一个openID账号,或者在http://www.openid.org.cn上注册一个账号,例如注册一个baozhengw的账号,则完整的OpenID账号则为: http://baozhengw.openid.org.cn/
6、在本地登录OpenID
运行第四步创建的JSP,见下图:
输入:http://baozhengw.openid.org.cn/
然后点登录,系统会跳转到OpenID服务器(系统会根据openid的域名跳转到对应的服务器),见下图(跳转到http://www.openid.org.cn/):
输入baozhengw,和注册时的口令,然后点登录,自动跳转到http://localhost:8088/portal/apps/v22/ui/frame/main.jsp(在配置文件中通过redirect.jsp跳转到后台,因为新增用户自动添加了关联角色,所以能进入后台):
7、其他注意事项
SecurityContextImpl securityContext =(SecurityContextImpl)obj;
Object tmpObj = null;
tmpObj = securityContext.getAuthentication().getPrincipal();
以传统的方式登录,即/j_spring_security_check本地数据库认证的模式,通过getPrincipal()实际返回的是UserDetailsService接口的实现类,在OpenJWeb中是CommUser类,而使用OpenID,则getPrincipal()返回的是OpenID账号,如http://baozhengw.openid.org.cn/ ,所以产品支持OpenID以后,getPrincipal()的返回值类型处理要考虑多种类型。
OpenJWeb 开源组织
QQ 29803446