之前我在项目中使用过CAS作为单点登录服务,不过那些项目,不管是asp.net MVC项目,还是java的spring boot项目,都是前后端不分的,只须使用CAS的客户端(对于asp.net mvc项目来说,cas的客户端就是DotNetCasClient.dll,而java项目,cas客户端就是一些java包),然后配置一下就好了。现在普遍前后端分离,那该如何集成CAS呢?
一、工作原理及流程
前后端分离项目,集成CAS,简单而言,就是:前端不需要关心单点登录,真正处理单点登录的是后端。前端的数据,以及对数据的操作,都源于后端。前端不需要考察自己的登录状态(其实登录的是浏览器),只需访问后端,后端自然会处理,前端仅仅要做的,就是收到后端返回202编码时,跳转到单点登录页面。这应该是一种代理模式。后端是前端的单点登录代理。具体流程如下:
前端向后端请求或提交数据,后端发现前端尚未登录,于是返回一个202状态码;前端接收到此状态码后,转向单点登录,并在地址后面以参数的形式,附上登录后跳转的地址,这个地址是后端一个接口地址;于是登录成功后,跳转到后端这个接口,在这个接口里面,跳转到前端登录返回后页面。
下面是详细说明。
二、运行环境
我的前后端分离项目,后端是Spring Boot程序,以此为例做说明。
单点登录服务:CAS 5.18
CAS客户端:cas-client-core 3.3.2
后端框架:Spring Boot2
前端框架:VUE 3
三、后端
后端主要是设置一个过滤器,对来自前端的请求进行是否已登录检查。过滤器基本来自于CAS客户端。但这个客户端需要稍为改写。原因据说是CAS无法将来自前端的AJAX请求直接重定向到单点登录服务器,所以改为向前端返回202状态码,然后前端接收到此状态码后,自行重定向到单点登录。这个我目前还不是很理解,需要进一步了解的同学可自行查阅附录参考文章。
1、pom.xml
<!-- 单点登录相关 -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
</dependency>
2、application.yml
cas:
casSigntouServerUrlPrefix: https://192.168.10.250:11443/cas250/logout
casServerLoginUrl: https://192.168.10.250:11443/cas250/login
casValidationServerUrlPrefix: https://192.168.10.250:11443/cas250
clientBackendUrl: http://192.168.10.8:8090 #后端地址
clientWebUrl: http://192.168.10.8:8080/#/afterLogin #前端地址
3、过滤器1,禁用ssl认证
不知为啥要这么做,照抄参考文章。反正我的前端和后端都不支持https。在实际应用过程中,我发现前后端分离的项目,如果后端为https,则有问题,似乎后端没有认证成功。可能跟这里的改写有关。是不是不实施这个过滤器就能支持https?不知道。
import javax.net.ssl.*;
import javax.servlet.*;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class IgnoreSSLValidateFilter implements Filter {
static {
//执行设置,禁用ssl认证
try {
TrustManager[] trustAllCerts = {new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
}};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HostnameVerifier allHostsValid = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
}
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
4、过滤器2,改写重定向为返回202状态码
其实就是String xRequested =request.getHeader(“x-requested-with”); if(“XMLHttpRequest”.equals(xRequested){…}这里做了更改。不明觉厉。
import org.jasig.cas.client.authentication.*;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.FilterConfig;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyAuthenticationFilter extends AbstractCasFilter {
private String casServerLoginUrl;
private boolean renew = false;
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();
private static final Logger LOGGER = LoggerFactory.getLogger(MyAuthenticationFilter.class);
public MyAuthenticationFilter() {
}
@Override
protected void initInternal(FilterConfig filterConfig) throws ServletException {
if (!this.isIgnoreInitConfiguration()) {
super.initInternal(filterConfig);
this.setCasServerLoginUrl(this.getPropertyFromInitParams(filterConfig, "casServerLoginUrl", (String)null));
LOGGER.trace("Loaded CasServerLoginUrl parameter: {}", this.casServerLoginUrl);
this.setRenew(this.parseBoolean(this.getPropertyFromInitParams(filterConfig, "renew", "false")));
LOGGER.trace("Loaded renew parameter: {}", this.renew);
this.setGateway(this.parseBoolean(this.getPropertyFromInitParams(filterConfig, "gateway", "false")));
LOGGER.trace("Loaded gateway parameter: {}", this.gateway);
String ignorePattern = this.getPropertyFromInitParams(filterConfig, "ignorePattern", (String)null);
LOGGER.trace("Loaded ignorePattern parameter: {}", ignorePattern);
String ignoreUrlPatternType = this.getPropertyFromInitParams(filterConfig, "ignoreUrlPatternType", "REGEX");
LOGGER.trace("Loaded ignoreUrlPatternType parameter: {}", ignoreUrlPatternType);
if (ignorePattern != null) {
Class<? extends UrlPatternMatcherStrategy> ignoreUrlMatcherClass = (Class)PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);
if (ignoreUrlMatcherClass != null) {
this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy) ReflectUtils.newInstance(ignoreUrlMatcherClass.getName(), new Object[0]);
} else {
try {
LOGGER.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType);
this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy)ReflectUtils.newInstance(ignoreUrlPatternType, new Object[0]);
} catch (IllegalArgumentException var6) {
LOGGER.error("Could not instantiate class [{}]", ignoreUrlPatternType, var6);
}
}
if (this.ignoreUrlPatternMatcherStrategyClass != null) {
this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);
}
}
String gatewayStorageClass = this.getPropertyFromInitParams(filterConfig, "gatewayStorageClass", (String)null);
if (gatewayStorageClass != null) {
this.gatewayStorage = (GatewayResolver)ReflectUtils.newInstance(gatewayStorageClass, new Object[0]);
}
String authenticationRedirectStrategyClass = this.getPropertyFromInitParams(filterConfig, "authenticationRedirectStrategyClass", (String)null);
if (authenticationRedirectStrategyClass != null) {
this.authenticationRedirectStrategy = (AuthenticationRedirectStrategy)ReflectUtils.newInstance(authenticationRedirectStrategyClass, new Object[0]);
}
}
}
@Override
public void init() {
super.init();
CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
}
@Override
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if (this.isRequestUrlExcluded(request)) {
LOGGER.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
LOGGER.debug("no ticket and no assertion found");
String modifiedServiceUrl;
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);
String xRequested =request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequested)){
response.getWriter().write("{\"code\":202, \"msg\":\"no ticket and no assertion found\"}");
}else{
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
LOGGER.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
public final void setRenew(boolean renew) {
this.renew = renew;
}
public final void setGateway(boolean gateway) {
this.gateway = gateway;
}
public final void setCasServerLoginUrl(String casServerLoginUrl) {
this.casServerLoginUrl = casServerLoginUrl;
}
public final void setGatewayStorage(GatewayResolver gatewayStorage) {
this.gatewayStorage = gatewayStorage;
}
private boolean isRequestUrlExcluded(HttpServletRequest request) {
if (this.ignoreUrlPatternMatcherStrategyClass == null) {
return false;
} else {
StringBuffer urlBuffer = request.getRequestURL();
if (request.getQueryString() != null) {
urlBuffer.append("?").append(request.getQueryString());
}
String requestUri = urlBuffer.toString();
return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
}
}
static {
PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class);
PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class);
PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class);
}
}
5、注册配置
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
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.core.annotation.Order;
import org.springframework.web.filter.CharacterEncodingFilter;
import javax.servlet.Filter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class SsoFilterConfig implements Serializable, InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(SsoFilterConfig.class);
public static final String CAS_SIGNOUT_FILTER_NAME = "CAS Single Sign Out Filter";
public static final String CAS_AUTH_FILTER_NAME = "CAS Filter";
public static final String CAS_IGNOREL_SSL_FILTER_NAME = "CAS Ignore SSL Filter";
public static final String CAS_FILTER_NAME = "CAS Validation Filter";
public static final String CAS_WRAPPER_NAME = "CAS HttpServletRequest Wrapper Filter";
public static final String CAS_ASSERTION_NAME = "CAS Assertion Thread Local Filter";
public static final String CHARACTER_ENCODING_NAME = "Character encoding Filter";
//CAS服务器退出地址
@Value("${cas.casSigntouServerUrlPrefix:https://127.0.0.1:8443/cas/logout}")
String casSigntouServerUrlPrefix;
//CAS服务器登录地址
@Value("${cas.casServerLoginUrl:https://127.0.0.1:8443/cas/login}")
String casServerLoginUrl;
//CAS服务器地址
@Value("${cas.casValidationServerUrlPrefix:https://127.0.0.1:8443/cas}")
String casValidationServerUrlPrefix;
//客户端地址(即本系统地址?)
@Value("${cas.clientBackendUrl:http://127.0.0.1:1234}")
String clientBackendUrl;
public SsoFilterConfig() {
}
/**
* 单点登出功能,放在其他filter之前
* casSigntouServerUrlPrefix为登出前缀:https://123.207.122.156:8081/cas/logout
*
* @return
*/
@Bean
@Order(0)
public FilterRegistrationBean getCasSignoutFilterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(getCasSignoutFilter());
registration.addUrlPatterns("/*", "*.html");
registration.addInitParameter("casServerUrlPrefix", casSigntouServerUrlPrefix);
registration.setName(CAS_SIGNOUT_FILTER_NAME);
registration.setEnabled(true);
return registration;
}
@Bean(name = CAS_SIGNOUT_FILTER_NAME)
public Filter getCasSignoutFilter() {
return new SingleSignOutFilter();
}
/**
* 忽略SSL认证
*
* @return
*/
@Bean
@Order(1)
public FilterRegistrationBean getCasSkipSSLValidationFilterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(getCasSkipSSLValidationFilter());
registration.addUrlPatterns("/*", "*.html");
registration.setName(CAS_IGNOREL_SSL_FILTER_NAME);
registration.setEnabled(true);
return registration;
}
@Bean(name = CAS_IGNOREL_SSL_FILTER_NAME)
public Filter getCasSkipSSLValidationFilter() {
return new IgnoreSSLValidateFilter();
}
/**
* 负责用户的认证
* casServerLoginUrl:https://123.207.122.156:8081/cas/login
* casServerName:https://123.207.122.156:8080/tdw/alerts/
*
* @return
*/
@Bean
@Order(2)
public FilterRegistrationBean getCasAuthFilterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
final Filter casAuthFilter = getCasAuthFilter();
registration.setFilter(casAuthFilter);
registration.addUrlPatterns("/*", "*.html");
registration.addInitParameter("casServerLoginUrl", casServerLoginUrl);
registration.addInitParameter("serverName", clientBackendUrl);
registration.setName(CAS_AUTH_FILTER_NAME);
registration.setEnabled(true);
return registration;
}
@Bean(name = CAS_AUTH_FILTER_NAME)
public Filter getCasAuthFilter() {
return new MyAuthenticationFilter();
}
/**
* 对Ticket进行校验
* casValidationServerUrlPrefix要用内网ip
* casValidationServerUrlPrefix:https://123.207.122.156:8081/cas
* casServerName:https://123.207.122.156:8080/tdw/alerts/
*
* @return
*/
@Bean
@Order(3)
public FilterRegistrationBean getCasValidationFilterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
final Filter casValidationFilter = getCasValidationFilter();
registration.setFilter(casValidationFilter);
registration.addUrlPatterns("/*", "*.html");
registration.addInitParameter("casServerUrlPrefix", casValidationServerUrlPrefix);
registration.addInitParameter("serverName", clientBackendUrl);
registration.setName(CAS_FILTER_NAME);
registration.setEnabled(true);
return registration;
}
@Bean(name = CAS_FILTER_NAME)
public Filter getCasValidationFilter() {
return new Cas20ProxyReceivingTicketValidationFilter();
}
/**
* 设置response的默认编码方式:UTF-8。
*
* @return
*/
@Bean
@Order(4)
public FilterRegistrationBean getCharacterEncodingFilterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(getCharacterEncodingFilter());
registration.addUrlPatterns("/*", "*.html");
registration.setName(CHARACTER_ENCODING_NAME);
registration.setEnabled(true);
return registration;
}
@Bean(name = CHARACTER_ENCODING_NAME)
public Filter getCharacterEncodingFilter() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
return characterEncodingFilter;
}
@Bean
public FilterRegistrationBean casHttpServletRequestWrapperFilter(){
FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();
authenticationFilter.setFilter(new HttpServletRequestWrapperFilter());
authenticationFilter.setOrder(6);
List<String> urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
authenticationFilter.setUrlPatterns(urlPatterns);
return authenticationFilter;
}
@Override
public void afterPropertiesSet() throws Exception {
}
}
6、给前端和CAS的相关接口
//import 自定义的 巴拉巴拉.server.modules.utils.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Api(tags = "cas单点登录接口")
@Controller
@RequestMapping("/cas")
public class LoginController {
@Value("${cas.clientWebUrl}")
String webUrl;
@ApiOperation(value = "测试接口")
@GetMapping("/test")
@ResponseBody
public String login(HttpServletRequest request) {
System.out.println(request.getRemoteUser().toString());
return "success";
}
@ApiOperation(value = "票根验证", notes = "验证通过时转发到前端主页")
@GetMapping("/checkTicket")
public void index(HttpServletResponse response) throws IOException {
// 前端页面地址
response.sendRedirect(webUrl);
}
@ApiOperation(value = "获取系统当前登录的用户名与用户名白名单")
@GetMapping("/getUsername")
@ResponseBody
public Result getUsername(HttpServletRequest request) {
Map<String, String> map = new HashMap<>();
map.put("currentUser", request.getRemoteUser());
return Result.ok().put("data",map);
}
}
四、前端
前端不需要进行登录状态检查。它只在收到后端返回的202状态码时才主动重定向到单点登录。除此之外,它提供了一个登录后返回页面,记录一下登录账号,并凭此到后端获取该账号的详细信息。当然专门设置一个返回页面不是必须的。可见,由始至终,前端并没有校验登录状态,也没有存储什么登录状态,它是否登录,以及是否需要登录,都是由后端控制的。在它向后端请求数据、或提交数据时,由后端的过滤器处理。
前端涉及到处理的模块主要有:
1)请求处理程序
2)路由卫士
3)登录后返回页面
1、请求处理程序request/index.js
import axios from "axios";
let redirectFlag = false;
const onResponse = (response) => {
const code = response.data.code;
if (code === 202 && !redirectFlag) {//转向单点登录
redirectFlag = true;
window.location.href = appConfig.cas.casRedirectUrl;
} else {
。。。
}
};
。。。
const getService = (config) => {
const service = axios.create(config);
// 添加请求拦截器
service.interceptors.request.use(onRequest, onRequestError);
// 添加响应拦截器
service.interceptors.response.use(onResponse, onResponseError);
return service;
};
const DEFAULTCONFIG = {
baseURL: "/api", // 所有的请求地址前缀部分
timeout: 60000, // 请求超时时间毫秒
withCredentials: true, // 异步请求携带cookie
headers: {
"X-Requested-With": "XMLHttpRequest",
},
};
const service = getService(DEFAULTCONFIG);
export default service;
2、路由卫士router/index.js
import { createRouter, createWebHashHistory } from "vue-router";
const router = createRouter({
history: createWebHashHistory(),
routes,//一些路由规则
});
// 路由守卫
const normalRouter = (to, from, next) => {
if (to.path === "/afterLogin") {//必须保证登录返回页面顺利加载
next();
} else {
//这部分代码与结合单点登录无关,由前端项目按照自己的逻辑自行定义
myRouterDo(to, from, next);
}
};
router.beforeEach(normalRouter);
export default router;
//自定义逻辑
function myRouterDo(to, from, next) {
。。。
}
3、登录后返回页面
以下代码中,getUserNameCAS()向后端获取登录账号;而在loginDo方法中,我会凭登录后账号向后端请求该账号的详细信息,做一些。
1)页面afterLogin.vue
<template>
<div>
<h1>===== 欢迎使用VUE3 =====</h1>
</div>
<div v-if="state.userName === null">
<span>未登录</span>
</div>
<div v-if="state.userName !== null">
<div class="flex-box navis">
<div>框架首页</div>
<div>
<el-link type="primary" underline="true" href="/#/business">业务首页</el-link>
</div>
</div>
<div class="flex-box user-info">
<div><span>用户名:</span><span>{{ state.userName }}</span></div>
<div>
<el-button type="primary" @click="logout" class="green-button">登出</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted, nextTick } from "vue";
import { getUserName as getUserNameApi } from "@/api/cas.js";
import { logout } from "@/utils/auth.js";
const state = reactive({
userName: null
});
onMounted(() => {
nextTick(() => {
getUserInfo();
});
});
function getUserInfo() {
getUserNameApi().then((res) => {
state.userName = res.data.currentUser;
});
}
</script>
<style scoped>
</style>
2)后端接口api/cas.js
import { request } from "@/request";
const appConfig = require("/public/web-config.json");
const $cas = appConfig.cas;
//目前没有用到
export const login = () => {
return request({
url: `/cas/checkTicket`,
method: "get",
});
};
export const logout = () => {
window.location.href = $cas.casLogout;
};
export const getUserName = () => {
return request({
url: `/cas/getUsername`,
method: "get",
});
};
五、小结
从流程看,跳转到单点登录是前端的主动行为,似乎后端并没有参与单点登录,它是如何代理前端的登录状态事宜的?
首先要厘清的是,所谓的登录,既不是前端登录,也不是后端登录,其实是浏览器登录,或者说,登录的载体是浏览器。其次,所谓的代理前端的登录状态事宜,即是后端考察浏览器的登录状态。当浏览器访问前端,转而访问后端,每一个请求,后端都会向CAS求证,它登录了吗?合法吗?后端是CAS客户端,单点登录后也是先返回后端,这一过程中,后端与CAS之间,早已暗通款曲,建立了秘密联系的通道。因此后端的确可以担负起考察浏览器登录状态的重任。
CAS作为单点登录服务,应该是很成熟的吧,用起来,只要配置得当,还是很丝滑的。不过,我印象中,CAS部署起来十分麻烦。调试也很麻烦,以前经常遇到重定向次数过多的问题。如果这次不是有现成的部署,我不一定会选用它来做单点服务器。
实际运行过程中,发现如果要集成这个CAS的话,前端和后端必须是同一台机器,否则CAS不认这个登录结果,前端页面于是在不停地狂闪。也不知道是在前端和CAS之间、还是前端和后端之间狂闪,抑或是三者之间踢皮球。猜测是最后第一种,登录成功以后,前端向后端请求数据,后端向CAS求证失败,于是指示前端跳转到CAS,来到CAS后,CAS发现来者已经登录,于是跳回前端,于是新一轮轮回开始了。猜测这其中涉及到cookie。因为我用的是IP作为前后端的地址,对于cookie来说,IP相同,端口不同,仍然算是同域;如果IP不同就不行了。所以如果前后端真的要分开部署在不同机器的话,恐怕只能用域名了。
另外,前后端项目部署于容器,比如docker,似乎也不行。没有时间确认,先这样吧。
六、单点登录与oAuth2
似乎单点登录与oAuth2有许多相同之处。oAuth2,使用第三方系统作为身份识别和授权服务,主要是授权。比如,我们使用微信来授权登录。当用户访问我们系统A时,首先转到微信,微信会询问用户,你同意系统A获取你的微信身份信息吗?用户同意后,微信就将用户的一些身份信息返回到我们系统A,我们就拿到了用户的身份,然后可以认为他登录了。从无须打造自己的登录服务这点来看,单点登录和oAuth2的效果是一样的。
但是,oAuth2并没有所谓一处登录,处处可使用的效果。每个使用微信的应用都是独立的,你登录了系统A,并不能直接访问系统B。同时,单点登录有单点登出,即从一个应用登出以后,其他的应用也都登出了;而oAuth2显然也没有这种功能。
我的感觉是,局域网应用,适合使用单点登录;互联网,适合使用oAuth2。
附录:参考资料
后端代码基本上抄自以下文章。