1.項目要接入cas服務,記錄下這周的過程以及遇到的坑
1.配置CasRealem和AuthorizingRealm的區別
由於上個服務 自己用的springboot+shiro 而沒有整合cas,上個服務是登錄后直接去庫里面查詢,那么何時去加載這個Ream。 我剛開始是實現了AuthorizingRealm而不是CasRealem,之后交給spring管理,結果發現怎么都進入不到自己的ream里面。最后發現是配置的問題
package com.sq.unionmanage.gateway.api; import com.sq.unionmanage.gateway.service.common.datasource.DataSourceConfig; import com.sq.unionmanage.gateway.service.shiro.PlatformShiroFilterFactoryBean; import com.sq.unionmanage.gateway.service.shiro.cache.RedisCacheManager; import com.sq.unionmanage.gateway.service.shiro.filter.ShiroFormAuthenticationFilter; import com.sq.unionmanage.gateway.service.shiro.filter.SqUserFilter; import com.sq.unionmanage.gateway.service.shiro.realm.ShiroRealm; import com.sq.unionmanage.gateway.service.shiro.session.RedisSessionDAO; import com.sq.unionmanage.gateway.service.shiro.session.UuIdSessionIdGenerator; import com.sq.unionmanage.gateway.service.util.DESUtil; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.web.mgt.CookieRememberMeManager; 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.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.filter.DelegatingFilterProxy; import javax.annotation.Resource; import javax.servlet.Filter; import java.util.Base64; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * @Author: * @Date:2020/03/21 * @Description: */ @Configuration @AutoConfigureAfter(DataSourceConfig.class) public class ShiroConfiguration { @Value(value = "${cms.login.url}") private String cmsLoginUrl; @Value(value = "${homepage.url}") private String homePageUrl; @Value(value = "${service.des.secret}") private String serviceDesSecret; @Value(value = "${sso.server.url}") private String ssoServerUrl; @Value(value = "${sso.login.url}") private String ssoLoginUrl; @Value(value = "${cms.server.url}") private String cmsServerUrl; // @Resource(name="scosSerRedisTemplate") private RedisTemplate scosSerRedisTemplate; @Value("${cms.login.url}") private String localLoginUrl; //本地客戶端的認證回調地址 @Value("${service.des.secret}") private String desSecret; //本地客戶端的認證回調地址 的DES加密密鑰 @Bean public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); // /* shiroRealm.setDefaultRoles("ROLE_USER"); shiroRealm.setCasServerUrlPrefix(ssoServerUrl); //casServic的作用是 登錄成功后向客戶端回調 shiroRealm.setCasService(cmsLoginUrl); return shiroRealm; } @Bean(name ="sessionIdGenerator") public UuIdSessionIdGenerator sessionIdGenerator(){ UuIdSessionIdGenerator sessionIdGenerator = new UuIdSessionIdGenerator(); return sessionIdGenerator; } @Bean(name = "sessionDAO") public RedisSessionDAO sessionDAO(UuIdSessionIdGenerator sessionIdGenerator){ RedisSessionDAO sessionDAO = new RedisSessionDAO(); sessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache"); sessionDAO.setSessionIdGenerator(sessionIdGenerator); sessionDAO.setRedisTemplate(scosSerRedisTemplate); return sessionDAO; } @Bean(name = "sessionIdCookie") public SimpleCookie sessionIdCookie(){ SimpleCookie sessionIdCookie = new SimpleCookie("unsid"); sessionIdCookie.setHttpOnly(true); sessionIdCookie.setMaxAge(-1); return sessionIdCookie; } @Bean("sessionManager") public DefaultWebSessionManager sessionManager(RedisSessionDAO sessionDAO, SimpleCookie sessionIdCookie){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(1800000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionValidationSchedulerEnabled(true); //sessionManager.setSessionValidationScheduler(sessionValidationScheduler); sessionManager.setSessionDAO(sessionDAO); sessionManager.setSessionIdCookieEnabled(true); sessionManager.setSessionIdCookie(sessionIdCookie); return sessionManager; } //<!-- 會話過期校驗調度器 --> //<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler"> // <property name="sessionValidationInterval" value="900000" /> // <property name="sessionManager" ref="sessionManager" /> //</bean> //@Bean(name ="sessionValidationScheduler") //public QuartzSessionValidationScheduler sessionValidationScheduler(){ // QuartzSessionValidationScheduler sessionValidationScheduler = new QuartzSessionValidationScheduler(); // sessionValidationScheduler.setSessionValidationInterval(900000); // sessionValidationScheduler.setSessionManager(sessionManager); // return sessionValidationScheduler; //} @Bean(name="shiroCacheManager") public RedisCacheManager shiroCacheManager(){ RedisCacheManager shiroCacheManager = new RedisCacheManager(); shiroCacheManager.setRedisTemplate(scosSerRedisTemplate); shiroCacheManager.setExpireSeconds(1800); return shiroCacheManager; } @Bean(name="rememberMeManager") public CookieRememberMeManager rememberMeManager(SimpleCookie rememberMeCookie){ CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); byte[] cipherKey = Base64.getEncoder().encode("4AvVhmFLUs0KTA3Kprsdag==".getBytes()); rememberMeManager.setCipherKey(cipherKey); rememberMeManager.setCookie(rememberMeCookie); return rememberMeManager; } @Bean(name ="rememberMeCookie") public SimpleCookie rememberMeCookie(){ SimpleCookie simpleCookie = new SimpleCookie(); simpleCookie.setName("unionRememberMe"); simpleCookie.setHttpOnly(true); simpleCookie.setMaxAge(432000); return simpleCookie; } @Bean public DefaultWebSecurityManager securityManager(RedisCacheManager shiroCacheManager, CookieRememberMeManager rememberMeManager, ShiroRealm shiroRealm, DefaultWebSessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm); securityManager.setCacheManager(shiroCacheManager); securityManager.setSessionManager(sessionManager); // 指定 SubjectFactory,如果要實現cas的remember me的功能,需要用到下面這個CasSubjectFactory,並設置到securityManager的subjectFactory中 //securityManager.setSubjectFactory(new CasSubjectFactory()); securityManager.setRememberMeManager(rememberMeManager); return securityManager; } @Bean("casSingleSignOutFilter") public SingleSignOutFilter casSingleSignOutFilter(){ SingleSignOutFilter casSingleSignOutFilter = new SingleSignOutFilter(); //casSingleSignOutFilter.setCasServerUrlPrefix(ssoServerUrl); casSingleSignOutFilter.setIgnoreInitConfiguration(true); return casSingleSignOutFilter; } @Bean public FilterRegistrationBean delegatingFilterProxy(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); DelegatingFilterProxy proxy = new DelegatingFilterProxy(); proxy.setTargetFilterLifecycle(true); proxy.setTargetBeanName("shiroFilter"); filterRegistrationBean.setFilter(proxy); return filterRegistrationBean; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean(name = "casFilter") public ShiroFormAuthenticationFilter casFilter() throws Exception { ShiroFormAuthenticationFilter casFilter = new ShiroFormAuthenticationFilter(); casFilter.setName("casFilter"); casFilter.setEnabled(true); casFilter.setFailureUrl("/unauthorized"); //casFilter.setLoginUrl(homePageUrl); return casFilter; } @Bean(name = "sqUserFilter") public SqUserFilter sqUserFilter() throws Exception { SqUserFilter sqUserFilter = new SqUserFilter(); return sqUserFilter; } @Bean(name ="shiroFilter") public PlatformShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager, ShiroFormAuthenticationFilter casFilter, SqUserFilter sqUserFilter) throws Exception { PlatformShiroFilterFactoryBean shiroFilterFactoryBean = new PlatformShiroFilterFactoryBean(); Map<String , Filter> filters = new HashMap<>(); filters.put("casFilter", casFilter); filters.put("sqUserFilter", sqUserFilter); shiroFilterFactoryBean.setFilters(filters); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/unauthorized"); //shiroFilterFactoryBean.setSuccessUrl(homePageUrl); shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); //shiroFilterFactoryBean.setDesSecret(serviceDesSecret); //注意此處使用的是LinkedHashMapU,是有順序的,shiro會按從上到下的順序匹配驗證,匹配了就不再繼續驗證 //所以上面的url要苛刻,寬松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的話其后的驗證規則就沒作用了。 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "casFilter"); //1.不攔截的請求 filterChainDefinitionMap.put("/unauthorized", "anon"); //filterChainDefinitionMap.put(homePageUrl,"anon"); filterChainDefinitionMap.put("/nginx.html", "anon"); filterChainDefinitionMap.put("/needlogin", "anon"); filterChainDefinitionMap.put("/logout", "anon"); //3.需要登錄 authc:該過濾器下的頁面必須登錄后才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter filterChainDefinitionMap.put("/mp/**", "authc"); //4.登錄過的不攔截 filterChainDefinitionMap.put("/**", "user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } }
2.shiroFilter 注入的幾個問題
3.302問題
登錄成功后,shiro會通過cas回調到我的服務器地址,這個時候一般是需要我們去配置一個首頁進行跳轉
package com.sq.unionmanage.gateway.service.shiro.filter; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.sq.unionmanage.gateway.service.common.ResponseResult; import org.apache.http.HttpStatus; 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.springframework.beans.factory.annotation.Value; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** * @Author:qxx * @Date:2019/9/6 * @Description: */ public class ShiroFormAuthenticationFilter extends CasFilter { @Value(value = "${homepage.url}") private String homePageUrl; @Value(value = "${sso.login.url}") private String ssoLoginUrl; //cas sso 登錄頁面 @Value("${cms.login.url}") private String localLoginUrl; //本地客戶端的認證回調地址 @Value("${service.des.secret}") private String desSecret; //本地客戶端的認證回調地址 的DES加密密鑰 /*@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return true; }*/ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { System.out.println("==========executeLogin=========="); AuthenticationToken token = createToken(request, response); try { System.out.println("========= token 是否為空===" + token); } catch (Exception e) { e.printStackTrace(); } if (token == null) { String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " + "must be created in order to execute a login attempt."; throw new IllegalStateException(msg); } try { System.out.println("========是否登錄成功==========="); Subject subject = getSubject(request, response); subject.login(token); System.out.println("========登錄成功==========="); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } @Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse rep = toHttp(response); System.out.println("=====onLoginSuccess====="); rep.setStatus(302); rep.setHeader("Location", homePageUrl); return true; } public HttpServletRequest toHttp(ServletRequest request) { return (HttpServletRequest)request; } public HttpServletResponse toHttp(ServletResponse response) { return (HttpServletResponse)response; } }
4.跨域問題
shiro登錄成功后,因為源碼里面有重定向,會導致header里面的信息都置為空,導致在前后端分時候出現跨域。解決辦法:
package com.sq.unionmanage.gateway.api.web.filter; import org.apache.http.HttpStatus; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @Author fanht * @Description 解決shiro 未認證后cors 跨域同源問題 * @Date 2020/3/11 下午7:12 * @Version 1.0 */ @Component @Order(1) public class CORSFilter extends BasicHttpAuthenticationFilter{ private static Logger logger = LoggerFactory.getLogger(AccessFilter.class); @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if(req.getMethod().equals(RequestMethod.OPTIONS.name())){ res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin")); res.setHeader("Access-Control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE"); // 響應首部 Access-Control-Allow-Headers 用於 preflight request (預檢請求)中,列出了將會在正式請求的 Access-Control-Expose-Headers 字段中出現的首部信息。修改為請求首部 res.setHeader("Access-Control-Allow-Headers",req.getHeader("Access-Control-Request-Headers")); //給option請求直接返回正常狀態 res.setStatus(HttpStatus.SC_OK); return false; } return super.preHandle(request, response); } }
package com.sq.unionmanage.gateway.api.web.filter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; /** * @Author fanht * @Description * @Date 2020/3/24 下午8:06 * @Version 1.0 */ @Component //@ConditionalOnProperty(name = "server.scheme.chanage.enabled", havingValue = "true") // 開啟注解才會啟動 public class RedirectFilterConfig { @Bean public FilterRegistrationBean registFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new AbsoluteSendRedirectFilter()); registration.addUrlPatterns("*"); registration.setName("filterRegistrationBean"); registration.setOrder(1); return registration; } }
5.跨域問題解決后,會提示 http轉https問題 解決辦法
package com.sq.unionmanage.gateway.api.web.filter; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; /** * @program: union-manage-gateway * @description: * @author: zjw * @create: 2020-03-16 16:39 **/ @Component //@Order(0) public class HttpTransWrapper extends HttpServletResponseWrapper{ private Logger logger = LoggerFactory.getLogger(this.getClass()); private final HttpServletRequest request; /** * Constructs a response adaptor wrapping the given response. * * @param response The response to be wrapped * @throws IllegalArgumentException if the response is null */ public HttpTransWrapper(final HttpServletRequest req, HttpServletResponse response) { super(response); this.request = req; } @Override public void sendRedirect(String location) throws IOException { if(StringUtils.isEmpty(location)){ super.sendRedirect(location); return; } try { final URI uri = new URI(location); if(uri.getScheme() != null){ super.sendRedirect(location); return; } } catch (URISyntaxException e) { logger.error("=======跳轉異常========" + e); super.sendRedirect(location); } String finalUrl = "https://" + this.request.getServerName(); if(request.getServerPort() != 80 && request.getServerPort() != 443 ){ finalUrl += ":" + request.getServerPort(); } finalUrl += location; logger.info("finalUrl:{}",finalUrl); if(finalUrl.indexOf("localhost") > 0){ //todo 如果是本地測試 仍然用http的 super.sendRedirect(location); }else { super.sendRedirect(finalUrl); } } }
package com.sq.unionmanage.gateway.api.web.filter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; /** * @Author fanht * @Description * @Date 2020/3/24 下午8:06 * @Version 1.0 */ @Component //@ConditionalOnProperty(name = "server.scheme.chanage.enabled", havingValue = "true") // 開啟注解才會啟動 public class RedirectFilterConfig { @Bean public FilterRegistrationBean registFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new AbsoluteSendRedirectFilter()); registration.addUrlPatterns("*"); registration.setName("filterRegistrationBean"); registration.setOrder(1); return registration; } }
7.登錄cas回調成功后,h5cookieId為空問題,我是在回調成功后將后端的sessionId給到了H5,讓他們加到session里面,原來以為這樣是可以的,結果還是不行。
8.沒有辦法,最終還是通過運維來解決:大體思路是這樣的: sso登錄成功后,回調地址配置為前端的地址,然后當前端向后端請求時候,nginx做轉發,轉發到后端的ip上面,這樣因為在sso轉發時候cookie已經種在了前端的域名下,nginx綁定到了我們后端的ip上,相當於是 沒有跨域了,也就不會出現跨域和cookie丟失的問題。然后測試發現,是可以的。
參考:https://segmentfault.com/a/1190000015235402
https://blog.csdn.net/qq_21251983/article/details/87631991