新开的项目,果断使用 spring boot 最新版本 2.0.3 ,免得后期升级坑太多,前期把雷先排了。
由于对 shiro 比较熟,故使用 shiro 来做权限控制。同时已经存在了 cas 认证中心, shiro 官方在 1.2 中就表明已经弃用了 CasFilter ,建议使用 buji-pac4j ,故使用 pac4j 来做单点登录的控制。
废话不说,代码如下:
首先是 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>2.2.1</version> </dependency> <dependency> <groupId>io.buji</groupId> <artifactId>buji-pac4j</artifactId> <version>3.1.0</version> <exclusions> <exclusion> <artifactId>shiro-web</artifactId> <groupId>org.apache.shiro</groupId> </exclusion> </exclusions> </dependency>
/** * @author gongtao * @version 2018-03-30 10:49 **/ @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(false); 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,防止重复代理和可能引起代理出错的问题 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; } }
上面是 shiro 的配置。
/** * @author gongtao * @version 2018-07-06 9:35 **/ @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); casClient.setIncludeClientNameInCallbackUrl(false); return casClient; } /** * 请求cas服务端配置 * @param casLogoutHandler */ @Bean public CasConfiguration casConfig(ShiroCasLogoutHandler casLogoutHandler){ final CasConfiguration configuration = new CasConfiguration(); //CAS server登录地址 configuration.setLoginUrl(casServerUrl + "/login"); //CAS 版本,默认为 CAS30,我们CAS中心使用的是4.0.0版本,对应的就是 CAS20 configuration.setProtocol(CasProtocol.CAS20); configuration.setAcceptAnyProxy(true); configuration.setLogoutHandler(casLogoutHandler); return configuration; } /** * shiro登出处理器,销毁session及登录状态等 */ @Bean public ShiroCasLogoutHandler casLogoutHandler(){ ShiroCasLogoutHandler casLogoutHandler = new ShiroCasLogoutHandler(); casLogoutHandler.setDestroySession(true); return casLogoutHandler; } }
/** * @author gongtao * @version 2018-07-06 9:50 **/ public class ShiroCasLogoutHandler<C extends WebContext> extends DefaultCasLogoutHandler<C> { public ShiroCasLogoutHandler() { super(); } public ShiroCasLogoutHandler(Store<String, Object> store) { super(store); } @Override protected void destroy(C context, SessionStore sessionStore, String channel) { // remove profiles final ShiroProfileManager manager = new ShiroProfileManager(context); manager.logout();//shiro登出操作 logger.debug("destroy the user profiles"); // and optionally the web session if (isDestroySession()) { logger.debug("destroy the whole session"); @SuppressWarnings("unchecked") final boolean invalidated = sessionStore.destroySession(context); if (!invalidated) { logger.error("The session has not been invalidated for {} channel logout", channel); } } } }
/** * @author gongtao * @version 2018-07-06 9:58 **/ public class ShiroProvidedSessionStore extends ShiroSessionStore { /**存储的TrackableSession,往后要操作时用这个session操作*/ private Session session; public ShiroProvidedSessionStore(Session session) { this.session = session; } @Override protected Session getSession(final boolean createSession) { return session; } }
@Slf4j public class ShiroSessionStore implements SessionStore<J2EContext> { /** * 获取shiro session * @param createSession * @return */ protected Session getSession(final boolean createSession) { return SecurityUtils.getSubject().getSession(createSession); } /** * 获取 shiro 的 sessionid * @param j2EContext * @return */ @Override public String getOrCreateSessionId(J2EContext j2EContext) { // final Session session = getSession(false); // if (session != null) { // return session.getId().toString(); // } // return null; Session session = getSession(true); return session.getId().toString(); } /** * 获取shiro session中的属性 * @param j2EContext * @param key * @return */ @Override public Object get(J2EContext j2EContext, String key) { final Session session = getSession(false); if (session != null) { return session.getAttribute(key); } return null; } /** * 设置session属性 * @param j2EContext * @param key * @param value */ @Override public void set(J2EContext j2EContext, String key, Object value) { final Session session = getSession(true); if (session != null) { try { session.setAttribute(key, value); } catch (final UnavailableSecurityManagerException e) { log.warn("Should happen just once at startup in some specific case of Shiro Spring configuration", e); } } } /** * 销毁session * @param j2EContext * @return */ @Override public boolean destroySession(J2EContext j2EContext) { getSession(true).stop(); return true; } /** * 获取shiro session并缓存用于单点登出 * @param j2EContext * @return */ @Override public Object getTrackableSession(J2EContext j2EContext) { return getSession(true); } /** * 从 getTrackableSession 中获取的session来构建SessionStore * @param j2EContext * @param trackableSession * @return */ @Override public SessionStore<J2EContext> buildFromTrackableSession(J2EContext j2EContext, Object trackableSession) { if(trackableSession != null) { return new ShiroProvidedSessionStore((Session) trackableSession); } return null; } /** * 刷新session属性,这里暂返回false,实际应用中需实现 * @param j2EContext * @return */ @Override public boolean renewSession(J2EContext j2EContext) { return false; } }
/** * @author gongtao * @version 2018-07-05 15:30 **/ public class CallbackFilter extends io.buji.pac4j.filter.CallbackFilter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { super.doFilter(servletRequest, servletResponse, filterChain); } }
CallbackFilter 是单点登录后回调使用的过滤器。
/** * 认证与授权 * @author gongtao * @version 2018-03-30 13:55 **/ public class CasRealm extends Pac4jRealm { private String clientName; /** * 认证 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { final Pac4jToken pac4jToken = (Pac4jToken) authenticationToken; final LinkedHashMap<String,CommonProfile> userProfilesMap = pac4jToken.getProfiles(); final CommonProfile commonProfile = userProfilesMap.get(clientName); System.out.println("单点登录返回的信息" + commonProfile.toString()); //todo final Pac4jPrincipal principal = new Pac4jPrincipal(userProfilesMap, getPrincipalNameAttribute()); final PrincipalCollection principalCollection = new SimplePrincipalCollection(principal, getName()); return new SimpleAuthenticationInfo(principalCollection, userProfilesMap.hashCode()); } /** * 授权/验权(todo 后续有权限在此增加) * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(); authInfo.addStringPermission("user"); return 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