spring boot 2.0 集成 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></pre>
import io.buji.pac4j.filter.LogoutFilter; import io.buji.pac4j.filter.SecurityFilter; import io.buji.pac4j.subject.Pac4jSubjectFactory; 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.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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.web.filter.DelegatingFilterProxy;
import org.jasig.cas.client.session.SingleSignOutFilter; import javax.servlet.DispatcherType; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map;/**
@author gongtao
@version 2018-03-30 10:49
@update 2018-08-29 升级 pac4j 版本到 4.0.0
/
@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;@Bean("securityManager")
public DefaultWebSecurityManager securityManager(Pac4jSubjectFactory subjectFactory, SessionManager sessionManager, CasRealm casRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(casRealm);
manager.setSubjectFactory(subjectFactory);
manager.setSessionManager(sessionManager);
return manager;
}@Bean
public CasRealm casRealm(){
CasRealm realm = new CasRealm();
// 使用自定义的 realm
realm.setClientName(clientName);
realm.setCachingEnabled(false);
//暂时不使用缓存
realm.setAuthenticationCachingEnabled(false);
realm.setAuthorizationCachingEnabled(false);
//realm.setAuthenticationCacheName("authenticationCache");
//realm.setAuthorizationCacheName("authorizationCache");
return realm;
}/**
- 使用 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;
}/**
- 加载 shiroFilter 权限控制规则(从数据库读取然后配置)
- @param shiroFilterFactoryBean
/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){
/下面这些规则配置最好配置到配置文件中 */
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/", "securityFilter");
filterChainDefinitionMap.put("/application/","securityFilter");
filterChainDefinitionMap.put("/index", "securityFilter");
filterChainDefinitionMap.put("/callback", "callbackFilter");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/","anon");
// filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}/**
- shiroFilter
- @param securityManager
- @param config
- @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager, Config config) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// 添加 casFilter 到 shiroFilter 中
loadShiroFilterChain(shiroFilterFactoryBean);
Map<String, Filter> filters = new HashMap<>(3);
//cas 资源认证拦截器
SecurityFilter securityFilter = new SecurityFilter();
securityFilter.setConfig(config);
securityFilter.setClients(clientName);
filters.put("securityFilter", securityFilter);
//cas 认证后回调拦截器
CallbackFilter callbackFilter = new CallbackFilter();
callbackFilter.setConfig(config);
callbackFilter.setDefaultUrl(projectUrl);
filters.put("callbackFilter", callbackFilter);
// 注销 拦截器
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setConfig(config);
logoutFilter.setCentralLogout(true);
logoutFilter.setLocalLogout(true);
logoutFilter.setDefaultUrl(projectUrl + "/callback?client_name=" + clientName);
filters.put("logout",logoutFilter);
shiroFilterFactoryBean.setFilters(filters);
return shiroFilterFactoryBean;
}@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;
}/**
- 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用 cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}@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);
bean.setOrder(Ordered.HIGHEST_PERCEDENCE);
return bean;
}
}
上面是 shiro 的配置。
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;/**
@author gongtao
@version 2018-07-06 9:35
@update 2018-08-29 升级 pac4j 版本到 4.0.0
/
@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("authcConfig")
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 服务端配置
- @param casLogoutHandler
*/
@Bean
public CasConfiguration casConfig(){
final CasConfiguration configuration = new CasConfiguration();
//CAS server 登录地址
configuration.setLoginUrl(casServerUrl + "/login");
//CAS 版本,默认为 CAS30,我们使用的是 CAS20
configuration.setProtocol(CasProtocol.CAS20);
configuration.setAcceptAnyProxy(true);
configuration.setPrefixUrl(casServerUrl + "/");
return configuration;
}}
以上为 pac4j 配置
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.redirect.RedirectAction;
import org.pac4j.core.util.CommonHelper;
/**
-
@author gongtao
-
@version 2018-07-06 9:41
-
@update 2018-08-29 升级 pac4j 版本到 4.0.0
/
public class CasClient extends org.pac4j.cas.client.CasClient {
public CasClient() {
super();
}
public CasClient(CasConfiguration configuration) {
super(configuration);
}
/*
- (non-Javadoc)
- @see org.pac4j.core.client.IndirectClient#getRedirectAction(org.pac4j.core.context.WebContext)
*/
@Override
public RedirectAction getRedirectAction(WebContext context) {
this.init();
if (getAjaxRequestResolver().isAjax(context)) {
this.logger.info("AJAX request detected -> returning the appropriate action");
RedirectAction action = getRedirectActionBuilder().redirect(context);
this.cleanRequestedUrl(context);
return getAjaxRequestResolver().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);
//这里按自己需求处理,默认是返回了 401,我在这边改为跳转到 cas 登录页面
//throw HttpAction.unauthorized(context);
return this.getRedirectActionBuilder().redirect(context);
} else {
return this.getRedirectActionBuilder().redirect(context);
}
}
}
private void cleanRequestedUrl(WebContext context) {
SessionStore<WebContext> sessionStore = context.getSessionStore();
if (sessionStore.get(context, Pac4jConstants.REQUESTED_URL) != null) {
sessionStore.set(context, Pac4jConstants.REQUESTED_URL, "");
}
}
private void cleanAttemptedAuthentication(WebContext context) {
SessionStore<WebContext> sessionStore = context.getSessionStore();
if (sessionStore.get(context, this.getName()+ ATTEMPTED_AUTHENTICATION_SUFFIX) != null) {
sessionStore.set(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX, "");
}
}
}
/** * @author gongtao * @version 2018-07-05 15:30 **/ public class CallbackFilter extends io.buji.pac4j.filter.CallbackFilter {@Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> IOException, ServletException { </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.doFilter(servletRequest, servletResponse, filterChain); }
}
CallbackFilter 是单点登录后回调使用的过滤器。
/** * 认证与授权 * @author gongtao * @version 2018-03-30 13:55 **/ public class CasRealm extends Pac4jRealm {</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String clientName; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 认证 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> authenticationToken * </span><span style="color: rgba(128, 128, 128, 1)">@return</span><span style="color: rgba(0, 128, 0, 1)"> * </span><span style="color: rgba(128, 128, 128, 1)">@throws</span><span style="color: rgba(0, 128, 0, 1)"> AuthenticationException </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Override </span><span style="color: rgba(0, 0, 255, 1)">protected</span> AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> AuthenticationException { </span><span style="color: rgba(0, 0, 255, 1)">final</span> Pac4jToken pac4jToken =<span style="color: rgba(0, 0, 0, 1)"> (Pac4jToken) authenticationToken; </span><span style="color: rgba(0, 0, 255, 1)">final</span> List<CommonProfile> commonProfileList =<span style="color: rgba(0, 0, 0, 1)"> pac4jToken.getProfiles();
final CommonProfile commonProfile = commonProfileList.get(0);
System.out.println("单点登录返回的信息" + commonProfile.toString());
//todo
final Pac4jPrincipal principal = new Pac4jPrincipal(commonProfileList, getPrincipalNameAttribute());
final PrincipalCollection principalCollection = new SimplePrincipalCollection(principal, getName());
return new SimpleAuthenticationInfo(principalCollection, commonProfileList.hashCode());
}</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 授权/验权(todo 后续有权限在此增加) * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> principals * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Override </span><span style="color: rgba(0, 0, 255, 1)">protected</span><span style="color: rgba(0, 0, 0, 1)"> AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authInfo </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SimpleAuthorizationInfo(); authInfo.addStringPermission(</span>"user"<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> authInfo; }
}
CasRealm 这个就是和之前 shiro 的 CasRealm 一样了。
最后就是 application.yml 的配置了。
#cas 配置 cas: client-name: mfgClient server: url: http://127.0.0.1:8080/cas project: url: http://127.0.0.1:8081
参考: https://blog.csdn.net/hxm_code/article/details/79226456