Spring Shiro CAS 客户端集成配置

二次信任 提交于 2020-03-12 12:05:31

如果不熟悉Shiro 和CAS的概念,可以在网上搜索一下这方面的资料,

在配置CAS客户端配置之前,首先要进行CAS服务端配置  

配置之前需要引入一些jar包具体如下:

        <dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>1.2.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-web</artifactId>
			<version>1.2.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-aspectj</artifactId>
			<version>1.2.0</version>
		</dependency>
		
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.2.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-cas</artifactId>
			<version>1.2.0</version>
		</dependency>

(一)cas登录

web.xml配置

 <filter>
      <filter-name>shiroFilter</filter-name>
      <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
      <init-param>
          <param-name>targetFilterLifecycle</param-name>
          <param-value>true</param-value>
      </init-param>
  </filter>
  <filter-mapping>
      <filter-name>shiroFilter</filter-name>
      <url-pattern>/*</url-pattern>
      <dispatcher>REQUEST</dispatcher>
      <dispatcher>FORWARD</dispatcher>
      <dispatcher>INCLUDE</dispatcher>
  </filter-mapping>

shiro-cas.xml核心配置

<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:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="  
         http://www.springframework.org/schema/beans   
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
         http://www.springframework.org/schema/context   
         http://www.springframework.org/schema/context/spring-context-3.0.xsd  
         http://www.springframework.org/schema/util  
         http://www.springframework.org/schema/util/spring-util-3.0.xsd"
	default-lazy-init="true">

	<description>Shiro Security Config</description>

	<!-- Shiro Filter -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<property name="securityManager" ref="securityManager" />
		<!-- 设定角色的登录链接,这里为cas登录页面的链接可配置回调地址 -->
		<property name="loginUrl" value="http://localhost:8089/login?service=http://localhost:8080/sso" />
		<property name="filters">
			<util:map>
				<!-- 添加casFilter到shiroFilter -->
				<entry key="casFilter" value-ref="casFilter" />
			</util:map>
		</property>
		<property name="filterChainDefinitions">
			<value>
				/sso = casFilter
				/center/** = authc
			</value>
		</property>
	</bean>
	<bean id="casFilter" class="com.shiro.cas.MyCasFilter">
		<!-- 配置验证错误时的失败页面 -->
		<property name="failureUrl" value="http://localhost:8080/login" />
	</bean>

	<bean id="casRealm" class="com.shiro.MyCasRealm">
		<property name="dataSource" ref="dataSource"></property>
		<property name="defaultRoles" value="MEMBER" />
		<property name="casServerUrlPrefix" value="http://localhost:8089/login" />

		<!-- 客户端的回调地址设置,必须和下面的shiro-cas过滤器拦截的地址一致 -->
		<property name="casService" value="http://localhost:8080/sso" />
	</bean>


    <!-- 如果要实现cas的remember me的功能,需要用到下面这个bean,并设置securityManager的subjectFactory中 -->
	<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="subjectFactory" ref="casSubjectFactory" />
		
	</bean>
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

	<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
		<property name="staticMethod"
				  value="org.apache.shiro.SecurityUtils.setSecurityManager" />
		<property name="arguments" ref="securityManager" />
	</bean>
</beans>  

说明一下上面的配置,上面的配置中采用自定义casFilter : MyCasFilter ,  也可以采用默认的CasFilter,自定义的MyCasFilter 可以在登录成功的时候增加一些自己的业务,比如将用户名写入cookie中,保存登录IP,保存登录日志等等业务,默认CasFilter配置如下:

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

MyCasFilter 的核心代码:

public class MyCasFilter extends CasFilter {
    
    private static Logger logger = LoggerFactory.getLogger(KsdCasFilter.class);

    @Override
    protected boolean executeLogin(ServletRequest request,
    		ServletResponse response) throws Exception {
    	return super.executeLogin(request, response);
    }
    
   
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        return super.onAccessDenied(request, response);
    }
    
    
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
                                     ServletResponse response) throws Exception {
    	
        Member member = (Member) subject.getPrincipal();
        SecurityUtils.getSubject().getSession().setAttribute("member", member);
        
        String domain = getRootDomain(request);
        String nickname = URLEncoder.encode(member.getNickname(), "utf-8");
        CookieUtils.setCookie((HttpServletResponse)response, "nickname",nickname, -1, domain, "/");
        
         
        WebUtils.redirectToSavedRequest(request, response,getSuccessUrl());
        return false;
    }
    
    public static String getRootDomain(ServletRequest request) {
    	String domain = request.getServerName();
		if(domain.equals("127.0.0.1") || domain.equalsIgnoreCase("localhost")) {
			domain = "gongxi.net";
		} else {
			String [] hostArr = domain.split("\\.");
			int length = hostArr.length;
			domain = hostArr.length >= 2 ? (hostArr[length - 2] + "." + hostArr[length - 1]) : domain;
		}
		return domain;
    }
    
    
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
                                     ServletResponse response) {
    	logger.info("redirecting user to the CAS error page");
        return super.onLoginFailure(token, ae, request, response);
    }
    
}

实现自定义的Cas Realm :MyCasRealm 

public class MyCasRealm extends CasRealm {
	

	protected DataSource dataSource;
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	private static final Logger log = LoggerFactory.getLogger(MyCasRealm.class);


	protected String userRolesQuery = "SELECT r.role_name FROM sys_user u,ka_sys_user_role ur,ka_sys_role r WHERE u.user_id=ur.user_id AND ur.role_id=r.role_id AND u.login_name=?";

	protected String permissionsQuery = "SELECT * FROM sys_menu m,sys_role_menu rm,sys_role r WHERE m.menu_id=rm.menu_id AND rm.role_id=r.role_id AND r.role_name=?";

	/* 
	 * 身份认证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		// super.doGetAuthenticationInfo(token);
		CasToken casToken = (CasToken) token;
		if (token == null) {
			return null;
		}
		String ticket = (String) casToken.getCredentials();
		if (!StringUtils.hasText(ticket)) {
			return null;
		}
		
		TicketValidator ticketValidator = ensureTicketValidator();
		Assertion casAssertion = null;
		try {
			casAssertion = ticketValidator.validate(ticket, getCasService());
		} catch (TicketValidationException e1) {
			throw new AuthenticationException(e1);
		}
		
		AttributePrincipal casPrincipal = casAssertion.getPrincipal();
		String username = casPrincipal.getName();
		// Null username is invalid
		if (!StringUtils.hasText(username)) {
			throw new InvalidAccountException("Null usernames are not allowed by this realm.");
		}
		AuthenticationInfo info = null;
		try {
			
			String userId = casPrincipal.getName();
			log.debug(
					"Validate ticket : {} in CAS server : {} to retrieve user : {}",
					new Object[] { ticket, getCasServerUrlPrefix(), userId });

			Map<String, Object> attributes = casPrincipal.getAttributes();
			// refresh authentication token (user id + remember me)
			casToken.setUserId(userId);
			String rememberMeAttributeName = getRememberMeAttributeName();
			String rememberMeStringValue = (String) attributes.get(rememberMeAttributeName);
			boolean isRemembered = rememberMeStringValue != null
					&& Boolean.parseBoolean(rememberMeStringValue);
			if (isRemembered) {
				casToken.setRememberMe(true);
			}

			info = new SimpleAuthenticationInfo(username, ticket,username);
		} catch (Exception e) {
			final String message = "There was an error while authenticating user [" + username + "]";
			if (log.isErrorEnabled()) {
				log.error(message, e);
			}
			// Rethrow any SQL errors as an authentication exception
			throw new AuthenticationException(message, e);
		}
		return info;
	}

	/*
	 * 权限查询
	 */
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principals) {
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		// add default roles
		//addRoles(simpleAuthorizationInfo, split(getDefaultRoles()));
		addRoles(simpleAuthorizationInfo,split(getDefaultRoles()));
		// add default permissions
		addPermissions(simpleAuthorizationInfo, split(getDefaultPermissions()));
		
		return simpleAuthorizationInfo;
	}
	

	private void addPermissions(
			SimpleAuthorizationInfo simpleAuthorizationInfo,
			List<String> permissions) {
		for (String permission : permissions) {
			simpleAuthorizationInfo.addStringPermission(permission);
		}
	}

	private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo,
			List<String> roles) {
		for (String role : roles) {
			simpleAuthorizationInfo.addRole(role);
		}
	}

	private List<String> split(String s) {
		List<String> list = new ArrayList<String>();
		String[] elements = StringUtils.split(s, ',');
		if (elements != null && elements.length > 0) {
			for (String element : elements) {
				if (StringUtils.hasText(element)) {
					list.add(element.trim());
				}
			}
		}
		return list;
	}
	
}

至此基本的配置就完成了。

(二)cas登出

       用户发出登出请求后,cas客户端(或者后台action)会给cas服务器会发出登出请求,使ticke过期;在登出的时候同时需要使HttpSession失效

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 登出请求处理

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        SecurityUtils.getSubject().logout();
        
        String logoutUrl = "http://localhost:8089/logout";
        String service = request.getParameter("service");
        if(StringUtils.isNotBlank(service)) {
            logoutUrl += "?service=".concat(service);
        }
        
        return "redirect:" + logoutUrl;
    }

 

 

其他问题:

1 中文昵称登录时频繁重定向,需要设置jvm的编码

    设置JVM的编码有以下方式

  1. 在系统的环境变量中添加一个变量,名为: JAVA_TOOL_OPTIONS, 值为:-Dfile.encoding=UTF-8
  2. 在运行java程序的时候指定参数java -Dfile.encoding=UTF-8
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!