springboot+shiro+cas 前后端分离 知识点汇总以及遇到的坑
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