第四部分: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();
    }
}