第四部分:spring security使用cas单点登录配置
spring security本身就提供了对cas的支持,只需要简单的配置就可以实现单点登录。
由于客户端项目没有使用spring security自带的权限管理,采用了自定义的实现,配置起来比正常的spring security要复杂一些。
(一)spring security域cas集成配置
1、增加cas依赖
需要为客户端项目增加spring-security-cas的依赖,它会自动把cas-client-core.jar的依赖加入到项目中。
<!-- cas -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
2、增加applicationContext-cas.xml
加入了securityFilter来处理权限,额外注入了一个基础数据库操作类resourcesDao。在需要使用数据库的地方可以根据项目情况来进行注入。注入只需要为属性增加set方法即可。
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p" xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"
default-lazy-init="true">
<!-- 不要过滤图片等静态资源 -->
<http pattern="/**/*.jpg" security="none" />
<http pattern="/**/*.png" security="none" />
<http pattern="/**/*.gif" security="none" />
<http pattern="/**/*.css" security="none" />
<http pattern="/**/*.js" security="none" />
<!--SSO -->
<http entry-point-ref="casEntryPoint" auto-config="true">
<intercept-url pattern="/" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<custom-filter ref="casFilter" after="CAS_FILTER"/>
<!-- 登出过滤器 -->
<custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER" />
<custom-filter ref="singleLogoutFilter" before="CAS_FILTER" />
<!-- 权限过滤器 -->
<custom-filter ref="securityFilter" before="FILTER_SECURITY_INTERCEPTOR" />
</http>
<beans:bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<beans:property name="loginUrl" value="${cas.server}/login" />
<beans:property name="serviceProperties" ref="serviceProperties" />
</beans:bean>
<beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<beans:property name="service" value="${cas.client}/j_spring_cas_security_check" />
<beans:property name="sendRenew" value="false" />
</beans:bean>
<beans:bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationFailureHandler">
<beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<!-- cas登录失败跳转页面,跳转首页 -->
<beans:property name="defaultFailureUrl" value="${cas.client}"/>
</beans:bean>
</beans:property>
<!-- 登录成功处理,将用户信息存入session -->
<beans:property name="authenticationSuccessHandler">
<beans:bean class="com.whty.bwjf.framework.core.cas.CasAuthenticationSuccessHandler">
<!-- cas登录成功后跳转页面 -->
<beans:property name="defaultTargetUrl" value="/admin.jsp"/>
<beans:property name="usersDao" ref="resourcesDao"></beans:property>
<beans:property name="resDao" ref="resourcesDao"></beans:property>
</beans:bean>
</beans:property>
</beans:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="casAuthenticationProvider" />
</authentication-manager>
<beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<!-- 注入获取tsp用户的service -->
<beans:property name="userDetailsService" ref="tspUserDetailServiceImpl" />
<beans:property name="serviceProperties" ref="serviceProperties" />
<beans:property name="ticketValidator">
<beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<beans:constructor-arg index="0" value="${cas.server}" />
</beans:bean>
</beans:property>
<beans:property name="key" value="an_id_for_this_auth_provider_only" />
</beans:bean>
<!-- 注销客户端 -->
<beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />
<!-- 注销服务器端 -->
<beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<beans:constructor-arg value="${cas.server}/logout" />
<beans:constructor-arg>
<beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
</beans:constructor-arg>
<beans:property name="filterProcessesUrl" value="/j_spring_cas_security_logout" />
</beans:bean>
<!-- 认证过滤器 -->
<beans:bean id="securityFilter"
class="com.whty.bwjf.tsp.core.security.TspSecurityFilter">
<!-- 用户拥有的权限 -->
<beans:property name="authenticationManager" ref="authenticationManager" />
<!-- 用户是否拥有所请求资源的权限 -->
<beans:property name="accessDecisionManager" ref="tspAccessDecisionManager" />
<!-- 资源与权限对应关系 -->
<beans:property name="securityMetadataSource" ref="tspSecurityMetadataSource" />
</beans:bean>
<!-- 判断是否有访问权限 -->
<beans:bean id="tspAccessDecisionManager"
class="com.whty.bwjf.tsp.core.security.TspAccessDecisionManager"></beans:bean>
<!-- 从数据库提取权限和资源,装配到HashMap中,供Spring Security使用,用于权限校验 -->
<beans:bean id="tspSecurityMetadataSource"
class="com.whty.bwjf.tsp.core.security.TspSecurityMetadataSource">
<!-- 菜单表DAO -->
<beans:constructor-arg name="resourcesDao" ref="resourcesDao"></beans:constructor-arg>
</beans:bean>
<!-- 基础数据库操作类 -->
<beans:bean id="resourcesDao"
class="com.whty.bwjf.framework.core.dao.imp.BaseDao">
<beans:property name="sessionFactory" ref="sessionFactory"></beans:property>
</beans:bean>
<!-- 为Spring Security提供一个经过用户认证后的UserDetails -->
<beans:bean id="tspUserDetailServiceImpl"
class="com.whty.bwjf.tsp.core.security.TspUserDetailServiceImpl">
<!-- 用户表DAO -->
<beans:property name="usersDao" ref="resourcesDao"></beans:property>
<!-- 菜单表DAO -->
<beans:property name="resDao" ref="resourcesDao"></beans:property>
</beans:bean>
</beans:beans>
2、cas属性文件
cas.server=http://192.168.5.129:8080/cas
cas.client=http://192.168.4.184:8091/ucs
3、userDetailsService
CasAuthenticationProvider中需要自行配置一个继承UserDetailsService接口的实现类,来保存用户信息和权限信息。
/**
*该类的主要作用是为Spring Security提供一个经过用户认证后的UserDetails。
*该UserDetails包括用户名、密码、是否可用、是否过期等信息。
*/
@SuppressWarnings("deprecation")
public class TspUserDetailServiceImpl implements UserDetailsService {
@Resource
private IBaseDao<SysUser> usersDao;
@Resource
private IBaseDao<SysResource> resDao;
public void setUsersDao(IBaseDao<SysUser> usersDao) {
this.usersDao = usersDao;
}
public void setResDao(IBaseDao<SysResource> resDao) {
this.resDao = resDao;
}
/**
* 登入默认会调到这里
*/
@Transactional
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//获取用户信息
String hql = "from SysUser t where t.account = :username";
Map<String, Object> params = new HashMap<String, Object>();
params.put("username", username);
List<SysUser> userList = null;
try {
userList = this.usersDao.find(hql,params);
} catch (Exception e) {
e.printStackTrace();
}
SysUser users = null;
if(userList == null || userList.size() < 1){
users = null;
}else{
users = userList.get(0);
}
//取得用户的权限
Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(users);
boolean enables = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
//封装成spring security的user
User userdetail = new User(users.getAccount(), users.getPassword(), enables, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuths);
return userdetail;
}
//取得用户的权限
private Set<GrantedAuthority> obtionGrantedAuthorities(SysUser user) {
Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
//获取用户所属组
Set<SysRole> sysRoles = user.getSysRoles();
for(SysRole sysRole : sysRoles ){
//获取用户组对应 角色集合
Set<SysResource> sysResources = sysRole.getSysResources();
for(SysResource sysResource : sysResources){
authSet.add(
new GrantedAuthorityImpl(sysResource.getText()));
}
}
return authSet;
}
}
4、登出配置
web.xml新增单点登出过滤器。
web.xml中需要加入cas的SingleSignOutFilter实现单点登出功能,该过滤器需要放在shiroFilter之前,spring字符集过滤器之后。在实际使用时发现,SingleSignOutFilter如果放在了spring字符集过滤器之前,数据在传输过程中就会出现乱码。
<!-- 用于单点退出,该过滤器用于实现单点登出功能-->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 该过滤器用于实现单点登出功能。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
(二)自定义权限与cas集成配置
如果使用spring security自带的验证框架,只需要前面三步,并且去掉自定义过滤器和相关bean就可以实现cas单点登录了。
1、登录成功后将用户信息放入Session
其实spring security已经将UserDetails对象保存在了Session中,但是公司的项目并没有使用Session中的UserDetails,而是自己使用了自己保存的session对象,因此需要将用户信息在登陆时保存到Session中。
/**
* 登录成功处理Handler,保存用户信息到session中
*/
public class CasAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler{
final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private IBaseDao<SysUser> usersDao;
@Resource
private IBaseDao<SysResource> resDao;
public void setUsersDao(IBaseDao<SysUser> usersDao) {
this.usersDao = usersDao;
}
public void setResDao(IBaseDao<SysResource> resDao) {
this.resDao = resDao;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException,
ServletException {
logger.info("cas验证成功");
String username = ((UserDetails) authentication.getPrincipal()).getUsername();
SysUser user = getUser(username);
request.getSession().setAttribute(Constans.SESSION_USER, user);
if (user != null) {
request.getSession().setAttribute(Constans.SESSION_AUTH, loadAut(user.getId()));
}
super.onAuthenticationSuccess(request, response, authentication);
}
//获取用户信息
private SysUser getUser(String username){
String hql = "from SysUser t where t.account = :username";
Map<String, Object> params = new HashMap<String, Object>();
params.put("username", username);
List<SysUser> userList = null;
try {
userList = this.usersDao.find(hql,params);
} catch (Exception e) {
logger.error("读取菜单失败");
logger.error(e.getMessage());
}
if(userList == null || userList.size() < 1){
return null;
}else{
return userList.get(0);
}
}
//获取权限信息
private Set<String> loadAut(String uid) {
Set<String> set = new HashSet<String>();
List<SysResource> resources = new ArrayList<SysResource>();
Map<String, Object> params = new HashMap<String, Object>();
String hql = "select res from SysResource res join res.sysRoles r join r.sysUsers u where 1 = 1 and u.id = :userId order by res.seq asc ";
params.put("userId", uid);
try {
resources = resDao.find(hql, params);
} catch (Exception e) {
logger.error("读取菜单失败");
logger.error(e.getMessage());
}
if (resources != null && resources.size() > 0) {
for(SysResource resource : resources){
set.add(resource.getUrl());
}
}
return set;
}
}
2、accessDecisionManager和securityMetadataSource
自定义具体如何实现可以参考spring security相关文章,本系统在实现cas集成时没有修改原系统的这两个实现类,现在仅放出来提供参考。
/**
*AccessdecisionManager在Spring security中是很重要的。
*
*在验证部分简略提过了,所有的Authentication实现需要保存在一个GrantedAuthority对象数组中。
*这就是赋予给主体的权限。 GrantedAuthority对象通过AuthenticationManager
*保存到 Authentication对象里,然后从AccessDecisionManager读出来,进行授权判断。
*
*Spring Security提供了一些拦截器,来控制对安全对象的访问权限,例如方法调用或web请求。
*一个是否允许执行调用的预调用决定,是由AccessDecisionManager实现的。
*这个 AccessDecisionManager 被AbstractSecurityInterceptor调用,
*它用来作最终访问控制的决定。 这个AccessDecisionManager接口包含三个方法:
*
void decide(Authentication authentication, Object secureObject,
List<ConfigAttributeDefinition> config) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
从第一个方法可以看出来,AccessDecisionManager使用方法参数传递所有信息,这好像在认证评估时进行决定。
特别是,在真实的安全方法期望调用的时候,传递安全Object启用那些参数。
比如,让我们假设安全对象是一个MethodInvocation。
很容易为任何Customer参数查询MethodInvocation,
然后在AccessDecisionManager里实现一些有序的安全逻辑,来确认主体是否允许在那个客户上操作。
如果访问被拒绝,实现将抛出一个AccessDeniedException异常。
这个 supports(ConfigAttribute) 方法在启动的时候被
AbstractSecurityInterceptor调用,来决定AccessDecisionManager
是否可以执行传递ConfigAttribute。
supports(Class)方法被安全拦截器实现调用,
包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。
*/
public class TspAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if(configAttributes == null) {
return;
}
//所请求的资源拥有的权限(一个资源对多个权限)
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while(iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
//访问所请求资源所需要的权限
String needPermission = configAttribute.getAttribute();
System.out.println("needPermission is " + needPermission);
//用户所拥有的权限authentication
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needPermission.equals(ga.getAuthority())) {
return;
}
}
}
//没有权限让我们去捕捉
throw new AccessDeniedException(" 没有权限访问!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean supports(Class<?> clazz) {
// TODO Auto-generated method stub
return true;
}
}
/**
* 加载资源与权限的对应关系 该过滤器的主要作用就是通过spring著名的IoC生成securityMetadataSource。
* securityMetadataSource相当于本包中自定义的MyInvocationSecurityMetadataSourceService。
* 该MyInvocationSecurityMetadataSourceService的作用是从数据库提取权限和资源,装配到HashMap中,
* 供Spring Security使用,用于权限校验。
*
* @author sparta 11/3/29
*/
public class TspSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private IBaseDao<SysResource> resourcesDao;
// 由spring调用
public TspSecurityMetadataSource(IBaseDao<SysResource> resourcesDao) throws Exception {
this.resourcesDao = resourcesDao;
loadResourceDefine();
}
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
private RequestMatcher pathMatcher;
// 返回所请求资源所需要的权限
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
Iterator<String> it = resourceMap.keySet().iterator();
while (it.hasNext()) {
String resURL = it.next();
Iterator<String> ite = resourceMap.keySet().iterator();
if (resURL != null && !"".equals(resURL)) {
pathMatcher = new AntPathRequestMatcher(resURL);
} else {
pathMatcher = new AntPathRequestMatcher("/other.do");
}
if (pathMatcher.matches(((FilterInvocation) object).getRequest())) {
Collection<ConfigAttribute> returnCollection = resourceMap.get(resURL);
return returnCollection;
}
}
return null;
}
// 加载所有资源与权限的关系
@Transactional
private void loadResourceDefine() throws Exception {
if (resourceMap == null) {
resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
// Session session = this.resourcesDao.get
BaseDao<SysResource> dao = (BaseDao) resourcesDao;
Session session = dao.getSessionFactory().openSession();
Query query = session.createQuery("from SysResource");
List<SysResource> resources = query.list();
// List<SysResource> resources =
// this.resourcesDao.findAll(SysResource.class);
for (SysResource resource : resources) {
Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
// 以权限名封装为Spring的security Object
// resource.getRoleName() 角色名称 可随意 role_admin 或者 admin
ConfigAttribute configAttribute = new SecurityConfig(resource.getText());
configAttributes.add(configAttribute);
// resource.getInterceptUrl() 格式必须是 拦截的包路径
// 或者是 比如 /manager/**/*.jh 或者 /system/manager/**/*.jsp
resourceMap.put(resource.getUrl(), configAttributes);
}
}
Set<Entry<String, Collection<ConfigAttribute>>> resourceSet = resourceMap.entrySet();
Iterator<Entry<String, Collection<ConfigAttribute>>> iterator = resourceSet.iterator();
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean supports(Class<?> clazz) {
// TODO Auto-generated method stub
return true;
}
}
3、session失效处理
公司项目使用了自定义权限处理,在session失效后casEntryPoint的 access-denied-page没有生效,因此自定义了一个session过滤器来处理session失效的问题。
这个过滤器应该放在单点登录过滤器之前,CharacterEncodingFilter之后。
<!-- session过滤器,检查是否有session -->
<filter>
<filter-name>onlineFilter</filter-name>
<filter-class>com.whty.bwjf.framework.core.util.OnlineFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>onlineFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>onlineFilter</filter-name>
<url-pattern>*.htm</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>onlineFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
/**
* 登陆验证过滤
*/
public class OnlineFilter extends HttpServlet implements Filter {
private static final long serialVersionUID = 1L;
private String redirect_url;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 这里设置如果没有登陆将要转发到的页面
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
HttpSession session = req.getSession(true);
String requestUri = req.getRequestURI();
String contextPath = req.getContextPath();
String url = requestUri.substring(contextPath.length());
// 从session里取的用户名信息
SysUser user = (SysUser) session.getAttribute(Constans.SESSION_USER);
// 这里获取session,为了检查session里有没有保存用户信息,没有的话回转发到登陆页面
// 判断如果没有取到用户信息,就跳转到登陆页面
if(user == null && StringUtils.isEmpty(req.getParameter("ticket"))) {
// 跳转到登陆页面
if(isAjaxRequest(req)){
try {
response.getWriter().print("login");
} catch (IOException e) {
e.printStackTrace();
}
}
else{
res.sendRedirect(redirect_url);
}
} else {
chain.doFilter(request, response);
}
}
private boolean isAjaxRequest(HttpServletRequest request) {
String header = request.getHeader("X-Requested-With");
if (header != null && "XMLHttpRequest".equals(header))
return true;
else
return false;
}
public void init(FilterConfig filterConfig) throws ServletException {
StringBuilder url = new StringBuilder();
url.append(SysConfig.getConfiguration().getProperty(Constans.SYSCONFIG_CAS_SERVER));
url.append("/login?service=");
url.append(SysConfig.getConfiguration().getProperty(Constans.SYSCONFIG_CAS_CLIENT));
url.append("/j_spring_cas_security_check");
redirect_url = url.toString();
}
}