一、文件jeeplus.properties,添加配置
内容如下:
cas.server.serverurlprefix.url=http://10.10.128.90:7001/cas
cas.project.service=http://10.10.128.90:7002/portal
shiro.service=${cas.project.service}/cas/login
shiro.failureUrl=/webpage/error/error_cas.jsp
cas.server.login.url=${cas.server.serverurlprefix.url}?service=${shiro.service}
cas.server.logout.url=${cas.server.serverurlprefix.url}/logout?service=${cas.project.service}
注意:只有门户在退出时去cas服务注销token,各个子系统在注销时只注销自己的session就可以了
在各个子系统中配置:
cas.server.serverurlprefix.url=http://10.10.128.90:7001/cas
cas.project.service=http://10.10.128.90:7003/zxt
cas.project.service2=http://10.10.128.90:7003/portal
shiro.service=${cas.project.service}/cas/login
shiro.failureUrl=/webpage/error/error_cas.jsp
cas.server.login.url=${cas.server.serverurlprefix.url}?service=${shiro.service}
cas.server.logout.url=${cas.project.service2}/logout
二、修改文件 spring-context-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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:/config/jeeplus.properties" />
<!-- Shiro权限过滤过滤器定义 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/sys/user/infoCareStatus = anon
${adminPath}/sys/user/validateLoginName = anon
${adminPath}/sys/user/validateMobile = anon
${adminPath}/sys/user/validateMobileExist = anon
${adminPath}/sys/user/resetPassword = anon
${adminPath}/sys/register = anon
${adminPath}/sys/register/registerUser = anon
${adminPath}/sys/register/getRegisterCode = anon
${adminPath}/sys/register/validateMobileCode = anon
${adminPath}/soft/sysVersion/getAndroidVer = anon
${adminPath}/soft/sysVersion/getIosVer = anon
/cas/login = casFilter
${adminPath}/login = user
${adminPath}/logout = logoutFilter
${adminPath}/** = user
/act/rest/service/editor/** = perms[act:model:edit]
/act/rest/service/model/** = perms[act:model:edit]
/act/rest/service/** = user
/ReportServer/** = user
</value>
</constructor-arg>
</bean>
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="${cas.server.login.url}" />
<property name="filters">
<map>
<entry key="casFilter" value-ref="casFilter" />
<entry key="logoutFilter" value-ref="logoutFilter" />
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<!-- CAS认证过滤器 -->
<bean id="casFilter" class="com.jeeplus.common.security.shiro.UserCasFilter">
<property name="failureUrl" value="${shiro.failureUrl}"/>
</bean>
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="${cas.server.logout.url}" />
</bean>
<bean id="casRealm" class="com.jeeplus.common.security.shiro.UserRealm">
<!-- 认证通过后的默认角色 -->
<property name="defaultRoles" value="ROLE_USER" />
<!-- cas服务端地址前缀 -->
<property name="casServerUrlPrefix" value="${cas.server.serverurlprefix.url}" />
<!-- 应用服务地址,用来接收cas服务端票据 -->
<property name="casService" value="${shiro.service}" />
</bean>
<!-- 定义Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="casRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 自定义会话管理配置 -->
<bean id="sessionManager" class="com.jeeplus.common.security.shiro.session.SessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 会话超时时间,单位:毫秒 -->
<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
<!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 -->
<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!-- <property name="sessionValidationSchedulerEnabled" value="false"/> -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionIdCookieEnabled" value="true"/>
</bean>
<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID,
当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="jeeplus.session.id"/>
</bean>
<!-- 自定义Session存储容器 -->
<bean id="sessionDAO" class="com.jeeplus.common.security.shiro.session.CacheSessionDAO">
<property name="sessionIdGenerator" ref="idGen" />
<property name="activeSessionsCacheName" value="activeSessionsCache" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 定义授权缓存管理器 -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="cacheManager" />
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- AOP式方法级权限检查 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
三、新建文件UserRealm.java
package com.jeeplus.common.security.shiro;
import com.jeeplus.common.config.Global;
import com.jeeplus.common.utils.Encodes;
import com.jeeplus.common.utils.SpringContextHolder;
import com.jeeplus.common.utils.StringUtils;
import com.jeeplus.common.web.Servlets;
import com.jeeplus.modules.sys.entity.Menu;
import com.jeeplus.modules.sys.entity.Role;
import com.jeeplus.modules.sys.entity.User;
import com.jeeplus.modules.sys.security.SystemAuthorizingRealm;
import com.jeeplus.modules.sys.service.SystemService;
import com.jeeplus.modules.sys.utils.LogUtils;
import com.jeeplus.modules.sys.utils.UserUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
/**
* 系统安全认证实现类
* @author jeeplus
* @version 2017-7-5
*/
@Service
//@DependsOn({"userMapper","roleMapper","menuMapper"})
public class UserRealm extends CasRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
private SystemService systemService;
@Autowired
HttpServletRequest request;
/**
* 认证回调函数, 登录时调用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
CasToken casToken = (CasToken)token;
if (token == null) {
return null;
} else {
String ticket = (String) casToken.getCredentials();
if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
return null;
} else {
//ticket检验器
TicketValidator ticketValidator = ensureTicketValidator();
int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
try {
String casservice=getCasService();
// 去CAS服务端中验证ticket的合法性
Assertion casAssertion = ticketValidator.validate(ticket, casservice);
// 从CAS服务端中获取相关属性,包括用户名、是否设置RememberMe等
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String userId = casPrincipal.getName();
if (logger.isDebugEnabled()){
logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, userId);
}
// 校验用户名密码
User user = getSystemService().getUserByLoginName(userId);
if (user != null) {
if (Global.NO.equals(user.getLoginFlag())){
throw new AuthenticationException("msg:该已帐号禁止登录.");
}
byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
SystemAuthorizingRealm.Principal principals=new SystemAuthorizingRealm.Principal(user,false);
String name=getName();
ByteSource bytesource= ByteSource.Util.bytes(userId);
return new SimpleAuthenticationInfo(principals, ticket,bytesource , name);
} else {
return null;
}
} catch (TicketValidationException e) {
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
}
}
/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Object _principal= principals.getPrimaryPrincipal();
String username = getFieldValueByName("loginName",_principal);
User user = getSystemService().getUserByLoginName(username);
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Menu> list = UserUtils.getMenuList();
for (Menu menu : list){
if (StringUtils.isNotBlank(menu.getPermission())){
// 添加基于Permission的权限信息
for (String permission : StringUtils.split(menu.getPermission(),",")){
info.addStringPermission(permission);
}
}
}
// 添加用户权限
info.addStringPermission("user");
// 添加用户角色信息
for (Role role : user.getRoleList()){
info.addRole(role.getEnname());
}
// 更新登录IP和时间
getSystemService().updateUserLoginInfo(user);
// 记录登录日志
LogUtils.saveLog(Servlets.getRequest(), "系统登录");
return info;
} else {
return null;
}
}
private String getFieldValueByName(String fieldName, Object o) {
try {
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String getter = "get" + firstLetter + fieldName.substring(1);
Method method = o.getClass().getMethod(getter, new Class[] {});
Object value = method.invoke(o, new Object[] {});
return (String)value;
} catch (Exception e) {
return null;
}
}
@Override
protected void checkPermission(Permission permission, AuthorizationInfo info) {
authorizationValidate(permission);
super.checkPermission(permission, info);
}
@Override
protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermitted(permissions, info);
}
@Override
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
authorizationValidate(permission);
return super.isPermitted(principals, permission);
}
@Override
protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermittedAll(permissions, info);
}
/**
* 授权验证方法
* @param permission
*/
private void authorizationValidate(Permission permission){
// 模块授权预留接口
}
/**
* 设定密码校验的Hash算法与迭代次数
*/
//@PostConstruct
public void initCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(SystemService.HASH_ALGORITHM);
matcher.setHashIterations(SystemService.HASH_INTERATIONS);
setCredentialsMatcher(matcher);
}
// /**
// * 清空用户关联权限认证,待下次使用时重新加载
// */
public void clearCachedAuthorizationInfo(Principal principal) {
SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
clearCachedAuthorizationInfo(principals);
}
/**
* 清空所有关联认证
* @Deprecated 不需要清空,授权缓存保存到session中
*/
@Deprecated
public void clearAllCachedAuthorizationInfo() {
// Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
// if (cache != null) {
// for (Object key : cache.keys()) {
// cache.remove(key);
// }
// }
}
/**
* 获取系统业务对象
*/
public SystemService getSystemService() {
if (systemService == null){
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
/**
* 授权用户信息
*/
public static class Principal implements Serializable {
private static final long serialVersionUID = 1L;
private String id; // 编号
private String loginName; // 登录名
private String name; // 姓名
private boolean mobileLogin; // 是否手机登录
// private Map<String, Object> cacheMap;
public Principal(User user, boolean mobileLogin) {
this.id = user.getId();
this.loginName = user.getLoginName();
this.name = user.getName();
this.mobileLogin = mobileLogin;
}
public String getId() {
return id;
}
public String getLoginName() {
return loginName;
}
public String getName() {
return name;
}
public boolean isMobileLogin() {
return mobileLogin;
}
/**
* 获取SESSIONID
*/
public String getSessionid() {
try{
return (String) UserUtils.getSession().getId();
}catch (Exception e) {
return "";
}
}
@Override
public String toString() {
return id;
}
}
}
四、单点登录认证异常
创建单点登录认证类UserCasFilter
package com.jeeplus.common.security.shiro;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
public class UserCasFilter extends CasFilter {
private String failureUrl;
private static Logger logger = LoggerFactory.getLogger(CasFilter.class);
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request, ServletResponse response) {
try {
request.setAttribute("exception", ae);
request.getRequestDispatcher(this.failureUrl).forward(request, response);
} catch (Exception e) {
e.printStackTrace();
logger.error("权限认证失败", e);
}
return false;
}
public void setFailureUrl(String failureUrl) {
this.failureUrl = failureUrl;
}
}
创建error/error_cas.jsp文件
<%
response.setStatus(500);
// 获取异常类
Throwable ex = Exceptions.getThrowable(request);
if (ex != null) {
LoggerFactory.getLogger("500.jsp").error(ex.getMessage(), ex);
}
// 编译错误信息
StringBuilder sb = new StringBuilder("错误信息:\n");
if (ex != null) {
sb.append(Exceptions.getStackTraceAsString(ex));
} else {
sb.append("未知错误.\n\n");
}
// 如果是异步请求或是手机端,则直接返回信息
if (Servlets.isAjaxRequest(request)) {
out.print(sb);
}
// 输出异常信息页面
else {
%>
<%@page import="org.slf4j.Logger,org.slf4j.LoggerFactory" %>
<%@page import="com.jeeplus.common.web.Servlets" %>
<%@page import="com.jeeplus.common.utils.Exceptions" %>
<%@page import="com.jeeplus.common.utils.StringUtils" %>
<%@page contentType="text/html;charset=UTF-8" isErrorPage="true" %>
<%@include file="/webpage/include/taglib.jsp" %>
<!DOCTYPE html>
<html>
<head>
<title>权限认证失败</title>
<link rel="stylesheet" href="${ctxStatic}/common/css/mainStyle.css"/>
<%@include file="/webpage/include/head.jsp" %>
<style type="text/css">
.errorBox {
position: absolute;
top: 50%;
left: 50%;
width: 600px;
height: 300px;
margin-left: -300px;
margin-top: -200px;
}
.errorBox .errorImg {
width: 100%;
text-align: center;
}
.errorBox .errorTxt1 {
width: 100%;
padding-top: 20px;
text-align: center;
color: #333;
font-size: 18px;
line-height: 1;
}
.errorBox .errorTxt2 {
width: 100%;
padding-top: 10px;
text-align: center;
color: #666;
font-size: 14px;
line-height: 1
}
.errorBox .authorizeErrorTxt {
width: 100%;
margin-top: -35px;
text-align: center;
color: #666;
font-size: 18px;
line-height: 1;
}
.errorBox .authorizeErrorBtn {
width: 100%;
margin-top: 15px;
text-align: center;
}
.errorBox .authorizeErrorBtn a {
display: block;
padding: 5px 15px;
border-radius: 15px;
border: 2px solid #00bd9c;
color: #333;
width: 116px;
font-size: 14px;
margin: 30px auto 0;
}
.errorBox .unknownErrorTxt {
width: 100%;
padding-top: 20px;
text-align: center;
color: #333;
font-size: 18px;
line-height: 1;
}
.errorBox .unknownErrorBtn {
width: 100%;
text-align: center;
}
.errorBox .unknownErrorBtn a {
display: inline-block;
padding: 5px 15px;
border-radius: 15px;
border: 2px solid #00bd9c;
color: #333;
width: 116px;
font-size: 14px;
margin: 30px auto 0;
}
</style>
<script type="text/javascript">
function showErrorMsg() {
var temp = $(".errorMessage").clone();
temp.removeClass('hide');
top.layer.open({
type: 1,
skin: 'layerui-layer-rim',
title: '错误详细信息',
area: ['1000px', '600px'],
content: temp.html(),
btn: ['关闭']
});
}
</script>
</head>
<body>
<div class="errorBox" style="height: 500px;margin-top:-300px;">
<div class="errorImg">
<img src="${ctxWebpage}/static/common/img/unknownError.png">
</div>
<div class="container-fluid">
<div class="unknownErrorTxt">权限认证失败</div>
<div class="unknownErrorBtn">
错误信息:<%=ex == null ? "权限认证失败." : StringUtils.toHtml(ex.getMessage())%> <br/> <br/>
请点击“查看详细信息”按钮,将详细错误信息发送给系统管理员,谢谢!<br/> <br/>
<a href="javascript:" onclick="window.close();" class="btn">关闭当前页</a>
<a href="javascript:" onclick="showErrorMsg()" class="btn">查看详细信息</a>
</div>
<div class="errorMessage hide">
<%=StringUtils.toHtml(sb.toString())%> <br/>
</div>
<script>try {
top.$.jBox.closeTip();
} catch (e) {
}</script>
</div>
</div>
</body>
</html>
<%
}
out = pageContext.pushBody();
%>
五、未能够识别出目标 ***;票根
修改cas服务端相关配置文件: cas.properties
st.timeToKillInSeconds=10
原因,默认值时间是1.8秒,当登录成功,携带ST返回客户端,客户端带着server和ST去服务器验证,但此时服务器端的ST已经失效。