使用Spring Boot和Shiro集成Pac4j实现CAS单点登录 -- 知识铺
新开的项目,果断使用 spring boot 最新版本 2.0.3 ,免得后期升级坑太多,前期把雷先排了。
由于对 shiro 比较熟,故使用 shiro 来做权限控制。同时已经存在了 cas 认证中心, shiro 官方在 1.2 中就表明已经弃用了 CasFilter ,建议使用 buji-pac4j ,故使用 pac4j 来做单点登录的控制。
废话不说,代码如下:
2018-08-29更新:由于pac4j 3.1 版本未支持单点登出,故升级到 4.0.0 版本,pac4j-cas 升级到 3.0.2版本,可以实现单点登出。
首先是 maven 配置。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-cas</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<version>4.0.0</version>
<exclusions>
<exclusion>
<artifactId>shiro-web</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
<span>import</span><span> io.buji.pac4j.filter.LogoutFilter;
</span><span>import</span><span> io.buji.pac4j.filter.SecurityFilter;
</span><span>import</span><span> io.buji.pac4j.subject.Pac4jSubjectFactory;
</span><span>import</span><span> org.apache.shiro.session.mgt.SessionManager;
</span><span>import</span><span> org.apache.shiro.session.mgt.eis.MemorySessionDAO;
</span><span>import</span><span> org.apache.shiro.session.mgt.eis.SessionDAO;
</span><span>import</span><span> org.apache.shiro.spring.LifecycleBeanPostProcessor;
</span><span>import</span><span> org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
</span><span>import</span><span> org.apache.shiro.spring.web.ShiroFilterFactoryBean;
</span><span>import</span><span> org.apache.shiro.web.mgt.DefaultWebSecurityManager;
</span><span>import</span><span> org.apache.shiro.web.servlet.SimpleCookie;
</span><span>import</span><span> org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
</span><span>import</span><span> org.pac4j.core.config.Config;
</span><span>import</span><span> org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
</span><span>import</span><span> org.springframework.beans.factory.annotation.Value;
</span><span>import</span><span> org.springframework.boot.web.servlet.FilterRegistrationBean;
</span><span>import</span><span> org.springframework.context.annotation.Bean;
</span><span>import</span><span> org.springframework.context.annotation.Configuration;
</span><span>import</span><span> org.springframework.context.annotation.DependsOn;
</span><span>import</span><span> org.springframework.web.filter.DelegatingFilterProxy;<br>import org.jasig.cas.client.session.SingleSignOutFilter;
</span><span>import</span><span> javax.servlet.DispatcherType;
</span><span>import</span><span> javax.servlet.Filter;
</span><span>import</span><span> java.util.HashMap;
</span><span>import</span><span> java.util.LinkedHashMap;
</span><span>import</span><span> java.util.Map;
</span><span>/**</span><span>
* </span><span>@author</span><span> gongtao
* </span><span>@version</span><span> 2018-03-30 10:49
* @update 2018-08-29 升级 pac4j 版本到 4.0.0
*</span><span>*/</span><span>
@Configuration
</span><span>public</span> <span>class</span><span> ShiroConfig {
</span><span>/**</span><span> 项目工程路径 </span><span>*/</span><span>
@Value(</span>"${cas.project.url}"<span>)
</span><span>private</span><span> String projectUrl;
</span><span>/**</span><span> 项目cas服务路径 </span><span>*/</span><span>
@Value(</span>"${cas.server.url}"<span>)
</span><span>private</span><span> String casServerUrl;
</span><span>/**</span><span> 客户端名称 </span><span>*/</span><span>
@Value(</span>"${cas.client-name}"<span>)
</span><span>private</span><span> String clientName;
@Bean(</span>"securityManager"<span>)
</span><span>public</span><span> DefaultWebSecurityManager securityManager(Pac4jSubjectFactory subjectFactory, SessionManager sessionManager, CasRealm casRealm){
DefaultWebSecurityManager manager </span>= <span>new</span><span> DefaultWebSecurityManager();
manager.setRealm(casRealm);
manager.setSubjectFactory(subjectFactory);
manager.setSessionManager(sessionManager);
</span><span>return</span><span> manager;
}
@Bean
</span><span>public</span><span> CasRealm casRealm(){
CasRealm realm </span>= <span>new</span><span> CasRealm();
</span><span>//</span><span> 使用自定义的realm</span>
<span> realm.setClientName(clientName);
realm.setCachingEnabled(</span><span>false</span><span>);
</span><span>//</span><span>暂时不使用缓存</span>
realm.setAuthenticationCachingEnabled(<span>false</span><span>);
realm.setAuthorizationCachingEnabled(</span><span>false</span><span>);
</span><span>//</span><span>realm.setAuthenticationCacheName("authenticationCache");
</span><span>//</span><span>realm.setAuthorizationCacheName("authorizationCache");</span>
<span>return</span><span> realm;
}
</span><span>/**</span><span>
* 使用 pac4j 的 subjectFactory
* </span><span>@return</span>
<span>*/</span><span>
@Bean
</span><span>public</span><span> Pac4jSubjectFactory subjectFactory(){
</span><span>return</span> <span>new</span><span> Pac4jSubjectFactory();
}
@Bean
</span><span>public</span><span> FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistration </span>= <span>new</span><span> FilterRegistrationBean();
filterRegistration.setFilter(</span><span>new</span> DelegatingFilterProxy("shiroFilter"<span>));
</span><span>//</span><span> 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理</span>
filterRegistration.addInitParameter("targetFilterLifecycle", "true"<span>);
filterRegistration.setEnabled(</span><span>true</span><span>);
filterRegistration.addUrlPatterns(</span>"/*"<span>);
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
</span><span>return</span><span> filterRegistration;
}
</span><span>/**</span><span>
* 加载shiroFilter权限控制规则(从数据库读取然后配置)
* </span><span>@param</span><span> shiroFilterFactoryBean
</span><span>*/</span>
<span>private</span> <span>void</span><span> loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){
</span><span>/*</span><span>下面这些规则配置最好配置到配置文件中 </span><span>*/</span><span>
Map</span><String, String> filterChainDefinitionMap = <span>new</span> LinkedHashMap<><span>();
filterChainDefinitionMap.put(</span>"/", "securityFilter"<span>);
filterChainDefinitionMap.put(</span>"/application/**", "securityFilter"<span>);
filterChainDefinitionMap.put(</span>"/index", "securityFilter"<span>);
filterChainDefinitionMap.put(</span>"/callback", "callbackFilter"<span>);
filterChainDefinitionMap.put(</span>"/logout", "logout"<span>);
filterChainDefinitionMap.put(</span>"/**","anon"<span>);
</span><span>//</span><span> filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]");</span>
<span> shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
</span><span>/**</span><span>
* shiroFilter
* </span><span>@param</span><span> securityManager
* </span><span>@param</span><span> config
* </span><span>@return</span>
<span>*/</span><span>
@Bean(</span>"shiroFilter"<span>)
</span><span>public</span><span> ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager, Config config) {
ShiroFilterFactoryBean shiroFilterFactoryBean </span>= <span>new</span><span> ShiroFilterFactoryBean();
</span><span>//</span><span> 必须设置 SecurityManager</span>
<span> shiroFilterFactoryBean.setSecurityManager(securityManager);
</span><span>//</span><span>shiroFilterFactoryBean.setUnauthorizedUrl("/403");
</span><span>//</span><span> 添加casFilter到shiroFilter中</span>
<span> loadShiroFilterChain(shiroFilterFactoryBean);
Map</span><String, Filter> filters = <span>new</span> HashMap<>(3<span>);
</span><span>//</span><span>cas 资源认证拦截器</span>
SecurityFilter securityFilter = <span>new</span><span> SecurityFilter();
securityFilter.setConfig(config);
securityFilter.setClients(clientName);
filters.put(</span>"securityFilter"<span>, securityFilter);
</span><span>//</span><span>cas 认证后回调拦截器</span>
CallbackFilter callbackFilter = <span>new</span><span> CallbackFilter();
callbackFilter.setConfig(config);
callbackFilter.setDefaultUrl(projectUrl);
filters.put(</span>"callbackFilter"<span>, callbackFilter);
</span><span>//</span><span> 注销 拦截器</span>
LogoutFilter logoutFilter = <span>new</span><span> LogoutFilter();
logoutFilter.setConfig(config);
logoutFilter.setCentralLogout(</span><span>true</span><span>);
logoutFilter.setLocalLogout(</span><span>true</span><span>);
logoutFilter.setDefaultUrl(projectUrl </span>+ "/callback?client_name=" +<span> clientName);
filters.put(</span>"logout"<span>,logoutFilter);
shiroFilterFactoryBean.setFilters(filters);
</span><span>return</span><span> shiroFilterFactoryBean;
}
@Bean
</span><span>public</span><span> SessionDAO sessionDAO(){
</span><span>return</span> <span>new</span><span> MemorySessionDAO();
}
</span><span>/**</span><span>
* 自定义cookie名称
* </span><span>@return</span>
<span>*/</span><span>
@Bean
</span><span>public</span><span> SimpleCookie sessionIdCookie(){
SimpleCookie cookie </span>= <span>new</span> SimpleCookie("sid"<span>);
cookie.setMaxAge(</span>-1<span>);
cookie.setPath(</span>"/"<span>);
cookie.setHttpOnly(</span><span>false</span><span>);
</span><span>return</span><span> cookie;
}
@Bean
</span><span>public</span><span> DefaultWebSessionManager sessionManager(SimpleCookie sessionIdCookie, SessionDAO sessionDAO){
DefaultWebSessionManager sessionManager </span>= <span>new</span><span> DefaultWebSessionManager();
sessionManager.setSessionIdCookie(sessionIdCookie);
sessionManager.setSessionIdCookieEnabled(</span><span>true</span><span>);
</span><span>//</span><span>30分钟</span>
sessionManager.setGlobalSessionTimeout(180000<span>);
sessionManager.setSessionDAO(sessionDAO);
sessionManager.setDeleteInvalidSessions(</span><span>true</span><span>);
sessionManager.setSessionValidationSchedulerEnabled(</span><span>true</span><span>);
</span><span>return</span><span> sessionManager;
}
</span><span>/**</span><span>
* 下面的代码是添加注解支持
</span><span>*/</span><span>
@Bean
@DependsOn(</span>"lifecycleBeanPostProcessor"<span>)
</span><span>public</span><span> DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator </span>= <span>new</span><span> DefaultAdvisorAutoProxyCreator();
</span><span>//</span><span> 强制使用cglib,防止重复代理和可能引起代理出错的问题
</span><span>//</span> <span>https://zhuanlan.zhihu.com/p/29161098</span>
defaultAdvisorAutoProxyCreator.setProxyTargetClass(<span>true</span><span>);
</span><span>return</span><span> defaultAdvisorAutoProxyCreator;
}
@Bean
</span><span>public</span> <span>static</span><span> LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
</span><span>return</span> <span>new</span><span> LifecycleBeanPostProcessor();
}
@Bean
</span><span>public</span><span> AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor </span>= <span>new</span><span> AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
</span><span>return</span><span> advisor;
}<br> <br> </span>
@Bean<br> public FilterRegistrationBean singleSignOutFilter() {<br> FilterRegistrationBean bean = new FilterRegistrationBean();<br> bean.setName("singleSignOutFilter");<br> SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();<br> singleSignOutFilter.setCasServerUrlPrefix(casServerUrl);<br> singleSignOutFilter.setIgnoreInitConfiguration(true);<br> bean.setFilter(singleSignOutFilter);<br> bean.addUrlPatterns("/*");<br> bean.setEnabled(true);<br> bean.setOrder(Ordered.HIGHEST_PERCEDENCE);<br> return bean;<br> }
<span>}</span>
上面是 shiro 的配置。
<span>import</span><span> io.buji.pac4j.context.ShiroSessionStore;
</span><span>import</span><span> org.pac4j.cas.config.CasConfiguration;
</span><span>import</span><span> org.pac4j.cas.config.CasProtocol;
</span><span>import</span><span> org.pac4j.core.config.Config;
</span><span>import</span><span> org.springframework.beans.factory.annotation.Value;
</span><span>import</span><span> org.springframework.context.annotation.Bean;
</span><span>import</span><span> org.springframework.context.annotation.Configuration;
</span><span>/**</span><span>
* </span><span>@author</span><span> gongtao
* </span><span>@version</span><span> 2018-07-06 9:35
* @update 2018-08-29 升级 pac4j 版本到 4.0.0
*</span><span>*/</span><span>
@Configuration
</span><span>public</span> <span>class</span><span> Pac4jConfig {
</span><span>/**</span><span> 地址为:cas地址 </span><span>*/</span><span>
@Value(</span>"${cas.server.url}"<span>)
</span><span>private</span><span> String casServerUrl;
</span><span>/**</span><span> 地址为:验证返回后的项目地址:</span><span>http://localhost</span><span>:8081 </span><span>*/</span><span>
@Value(</span>"${cas.project.url}"<span>)
</span><span>private</span><span> String projectUrl;
</span><span>/**</span><span> 相当于一个标志,可以随意 </span><span>*/</span><span>
@Value(</span>"${cas.client-name}"<span>)
</span><span>private</span><span> String clientName;
</span><span>/**</span><span>
* pac4j配置
* </span><span>@param</span><span> casClient
* </span><span>@param</span><span> shiroSessionStore
* </span><span>@return</span>
<span>*/</span><span>
@Bean(</span>"authcConfig"<span>)
</span><span>public</span><span> Config config(CasClient casClient, ShiroSessionStore shiroSessionStore) {
Config config </span>= <span>new</span><span> Config(casClient);
config.setSessionStore(shiroSessionStore);
</span><span>return</span><span> config;
}
</span><span>/**</span><span>
* 自定义存储
* </span><span>@return</span>
<span>*/</span><span>
@Bean
</span><span>public</span><span> ShiroSessionStore shiroSessionStore(){
</span><span>return</span> <span>new</span><span> ShiroSessionStore();
}
</span><span>/**</span><span>
* cas 客户端配置
* </span><span>@param</span><span> casConfig
* </span><span>@return</span>
<span>*/</span><span>
@Bean
</span><span>public</span><span> CasClient casClient(CasConfiguration casConfig){
CasClient casClient </span>= <span>new</span><span> CasClient(casConfig);
</span><span>//</span><span>客户端回调地址</span>
casClient.setCallbackUrl(projectUrl + "/callback?client_name=" +<span> clientName);
casClient.setName(clientName);
</span><span>return</span><span> casClient;
}
</span><span>/**</span><span>
* 请求cas服务端配置
* </span><span>@param</span><span> casLogoutHandler
</span><span>*/</span><span>
@Bean
</span><span>public</span><span> CasConfiguration casConfig(){
</span><span>final</span> CasConfiguration configuration = <span>new</span><span> CasConfiguration();
</span><span>//</span><span>CAS server登录地址</span>
configuration.setLoginUrl(casServerUrl + "/login"<span>);
</span><span>//</span><span>CAS 版本,默认为 CAS30,我们使用的是 CAS20</span>
<span> configuration.setProtocol(CasProtocol.CAS20);
configuration.setAcceptAnyProxy(</span><span>true</span><span>);
configuration.setPrefixUrl(casServerUrl </span>+ "/"<span>);
</span><span>return</span><span> configuration;
}
</span><span>
}</span>
以上为pac4j 配置
<span>import<span> org.pac4j.cas.config.CasConfiguration;
<span>import<span> org.pac4j.core.context.Pac4jConstants;
<span>import<span> org.pac4j.core.context.WebContext;
<span>import<span> org.pac4j.core.context.session.SessionStore;
<span>import<span> org.pac4j.core.redirect.RedirectAction;
<span>import<span> org.pac4j.core.util.CommonHelper;
<span>/**<span>
* <span>@author<span> gongtao
* <span>@version<span> 2018-07-06 9:41
* @update 2018-08-29 升级 pac4j 版本到 4.0.0
*<span>*/
<span>public <span>class CasClient <span>extends<span> org.pac4j.cas.client.CasClient {
<span>public<span> CasClient() {
<span>super<span>();
}
<span>public<span> CasClient(CasConfiguration configuration) {
<span>super<span>(configuration);
}
<span>/*<span>
* (non-Javadoc)
* @see org.pac4j.core.client.IndirectClient#getRedirectAction(org.pac4j.core.context.WebContext)
<span>*/<span>
@Override
<span>public<span> RedirectAction getRedirectAction(WebContext context) {
<span>this<span>.init();
<span>if<span> (getAjaxRequestResolver().isAjax(context)) {
<span>this.logger.info("AJAX request detected -> returning the appropriate action"<span>);
RedirectAction action =<span> getRedirectActionBuilder().redirect(context);
<span>this<span>.cleanRequestedUrl(context);
<span>return<span> getAjaxRequestResolver().buildAjaxResponse(action.getLocation(), context);
} <span>else<span> {
<span>final String attemptedAuth = (String)context.getSessionStore().get(context, <span>this.getName() +<span> ATTEMPTED_AUTHENTICATION_SUFFIX);
<span>if<span> (CommonHelper.isNotBlank(attemptedAuth)) {
<span>this<span>.cleanAttemptedAuthentication(context);
<span>this<span>.cleanRequestedUrl(context);
<span>//<span>这里按自己需求处理,默认是返回了401,我在这边改为跳转到cas登录页面
<span>//<span>throw HttpAction.unauthorized(context);
<span>return <span>this<span>.getRedirectActionBuilder().redirect(context);
} <span>else<span> {
<span>return <span>this<span>.getRedirectActionBuilder().redirect(context);
}
}
}
<span>private <span>void<span> cleanRequestedUrl(WebContext context) {
SessionStore<WebContext> sessionStore =<span> context.getSessionStore();
<span>if (sessionStore.get(context, Pac4jConstants.REQUESTED_URL) != <span>null<span>) {
sessionStore.set(context, Pac4jConstants.REQUESTED_URL, ""<span>);
}
}
<span>private <span>void<span> cleanAttemptedAuthentication(WebContext context) {
SessionStore<WebContext> sessionStore =<span> context.getSessionStore();
<span>if (sessionStore.get(context, <span>this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX) != <span>null<span>) {
sessionStore.set(context, <span>this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX, ""<span>);
}
}
}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
<span>/**</span><span>
* </span><span>@author</span><span> gongtao
* </span><span>@version</span><span> 2018-07-05 15:30
*</span><span>*/</span>
<span>public</span> <span>class</span> CallbackFilter <span>extends</span><span> io.buji.pac4j.filter.CallbackFilter {
@Override
</span><span>public</span> <span>void</span> doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) <span>throws</span><span> IOException, ServletException {
</span><span>super</span><span>.doFilter(servletRequest, servletResponse, filterChain);
}
}</span>
CallbackFilter 是单点登录后回调使用的过滤器。
<span>/**</span><span>
* 认证与授权
* </span><span>@author</span><span> gongtao
* </span><span>@version</span><span> 2018-03-30 13:55
*</span><span>*/</span>
<span>public</span> <span>class</span> CasRealm <span>extends</span><span> Pac4jRealm {
</span><span>private</span><span> String clientName;
</span><span>/**</span><span>
* 认证
* </span><span>@param</span><span> authenticationToken
* </span><span>@return</span><span>
* </span><span>@throws</span><span> AuthenticationException
</span><span>*/</span><span>
@Override
</span><span>protected</span> AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) <span>throws</span><span> AuthenticationException {
</span><span>final</span> Pac4jToken pac4jToken =<span> (Pac4jToken) authenticationToken;
</span><span>final</span> List<CommonProfile> commonProfileList =<span> pac4jToken.getProfiles();
</span><span>final</span> CommonProfile commonProfile = commonProfileList.get(0<span>);
System.out.println(</span>"单点登录返回的信息" +<span> commonProfile.toString());
</span><span>//</span><span>todo </span>
<span>final</span> Pac4jPrincipal principal = <span>new</span><span> Pac4jPrincipal(commonProfileList, getPrincipalNameAttribute());
</span><span>final</span> PrincipalCollection principalCollection = <span>new</span><span> SimplePrincipalCollection(principal, getName());
</span><span>return</span> <span>new</span><span> SimpleAuthenticationInfo(principalCollection, commonProfileList.hashCode());
}
</span><span>/**</span><span>
* 授权/验权(todo 后续有权限在此增加)
* </span><span>@param</span><span> principals
* </span><span>@return</span>
<span>*/</span><span>
@Override
</span><span>protected</span><span> AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authInfo </span>= <span>new</span><span> SimpleAuthorizationInfo();
authInfo.addStringPermission(</span>"user"<span>);
</span><span>return</span><span> authInfo;
}
}</span>
CasRealm 这个就是和之前 shiro 的 CasRealm 一样了。
最后就是 application.yml 的配置了。
<span>#cas配置
cas:
client</span>-<span>name: mfgClient
server:
url: http:</span><span>//</span><span>127.0.0.1:8080/cas</span>
<span> project:
url: http:</span><span>//</span><span>127.0.0.1:8081</span>
参考: https://blog.csdn.net/hxm_code/article/details/79226456
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20240424/%E4%BD%BF%E7%94%A8Spring-Boot%E5%92%8CShiro%E9%9B%86%E6%88%90Pac4j%E5%AE%9E%E7%8E%B0CAS%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95--%E7%9F%A5%E8%AF%86%E9%93%BA/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com