如果不熟悉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的编码有以下方式
- 在系统的环境变量中添加一个变量,名为: JAVA_TOOL_OPTIONS, 值为:-Dfile.encoding=UTF-8
- 在运行java程序的时候指定参数java -Dfile.encoding=UTF-8