前后端分离实现单点登录
环境介绍 前端vue单独部署 后端springboot单独部署
会遇到的问题 跨域 cas认证失败无法重定向,前端302无法捕捉。
问题解决 1、跨域 直接上代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class CorsConfig implements WebMvcConfigurer { @Override @Order(0) public void addCorsMappings(CorsRegistry registry) { //设置允许跨域的路径 registry.addMapping("/**") //设置允许跨域请求的域名 .allowedOrigins("*") //这里:是否允许证书 不再默认开启 .allowCredentials(true) //设置允许的方法 .allowedMethods("*") //跨域允许时间 .maxAge(3600); } }
2、cas认证失败无法重定向,前端302无法捕捉 这里就比较复杂了,我的方案就是CAS源码竟然是无法认证直接重定向,而ajax请求又不能直接重定向,导致前端302,而302vue response拦截器是拦截不到的。所以就想到不让cas给我重定向,给我返回状态码,告诉前端认证失败,让前端直接跳转cas服务器登录地址。上代码
修改cas源码过滤器,复制源码AuthenticationFilter这个过滤器,重写他,其实这里只改了重定向的代码其他都一样。
这个类复制出来把源码复制进去,修改图里面的位置,再把原来使用AuthenticationFilter的地方换成你新的类,这样认证失败就返回的是状态码前端可以拦截。
这里还是贴上我cas client的一些代码
这个是我复制源码AuthenticationFilter新建的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 package com.nascent.daren.filter; import com.alibaba.fastjson.JSON; import com.nascent.utils.R; import org.jasig.cas.client.Protocol; import org.jasig.cas.client.authentication.AuthenticationRedirectStrategy; import org.jasig.cas.client.authentication.ContainsPatternUrlPatternMatcherStrategy; import org.jasig.cas.client.authentication.DefaultAuthenticationRedirectStrategy; import org.jasig.cas.client.authentication.DefaultGatewayResolverImpl; import org.jasig.cas.client.authentication.ExactUrlPatternMatcherStrategy; import org.jasig.cas.client.authentication.GatewayResolver; import org.jasig.cas.client.authentication.RegexUrlPatternMatcherStrategy; import org.jasig.cas.client.authentication.UrlPatternMatcherStrategy; import org.jasig.cas.client.configuration.ConfigurationKeys; import org.jasig.cas.client.util.AbstractCasFilter; import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.util.ReflectUtils; import org.jasig.cas.client.validation.Assertion; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; public class DaRenAuthenticationFilter extends AbstractCasFilter { /** * The URL to the CAS Server login. */ private String casServerLoginUrl; /** * Whether to send the renew request or not. */ private boolean renew = false; /** * Whether to send the gateway request or not. */ private boolean gateway = false; private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl(); private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy(); private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null; private static final Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES = new HashMap<String, Class<? extends UrlPatternMatcherStrategy>>(); static { PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class); PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class); PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class); } public DaRenAuthenticationFilter() { this(Protocol.CAS2); } protected DaRenAuthenticationFilter(final Protocol protocol) { super(protocol); } protected void initInternal(final FilterConfig filterConfig) throws ServletException { if (!isIgnoreInitConfiguration()) { super.initInternal(filterConfig); setCasServerLoginUrl(getString(ConfigurationKeys.CAS_SERVER_LOGIN_URL)); setRenew(getBoolean(ConfigurationKeys.RENEW)); setGateway(getBoolean(ConfigurationKeys.GATEWAY)); final String ignorePattern = getString(ConfigurationKeys.IGNORE_PATTERN); final String ignoreUrlPatternType = getString(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE); if (ignorePattern != null) { final Class<? extends UrlPatternMatcherStrategy> ignoreUrlMatcherClass = PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType); if (ignoreUrlMatcherClass != null) { this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlMatcherClass.getName()); } else { try { logger.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType); this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlPatternType); } catch (final IllegalArgumentException e) { logger.error("Could not instantiate class [{}]", ignoreUrlPatternType, e); } } if (this.ignoreUrlPatternMatcherStrategyClass != null) { this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern); } } final Class<? extends GatewayResolver> gatewayStorageClass = getClass(ConfigurationKeys.GATEWAY_STORAGE_CLASS); if (gatewayStorageClass != null) { setGatewayStorage(ReflectUtils.newInstance(gatewayStorageClass)); } final Class<? extends AuthenticationRedirectStrategy> authenticationRedirectStrategyClass = getClass(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS); if (authenticationRedirectStrategyClass != null) { this.authenticationRedirectStrategy = ReflectUtils.newInstance(authenticationRedirectStrategyClass); } } } public void init() { super.init(); CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null."); } public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; if (isRequestUrlExcluded(request)) { logger.debug("Request is ignored."); filterChain.doFilter(request, response); return; } final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; if (assertion != null) { filterChain.doFilter(request, response); return; } final String serviceUrl = constructServiceUrl(request, response); final String ticket = retrieveTicketFromRequest(request); final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { filterChain.doFilter(request, response); return; } final String modifiedServiceUrl; logger.debug("no ticket and no assertion found"); if (this.gateway) { logger.debug("setting gateway attribute in session"); modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; } logger.debug("Constructed service url: {}", modifiedServiceUrl); final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); logger.debug("redirecting to \"{}\"", urlToRedirectTo); PrintWriter out = response.getWriter(); response.setContentType("application/json; charset=UTF-8"); out.println(JSON.toJSONString(R.error(401,"登陆出错"))); //this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo); } public final void setRenew(final boolean renew) { this.renew = renew; } public final void setGateway(final boolean gateway) { this.gateway = gateway; } public final void setCasServerLoginUrl(final String casServerLoginUrl) { this.casServerLoginUrl = casServerLoginUrl; } public final void setGatewayStorage(final GatewayResolver gatewayStorage) { this.gatewayStorage = gatewayStorage; } private boolean isRequestUrlExcluded(final HttpServletRequest request) { if (this.ignoreUrlPatternMatcherStrategyClass == null) { return false; } final StringBuffer urlBuffer = request.getRequestURL(); if (request.getQueryString() != null) { urlBuffer.append("?").append(request.getQueryString()); } final String requestUri = urlBuffer.toString(); return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri); } }
这个是springboot配置的cas类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 import org.jasig.cas.client.session.SingleSignOutFilter; import org.jasig.cas.client.session.SingleSignOutHttpSessionListener; import org.jasig.cas.client.util.AssertionThreadLocalFilter; import org.jasig.cas.client.util.HttpServletRequestWrapperFilter; import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Configuration @Component public class CasConfigure { /** * cas服务端地址 */ // private String casServerLoginUrl=""; private String casServerLoginUrl=""; /**lo * 当前应用地址 */ @Value("${env.serverName}") private String serverName; /** * 该监听器用于实现单点登出功能,session失效监听器 */ @Bean public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener() { ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> listener = new ServletListenerRegistrationBean<>(); listener.setEnabled(true); listener.setListener(new SingleSignOutHttpSessionListener()); listener.setOrder(1); return listener; } /** * 该过滤器用于实现单点登出功能,单点退出配置,一定要放在其他filter之前 * 当调用当前应用的/logout时,该拉截器将会重定向到cas服务端的/logout请求 */ @Bean public FilterRegistrationBean logOutFilter() { FilterRegistrationBean filterRegistration = new FilterRegistrationBean();//new SecurityContextLogoutHandler() LogoutFilter logoutFilter = new LogoutFilter(casServerLoginUrl + "/logout?service=" + serverName,new SecurityContextLogoutHandler()); filterRegistration.setFilter(logoutFilter); filterRegistration.setEnabled(true); filterRegistration.addUrlPatterns("/logout"); filterRegistration.addInitParameter("casServerUrlPrefix", casServerLoginUrl); filterRegistration.addInitParameter("serverName", serverName); filterRegistration.setOrder(2); return filterRegistration; } /** * 该过滤器用于实现单点登出功能,当一个系统登出时,cas服务端会通知,各个应 * 用进行进行退出操作,该过滤器就是用来接收cas回调的请求,如果是前后端分离 * 应用,需要重写SingleSignOutFilter过滤器,按自已的业务规则去处理 */ @Bean public FilterRegistrationBean singleSignOutFilter() { FilterRegistrationBean filterRegistration = new FilterRegistrationBean(); filterRegistration.setFilter(new SingleSignOutFilter()); filterRegistration.setEnabled(true); filterRegistration.addUrlPatterns("/*"); filterRegistration.addInitParameter("casServerUrlPrefix", casServerLoginUrl); filterRegistration.addInitParameter("serverName", serverName); filterRegistration.setOrder(3); return filterRegistration; } /** * 该过滤器负责单点登录功能,用户登录的认证工作 * @return */ @Bean public FilterRegistrationBean authenticationFilterRegistrationBean() { FilterRegistrationBean authenticationFilter = new FilterRegistrationBean(); authenticationFilter.setFilter(new DaRenAuthenticationFilter()); //这里就是被换的类 Map<String, String> initParameters = new HashMap<String, String>(); initParameters.put("casServerLoginUrl", casServerLoginUrl); initParameters.put("ignorePattern", "/api/"); initParameters.put("serverName",serverName); authenticationFilter.setInitParameters(initParameters); authenticationFilter.setOrder(4); List<String> urlPatterns = new ArrayList<String>(); urlPatterns.add("/*"); authenticationFilter.setUrlPatterns(urlPatterns); return authenticationFilter; } /** * 该过滤器用于单点登录功能,负责对Ticket的校验工作 * @return */ @Bean public FilterRegistrationBean ValidationFilterRegistrationBean(){ FilterRegistrationBean authenticationFilter = new FilterRegistrationBean(); authenticationFilter.setOrder(5); authenticationFilter.setFilter(new Cas20ProxyReceivingTicketValidationFilter()); Map<String, String> initParameters = new HashMap<>(); initParameters.put("casServerUrlPrefix", casServerLoginUrl); initParameters.put("serverName", serverName); authenticationFilter.setInitParameters(initParameters); List<String> urlPatterns = new ArrayList<String>(); urlPatterns.add("/*"); authenticationFilter.setUrlPatterns(urlPatterns); return authenticationFilter; } /** * 该过滤器用于单点登录功能 ,对HttpServletRequest请求包装, 可通过HttpServletRequest的getRemoteUser()方法获得登录用户的登录名 * @return */ @Bean public FilterRegistrationBean casHttpServletRequestWrapperFilter(){ FilterRegistrationBean authenticationFilter = new FilterRegistrationBean(); authenticationFilter.setFilter(new HttpServletRequestWrapperFilter()); authenticationFilter.setOrder(6); List<String> urlPatterns = new ArrayList<String>(); urlPatterns.add("/*"); authenticationFilter.setUrlPatterns(urlPatterns); return authenticationFilter; } /** * 该过滤器使得可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 这个类把Assertion信息放在ThreadLocal变量中,这样应用程序不在web层也能够获取到当前登录信息 * @return */ @Bean public FilterRegistrationBean casAssertionThreadLocalFilter(){ FilterRegistrationBean authenticationFilter = new FilterRegistrationBean(); authenticationFilter.setFilter(new AssertionThreadLocalFilter()); authenticationFilter.setOrder(7); List<String> urlPatterns = new ArrayList<>(); urlPatterns.add("/*"); authenticationFilter.setUrlPatterns(urlPatterns); return authenticationFilter; } }
到这里前端就可以捕捉跳转cas服务器的登录地址。 这里一定要注意前端跳转指定Cas回调地址必须是后端地址,因为cas服务器返回的票据还需要后端验证。这样session cookie才会正确。那么后台还得提供一个ValidateController里面在跳转前端服务器地址。 上代码 前端拦截器
1 2 3 4 5 6 7 8 axiosInstance.interceptors.response.use(response => { let status = response.data.code; let url = "http://cas服务登录地址/?service=回调后端控制器/daren/checkToken"; if(status ===401){ window.location.href = url; return } }
后台控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @RequestMapping("/daren") public class ValidateController { @RequestMapping("/checkToken") public void index(HttpServletRequest request, HttpServletResponse response) throws IOException { response.sendRedirect(”前端服务器地址");// } }
好了整个流程就可以走通了。
需要注意的点 1、所有前后端地址,如果是本地调试,需要统一,都是用ip那么地址都配置成ip,是localhost就都是localhost 2、重要点还是在改掉原来的cas重定向的逻辑。理解了就知道怎么弄了。