第三部分:shiro集成spring使用cas单点登录配置

第三部分 shiro集成spring使用cas单点登录配置

(一)shiro单点登录

  配置的主要目的在于将登录页面改为${cas.server}?service=${cas.client}/login的形式,service后面为本地的回调地址。在cas服务器端登录成功后,会生成ticket返回给客户端,客户端的shiro使用ticket最为凭据保存起来。
  
  shiro配置单点登陆后,在注销时原始的cas-client只能删除HttpSession,不能删除shiro的Session,因此未使用shiro的session管理器。
  如果想启用shiro的Session管理器,可以参考Shiro & CAS 实现单点登录
  原有的CasRealm在AuthenticationInfo中只保存了用户名作为principal,MyCasRealm中重写了此方法,改为保存用户信息类。
  需要在pom增加依赖,shiro-cas会通过依赖传递自动增加cas-client-core-3.2.1的依赖。

<!-- shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-cas</artifactId>
    <version>1.2.3</version>
</dependency>
1、spring-shiro.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    " default-lazy-init="false">
    <description>Shiro安全配置</description>

    <!-- 缓存管理器 -->
    <bean id="cacheManager" class="com.whty.framework.base.common.cache.SpringCacheManagerWrapper">
        <property name="cacheManager" ref="springCacheManager"/>
    </bean>

    <!-- Realm实现 -->
    <bean id="casRealm" class="com.whty.oim.base.shiro.MyCasRealm">
        <property name="cachingEnabled" value="true"/>
        <property name="authenticationCachingEnabled" value="true"/>
        <property name="authenticationCacheName" value="authenticationCache"/>
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authorizationCacheName" value="authorizationCache"/>
        <!-- CAS Server -->
        <property name="casServerUrlPrefix" value="${cas.server}"/>
        <!-- 客户端的回调地址设置,必须和下面的shiro-cas过滤器拦截的地址一致 -->
        <property name="casService" value="${cas.client}/login"/>
    </bean>

    <bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"/>
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="casRealm"/>
        <!-- <property name="sessionManager" ref="sessionManager"/> -->
        <property name="cacheManager" ref="cacheManager"/>
        <!-- sessionMode参数设置为native时,那么shrio就将用户的基本认证信息保存到缺省名称为shiro-activeSessionCache 的Cache中 -->
        <!--<property name="sessionMode" value="native" />-->
        <property name="subjectFactory" ref="casSubjectFactory"/>
    </bean>

    <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
        <property name="arguments" ref="securityManager"/>
    </bean>

    <bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
        <!-- 配置验证错误时的失败页面  -->
        <property name="failureUrl" value="${cas.client}"/>
    </bean>

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <!-- 设定角色的登录链接,这里为cas登录页面的链接可配置回调地址  -->
        <property name="loginUrl" value="${cas.server}?service=${cas.client}/login" />
        <property name="successUrl" value="/index" />
        <property name="unauthorizedUrl" value="/"/> 
        <property name="filters">
            <util:map>
                <!-- <entry key="authc" value-ref="authcFilter"/>
                <entry key="captchaFilter" value-ref="captchaFilter"/> -->
                <!-- 添加casFilter到shiroFilter -->
                <entry key="cas" value-ref="casFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
            /logout = logout
            /login = cas
            /** = user
            </value>
        </property>
    </bean>


    <!--保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

</beans>
2、cas.properties

  cas.server=http://192.168.5.129:8080/cas
  cas.client=http://192.168.4.184:8091/ucs

3、自定义Realm实现

  主要修改了CasRealm的doGetAuthenticationInfo()方法,CasRealm默认只保存了username,本系统改为保存ShiroUser对象。

public class MyCasRealm extends CasRealm {

    Logger logger = LoggerFactory.getLogger(MyCasRealm.class);

    @Autowired
    private UcsUserService ucsUserService;

    @Autowired
    private UcsRoleService ucsRoleService;

    @Autowired
    private UcsPermissionService ucsPermissionService;

    @Value("${domain}")
    private String domain;

    /**
     * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用.
     * 
     * @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        UcsUser ucsUser = ucsUserService.selectByUsername(shiroUser.loginName);
        //把principals放session中 key=userId value=principals
        SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(ucsUser.getId()),SecurityUtils.getSubject().getPrincipals());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //赋予角色
        List<UcsRole> ucsRoles = ucsRoleService.selectByUser(ucsUser.getId());
        if (!CheckEmptyUtil.isEmpty(ucsRoles)){
            for(UcsRole ucsRole:ucsRoles){
                info.addRole(ucsRole.getId());
            }
        }
        //赋予权限
//        List<UcsPermission> ucsPermissions = ucsPermissionService.selectByUser(ucsUser.getId(), domain);
        if (!CheckEmptyUtil.isEmpty(shiroUser.getMenus())){
            for(EasyUIMenu permission:shiroUser.getMenus()){
                if(UcsBaseConstant.PermissionType.BUTTON.equals(permission.getType()))
                info.addStringPermission(permission.getUrl());
            }
        }
        return info;
    }

    /**
     * Authenticates a user and retrieves its information.
     * 
     * @param token the authentication token
     * @throws AuthenticationException if there is an error during authentication.
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        CasToken casToken = (CasToken) token;
        if (token == null) {
            return null;
        }

        String ticket = (String)casToken.getCredentials();
        if (!StringUtils.hasText(ticket)) {
            return null;
        }

        TicketValidator ticketValidator = ensureTicketValidator();

        try {
            // contact CAS server to validate service ticket
            Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
            // get principal, user id and attributes
            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
            String username = casPrincipal.getName();
            logger.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
                    ticket, getCasServerUrlPrefix(), username
            });

            Map<String, Object> attributes = casPrincipal.getAttributes();
            // refresh authentication token (user id + remember me)
            casToken.setUserId(username);
            String rememberMeAttributeName = getRememberMeAttributeName();
            String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
            boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
            if (isRemembered) {
                casToken.setRememberMe(true);
            }
            // create simple authentication info
            // 根据用户名获取账号信息
            UcsUser ucsUser = ucsUserService.selectByUsername(username);
            List<UcsPermission> ucsPermissions = null;
            if (ucsUser != null) {
                ucsPermissions = ucsPermissionService.selectByUser(ucsUser.getId(), domain);
            } else {
                throw new UnknownAccountException();//登录失败
            }
            //给菜单排序
            List<EasyUIMenu> menus = toEasyUIMenu(ucsPermissions);
            if(!CheckEmptyUtil.isEmpty(menus)){
                Collections.sort(menus);
            }
            ShiroUser shiroUser = new ShiroUser(ucsUser.getId(), ucsUser.getUsername(), ucsUser.getName(), menus);

            return new SimpleAuthenticationInfo(shiroUser, ticket, getName());
        } catch (TicketValidationException e) { 
            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
        }
    }

    private List<EasyUIMenu> toEasyUIMenu(List<UcsPermission> ucsPermissions){
        List<EasyUIMenu> menus = new ArrayList<EasyUIMenu>(0);
        for (UcsPermission ucsPermission : ucsPermissions) {
            menus.add(toEasyUIMenu(ucsPermission));
        }
        return menus;
    }
    private EasyUIMenu toEasyUIMenu(UcsPermission ucsPermission){
        return new EasyUIMenu(ucsPermission.getId(), ucsPermission.getPid(), ucsPermission.getDomain(), ucsPermission.getName(), ucsPermission.getType(), ucsPermission.getSort(), ucsPermission.getIcon(), ucsPermission.getUrl(), ucsPermission.getDescription(), ucsPermission.getStatus());
    }

    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }

    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }

    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
        clearAllCache();
    }

    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }

    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }

    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }

}

(二)cas登出

  
  在任意一个子系统登出后,cas服务器会向所有发过登录请求的子系统发送一个登出请求,使所有子系统的HttpSession失效。如果使用了shiro的session管理器,需要修改cas客户端代码,在移除HttpSession的时候也移除shiro的Session。

1、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>
2、登出请求

  登出只需要将当前页面跳转到cas服务器的logout页面即可实现登出。如果需要登出后跳转页面,可以加入service参数,并参考第一部分中第四节进行配置。
  子系统使用的方法是在后台拼装好登出链接,发送给前台页面,前台页面接受使用el表达式接收。具体实现需要根据每个子系统的不同来自行实现。

@Value("${cas.server}")
private String cas_server;  
@Value("${cas.client}")
private String cas_client;

@RequestMapping(value = "/index")
public String index(Model model){
    StringBuilder logoutUrl = new StringBuilder();
    logoutUrl.append(cas_server);
    logoutUrl.append("/logout?service=");
    logoutUrl.append(cas_client);
    model.addAttribute("logoutUrl", logoutUrl.toString());
    return "system/index";
}

  前台页面:

<script>
    var logoutUrl = '${logoutUrl}';
</script>
缓存配置(可选)

我使用的ehcache作为shiro的缓存,也可以使用其他的。
spring-cache.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"
    default-lazy-init="false">

    <bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="ehcacheManager"/>
    </bean>

    <!--ehcache-->
    <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:properties/ehcache.xml"/>
    </bean>

</beans>

ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>  
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:noNamespaceSchemaLocation="ehcache.xsd"  
    updateCheck="true" monitoring="autodetect"  
    dynamicConfig="true">  


    <diskStore path="C:\\ehcache\\cache"/> 
    <!--<diskStore path="/application/cache"/>   -->
    <!-- 登录记录缓存 锁定10分钟 -->
    <cache name="passwordRetryEhcache"
        maxEntriesLocalHeap="2000"
        eternal="false"
        timeToIdleSeconds="1800"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        statistics="true">
    </cache>

    <cache name="authorizationCache"
        maxEntriesLocalHeap="2000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        statistics="true">
    </cache>

    <cache name="authenticationCache"
        maxEntriesLocalHeap="2000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        statistics="true">
    </cache>

    <cache name="shiro-activeSessionCache"
        maxEntriesLocalHeap="2000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        statistics="true">
    </cache>

    <defaultCache  
        maxElementsInMemory="1000"  
        eternal="false"  
        timeToIdleSeconds="120"  
        timeToLiveSeconds="120"  
        overflowToDisk="true"  
        maxElementsOnDisk="10000"  
        diskSpoolBufferSizeMB="30"  
        diskPersistent="false"  
        diskExpiryThreadIntervalSeconds="120"  
        memoryStoreEvictionPolicy="LRU"  
        statistics="false"  
        />  


</ehcache> 
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值