前景提要

springboot+cas+shiro+pac4j实现单点登录,但是登出有问题,服务端退出了,客户端还是登录状态!
0 [待解决问题] 之前没在shiroConfig里加public FilterRegistrationBean singleSignOutFilter() ;可以实现单点登录,登出有问题,家里这个方法后,启动报错:
java.lang.IllegalArgumentException: casServerUrlPrefix cannot be null.
at org.jasig.cas.client.util.CommonUtils.assertNotNull(CommonUtils.java:87)
at org.jasig.cas.client.session.SingleSignOutHandler.init(SingleSignOutHandler.java:130)
at org.jasig.cas.client.session.SingleSignOutFilter.init(SingleSignOutFilter.java:54)
…………………………………
package com.audaque.gm.config;
import com.audaque.gm.modules.sys.shiro.ShiroPermsFilterFactoryBean;
import com.audaque.gm.modules.sys.shiro.UserFilter;
import com.audaque.gm.modules.sys.shiro.UserPermFilter;
import com.audaque.gm.modules.sys.sso.CallbackFilter;
import com.audaque.gm.modules.sys.sso.CasRealm;
import com.audaque.gm.support.shiro.listener.UserSessionListener;
import com.audaque.gm.support.shiro.session.UserSessionDAO;
import com.audaque.gm.support.shiro.session.UserSessionFactory;
import io.buji.pac4j.filter.LogoutFilter;
import io.buji.pac4j.filter.SecurityFilter;
import io.buji.pac4j.subject.Pac4jSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.pac4j.core.config.Config;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.*;
import java.util.Map.Entry;
/** shiro配置 */
@DependsOn(“springContextUtils”)
@Configuration
public class ShiroConfig {
/** 项目工程路径 */
@Value("${cas.project.url}")
private String projectUrl;
/** 项目cas服务路径 */
@Value("${cas.server.url}")
private String casServerUrl;
/** 客户端名称 */
@Value("${cas.client-name}")
private String clientName; /** 安全管理器 @param sessionManager @return
*/
@Bean
public SecurityManager securityManager(SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager);
securityManager.setSubjectFactory(subjectFactory());
securityManager.setRealm(this.userRealm());
return securityManager;
} /** 用户realm @return
*/
@Bean
public CasRealm userRealm(){
CasRealm userRealm = new CasRealm();
//——新加代码开始——-
// 使用自定义的realm
userRealm.setClientName(clientName);
userRealm.setCachingEnabled(false);
//暂时不使用缓存
userRealm.setAuthenticationCachingEnabled(false);
userRealm.setAuthorizationCachingEnabled(false);
//———-新加代码结束———-
return userRealm;
} /** 使用 pac4j 的 subjectFactory @return
*/
@Bean
public Pac4jSubjectFactory subjectFactory(){
return new Pac4jSubjectFactory();
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new DelegatingFilterProxy(“shiroFilter”));
// 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
filterRegistration.addInitParameter(“targetFilterLifecycle”, “true”);
filterRegistration.setEnabled(true);
filterRegistration.addUrlPatterns("/*");
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
return filterRegistration;
}
private void loadShiroFilterChain(ShiroPermsFilterFactoryBean shiroFilter) {
shiroFilter.setLoginUrl("/doLogin");
shiroFilter.setSuccessUrl("/");
// 跳转cas服务器
// shiroFilter.setUnauthorizedUrl("/error/403"); // user过滤器,处理ajax请求超时不跳转情况 Map<String, Filter> filters = new HashMap<>(2); filters.put(“user”, new UserFilter()); filters.put(“perms”, new UserPermFilter()); shiroFilter.setFilters(filters); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/callback", “callbackFilter”); filterMap.put("/logout", “logout”); filterMap.put("/doLogin", “anon”); filterMap.put("/static/**", “anon”); filterMap.put("/error/**", “anon”); filterMap.put("/captcha.jpg", “anon”); filterMap.put("/rest/**", “anon”); filterMap.put("/*", “securityFilter”); shiroFilter.setFilterChainDefinitionMap(filterMap);
} /** shiro过滤器 /rest/**,请求采用token验证(com.audaque.gm.support.interceptor.RestApiInterceptor) @param securityManager @return
*/
@Bean(“shiroFilter”)
public ShiroPermsFilterFactoryBean shiroFilter(SecurityManager securityManager, Config config) {
ShiroPermsFilterFactoryBean shiroFilter = new ShiroPermsFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
loadShiroFilterChain(shiroFilter);
HashMap<String, Filter> filterMap = new HashMap<>(3);
// cas 资源认证拦截器
SecurityFilter securityFilter = new SecurityFilter();
securityFilter.setConfig(config);
securityFilter.setClients(clientName);
filterMap.put(“securityFilter”, securityFilter);
//cas 认证后回调拦截器
CallbackFilter callbackFilter = new CallbackFilter();
callbackFilter.setConfig(config);
callbackFilter.setDefaultUrl(projectUrl);
filterMap.put(“callbackFilter”, callbackFilter);
// 注销 拦截器
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setConfig(config);
logoutFilter.setCentralLogout(true);
logoutFilter.setLocalLogout(true);
logoutFilter.setLogoutUrlPattern(".*");
logoutFilter.setDefaultUrl(projectUrl + “/callback?client_name=” + clientName);
filterMap.put(“logout”,logoutFilter);
shiroFilter.setFilters(filterMap);
return shiroFilter;
}
//——新加代码开始———-
@Bean
public SessionDAO sessionDAO(){
return new MemorySessionDAO();
} /** 自定义cookie名称 @return
*/
@Bean
public SimpleCookie sessionIdCookie(){
SimpleCookie cookie = new SimpleCookie(“sid”);
cookie.setMaxAge(-1);
cookie.setPath("/");
cookie.setHttpOnly(false);
return cookie;
}
@Bean
public DefaultWebSessionManager sessionManager(SimpleCookie sessionIdCookie, SessionDAO sessionDAO){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionIdCookie(sessionIdCookie);
sessionManager.setSessionIdCookieEnabled(true);
//30分钟
sessionManager.setGlobalSessionTimeout(180000);
sessionManager.setSessionDAO(sessionDAO);
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionValidationSchedulerEnabled(true);
return sessionManager;
}
//———–新加代码结束————— /** session管理器 @return
*/
@Bean
public SessionManager sessionManager(GlobalProperties globalProperties){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
if (globalProperties.isRedisSessionDao()) {
// 开启redis会话管理器
sessionManager.setSessionFactory(new UserSessionFactory());
sessionManager.setSessionDAO(new UserSessionDAO());
List sessionListeners = new ArrayList<>();
sessionListeners.add(new UserSessionListener());
sessionManager.setSessionListeners(sessionListeners);
}
return sessionManager;
} /** 使用cglib方式创建代理对象 @return
*/
@Bean
@DependsOn(“lifecycleBeanPostProcessor”)
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
} /** shiro生命周期处理器 @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
} /** 启用注解 @param securityManager @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
//——-新加代码开始———
@Bean
public FilterRegistrationBean singleSignOutFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setName(“singleSignOutFilter”);
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casServerUrl);
singleSignOutFilter.setIgnoreInitConfiguration(true);
bean.setFilter(singleSignOutFilter);
bean.addUrlPatterns("/*");
bean.setEnabled(true);
return bean;
}
//——新加代码结束———-
}
package com.audaque.gm.modules.sys.sso;
import com.audaque.gm.common.utils.ShiroUtils;
import com.audaque.gm.modules.sys.entity.SysUserEntity;
import com.audaque.gm.modules.sys.service.SysUserService;
import com.audaque.gm.modules.task.GetOaUserTask;
import io.buji.pac4j.realm.Pac4jRealm;
import io.buji.pac4j.subject.Pac4jPrincipal;
import io.buji.pac4j.token.Pac4jToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.pac4j.core.profile.CommonProfile;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Set;
@Slf4j
public class CasRealm extends Pac4jRealm { private String clientName; @Autowired private SysUserService sysUserService; /** * 权限验证 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("—- [CasRealm] »» doGetAuthorizationInfo"); Long userId = ShiroUtils.getUserId(); Set roles = sysUserService.listUserRoles(userId); Set perms = sysUserService.listUserPerms(userId); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(perms); return info; } /** * 登录验证 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { log.info("—- [CasRealm] »» doGetAuthenticationInfo"); String username; String password = null; int hashCode = 0; SimpleAuthenticationInfo simpleAuthenticationInfo; if (token instanceof UsernamePasswordToken) { username = (String) token.getPrincipal(); password = new String((char[])token.getCredentials()); } else { Pac4jToken pac4jToken = (Pac4jToken) token; List profiles = pac4jToken.getProfiles(); hashCode = profiles.hashCode(); username = pac4jToken.getProfiles().get(0).getId(); } SysUserEntity user = sysUserService.getByUserName(username); //如果数据库中没有该用户,先把它添加到数据库,再执行操作 if(user==null) { GetOaUserTask task = new GetOaUserTask(); task.run(); } //再查询一遍 user = sysUserService.getByUserName(username); simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, password == null ? hashCode : password, getName()); return simpleAuthenticationInfo; } @Override public boolean supports(AuthenticationToken token) { Boolean flag = false; if (super.supports(token) || token instanceof UsernamePasswordToken) { flag = true; } return flag; } public String getClientName() { return clientName; } public void setClientName(String clientName) { this.clientName = clientName; }
}
package com.audaque.gm.modules.sys.sso;
import lombok.extern.slf4j.Slf4j;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.core.context.Pac4jConstants;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.http.ajax.AjaxRequestResolver;
import org.pac4j.core.redirect.RedirectAction;
import org.pac4j.core.redirect.RedirectActionBuilder;
import org.pac4j.core.util.CommonHelper;
@Slf4j
public class CasClient extends org.pac4j.cas.client.CasClient { public CasClient() { super(); } public CasClient(CasConfiguration configuration) { super(configuration); } @Override public RedirectAction getRedirectAction(WebContext context) { log.info("—- [CasClient] »» getRedirectAction"); this.init(); AjaxRequestResolver requestResolver = getAjaxRequestResolver(); if (requestResolver.isAjax(context)) { RedirectAction action = getRedirectActionBuilder().redirect(context); this.cleanRequestedUrl(context); return requestResolver.buildAjaxResponse(action.getLocation(), context); } else { final String attemptedAuth = (String)context.getSessionStore().get(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX); if (CommonHelper.isNotBlank(attemptedAuth)) { this.cleanAttemptedAuthentication(context); this.cleanRequestedUrl(context); return getRedirectActionBuilder().redirect(context); } else { RedirectActionBuilder redirectActionBuilder = getRedirectActionBuilder(); RedirectAction redirect = redirectActionBuilder.redirect(context); return redirect; } } } private void cleanRequestedUrl(WebContext context) { SessionStore sessionStore = context.getSessionStore(); if (sessionStore.get(context, Pac4jConstants.REQUESTED_URL) != null) { sessionStore.set(context, Pac4jConstants.REQUESTED_URL, “”); } } private void cleanAttemptedAuthentication(WebContext context) { SessionStore sessionStore = context.getSessionStore(); if (sessionStore.get(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX) != null) { sessionStore.set(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX, “”); } }
}
package com.audaque.gm.modules.sys.sso;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
@Slf4j
public class CallbackFilter extends io.buji.pac4j.filter.CallbackFilter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("—- [CallbackFilter] »» doFilter"); super.doFilter(servletRequest, servletResponse, filterChain); }
}
package com.audaque.gm.config;
import com.audaque.gm.modules.sys.sso.CasClient;
import io.buji.pac4j.context.ShiroSessionStore;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.cas.config.CasProtocol;
import org.pac4j.core.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Pac4jConfig { /** 地址为:cas地址 */ @Value("${cas.server.url}") private String casServerUrl; /** 地址为:验证返回后的项目地址:http://localhost:8081 */ @Value("${cas.project.url}") private String projectUrl; /** 相当于一个标志,可以随意 */ @Value("${cas.client-name}") private String clientName; /** * pac4j 配置 * @param casClient * @param shiroSessionStore * @return */ @Bean public Config config(CasClient casClient, ShiroSessionStore shiroSessionStore) { Config config = new Config(casClient); config.setSessionStore(shiroSessionStore); return config; } /** * 自定义存储 * @return */ @Bean public ShiroSessionStore shiroSessionStore(){ return new ShiroSessionStore(); } /** * cas 客户端配置 * @param casConfig * @return */ @Bean public CasClient casClient(CasConfiguration casConfig){ CasClient casClient = new CasClient(casConfig); //客户端回调地址 casClient.setCallbackUrl(projectUrl + “/callback?client_name=” + clientName); casClient.setName(clientName); return casClient; } /** * 请求cas服务端配置 */ @Bean public CasConfiguration casConfig(){ final CasConfiguration configuration = new CasConfiguration(); //CAS server登录地址 configuration.setLoginUrl(casServerUrl + “/login”); //CAS 版本,默认为 CAS30 configuration.setProtocol(CasProtocol.CAS20); configuration.setAcceptAnyProxy(true); configuration.setPrefixUrl(casServerUrl + “/”); return configuration; }
}
package com.audaque.gm.modules.sys.shiro;
import com.audaque.gm.modules.sys.entity.SysMenuEntity;
import com.audaque.gm.common.entity.Query;
import com.audaque.gm.common.utils.SpringContextUtils;
import com.audaque.gm.modules.sys.mapper.SysMenuMapper;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** 产生责任链,确定每个url的访问权限 */
public class ShiroPermsFilterFactoryBean extends ShiroFilterFactoryBean {
private static final Logger log = LoggerFactory.getLogger(ShiroPermsFilterFactoryBean.class);
private SysMenuMapper sysMenuMapper = SpringContextUtils.getBean(“sysMenuMapper”, SysMenuMapper.class); /** 默认配置权限链
*/
public static final Map<String, String> DEFAULT_CHAIN_MAP = new HashMap<>(16); /** 增加数据库权限 @param filterChainDefinitionMap
*/
@Override
public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
log.info("—- [ShiroPermsFilterFactoryBean] »» setFilterChainDefinitionMap");
// 清空默认配置之后保存配置
DEFAULT_CHAIN_MAP.clear();
DEFAULT_CHAIN_MAP.putAll(filterChainDefinitionMap);
// 从菜单表中查询菜单配置
List lists = sysMenuMapper.list(new Query());
for(SysMenuEntity menu : lists) {
String permKey = menu.getPerms();
String permUrl = menu.getUrl();
if(StringUtils.isNotEmpty(permKey) && StringUtils.isNotEmpty(permUrl)) {
filterChainDefinitionMap.put(permUrl, “perms[” + permKey + “]”);
}
}
filterChainDefinitionMap.put("/**", “user”);
filterChainDefinitionMap.put("/*", “securityFilter”);
super.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("[ShiroPermsFilterFactoryBean] »»»> init perms finished.");
}
}
application.yml 配置:
cas单点登录配置
cas:
project:
url: http://172.16.110.92:8087/Apiweb
server:
url: http://10.143.2.174:8089/audaque-platform
client-name: apiClient
logoutUrl: http://10.143.2.174:8089/audaque-platform/logout superming168 | 菜鸟二级 | 园豆: 202
提问于:2019-05-07 17:44 显示帮助
使用"Ctrl+Enter"可进行快捷提交,评论支持部分 Markdown 语法:[link](http://zshipu.com/t/index.html?url=http://example.com) _italic_ **bold** `code`。
< > 分享
分享您的问题

清除回答草稿
您需要 登录 以后才能回答,未注册用户请先 注册 。