前言
还记得那是一个惺忪的早晨,带着困意的我正准备去公司上班,坐车坐到一半,钉钉突然袭来一条红色预警信息,公司查出来有个人阳了,好消息:万幸我还在去公司的路上,没有和他有过多的接触,坏消息:那个人的工位离我不过3米远,内心os:我靠,没办法这种情况也只能打道回府了,开始了为期快一个月的居家办公生活,即使居家了也没能阻止我成为小洋人,万幸症状不严重,熬过了那段时间之后,慢慢的也恢复从前的生活了。
pc端接入单点登录
本人由于对接的是pc端的浙里办登录,先说下思路吧,先去申请一个 IRS 组件,之后去找浙里办那边的技术人员让其通过一下,然后他们会给我们一个 appid ,看到这个 appid 就知道他们单点登录是通过 Oauth2 协议实现的了,然后我们携带好 appid 去访问他们提供的单点统一登录的地址就行,https://portal.zjzwfw.gov.cn/uc/sso/login?appId=xxxxx,注意这是对接的 pc 端,这个地址只能通过浏览器地址栏输入的形式进行访问,然后浙里办收到单点登录请求之后,会返回一个 ticketId 请求重定向到我们系统,至于票据 ticketId 的接收,我们自己系统可以通过接口接收、或者通过一个页面接收,由于对接的是 pc 端,我们这里采用页面接收,页面接收到 ticketId 后,拿着 ticketId 去请求我们自己的接口,接口通过 ticketId 去请求 token 返回 token 给我们的页面,获取 token 成功后,然后通过 token 去获取用户信息,如果该浙里办用户绑定过我们系统的账号,最终才能进入系统,否则跳转到账号绑定页面。
实现流程大概就是上文描述的这样了,接下来说一下大家对单点登录理解起来比较绕的点吧
- 回调地址是什么东西?系统 a 发起一个请求到系统 b,系统 b 收到请求后,将请求重定向回系统 a ,这个重定向地址就是回调地址。该怎么配置回调地址呢?找一台公网可访问的服务器,将服务部署到上面,这个回调地址可以是一个接口,也可以是一个公网可访问的页面,然后把对应的访问地址告知系统 b 即可。这样当系统 a 访问 b 系统的统一登录地址时,b 系统就知道将票据 回调给哪个地址了。拿到了票据后,就可以进行接下来的一系列操作了。浙里办是分公网、政务云环境的,各位根据自己系统所在的环境来就行,总之就是要让系统 b 可以访问到系统 a 的那个回调地址。
- 回调地址配页面还是接口? 拿浙里办pc端的单点登录来说,由于pc端浙里办登录的口子,只能通过浏览器输入url访问,如果想要实现用户登录了浙里办,再次访问我们系统,免登就可以进入系统的效果,那么这种情况下我们系统原有的登录页是要舍弃的。原因很简单,前端如果采用发请求的方式去请求浙里办统一登录页面,当用户没有登录的时候,浙里办那边那个请求已经被重定向到浙里办登录页了,但是此时前端 api 的限制,无法进行重定向,只能干瞪眼看着一个 302 状态码,却无法进行重定向的操作哦。回调接收页面的逻辑很简单,就是在加载页面的时候重定向到浙里办的登录页即可,这样一来所有的系统登录页都统一了,也就实现了单个系统登录,其他系统无需登录的效果了。
- 如果pc端即想要保留自己的用户体系登录页、也想接入浙里办单点登录,可以在原有的登录页上面加一个浙里办登录的口子,口子 url 填写 https://portal.zjzwfw.gov.cn/uc/sso/login?appId=xxxxx 这个地址就行,供用户选择登录方式,如果用户登录过浙里办,点击我们系统的浙里办登录口子就直接跳转进入系统了,没有登录过,会走 Auth2 认证授权那一套登录逻辑。不了解 Oauth2 的小伙伴可以移步,结合源码剖析Oauth2分布式认证与授权的实现流程
实战模拟加深理解
系统a:端口号 8082,接下来的内容意在编写伪代码,模拟上述过程加深理解,系统 a 回调页面模拟代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>单点登录</title>
</head>
<body>
中间页
<script>
window.onload = function load() {
const searchParams = new URLSearchParams(window.location.search);
const ticketId = searchParams.get("ticketId"); // 从url参数获取ticketId
//地址栏有票据
if (ticketId) {
//根据票据获取token
fetch(`/auth/getTokenByTicketId?ticketId=` + ticketId, {
method: 'post'
}).then(response => {
return response.text();
}).then(result => {
//根据token获取用户信息
fetch(`/auth/getUserInfoByToken?token=`+result, {
method: 'post'
}).then(response => {
return response.json();
}).then(result => {
console.log('user info==', result);
})
})
}
//地址栏无票据,跳转至统一登录页
else {
//直接重定向至统一登录页
//window.location.href = "http://localhost:8080/auth/zw/appid=111";
//模拟通过发起请求,去访问统一登录页
fetch(`http://localhost:8080/auth/zw/appid=111`, {
method: 'get'
}).then(response => {
return response.text();
}).then(result => {
console.log(result);
})
}
}
</script>
</body>
</html>
系统 a 获取 token、用户信息接口:
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/getTokenByTicketId")
public String dologin(@RequestParam("ticketId") String ticketId) {
if (!"zzh".equals(ticketId)) return "通过此ticketId获取token失败,无效ticketId";
log.info("-------发起请求调用浙里办获取token接口 api 获取到的 token为:token-----");
return "token";
}
/**
* 获取用户信息
*/
@PostMapping("/getUserInfoByToken")
public String userInfo(@RequestParam("token") String token) {
log.info("-----调用浙里办获取用户信息接口,获取用户信息-----");
if (token != null && "token".equals(token)) {
return "通过该token成功获取到用户信息。";
} else {
return "无效token";
}
}
}
浙里办后端模拟代码
很简单的一个模拟,主要分二种情况
- 用户登录过浙里办,携带票据重定向到系统 a 的回调页(系统 a 的回调接口)上面
- 用户没有登录浙里办,跳转到浙里办登录页。
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthController {
/**
* 浙里办模拟重定向到回调地址,并且附ticketId
*/
@CrossOrigin
@GetMapping("/zw/{appid}")
public void zw(@PathVariable("appid") String appid, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.err.println("-------appid-------" + appid);
response.setHeader("Access-Control-Allow-Origin", "*");
//重定向到系统 a 配置的回调页面
//response.sendRedirect("http://localhost:8082/mid.html?ticketId=zzh");
//重定向到系统 a 配置的回调接口上
//response.sendRedirect("http://localhost:8082/auth/test/569a1529886e47f8a5f807ab3133b541");
//重定向到浙里办登录页
response.sendRedirect("http://localhost:8080/auth/index");
}
@CrossOrigin
@GetMapping("/index")
public String d(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return "浙里办登录页";
}
}
模拟-前端通过发起请求的方式去访问统一登录页
可以看到我们的请求得到的反馈是 302(请求重定向),此时的前端只能得到重定向页面的整个页面源码,无法手动重定向!!!!!!,因此 pc 端去通过发起请求的方式去访问浙里办统一登录页,无法手动跟随重定向,这也是为什么 pc 端只能用浏览器输入url去访问统一登录页面的原因!!!!!
模拟-前端通过浏览器输入url方式访问统一登录页
修改回调页面代码再次进行测试
可以看到重定向成功
模拟-带票据访问回调页
可以看到成功获取到用户信息,之后的接入大家的业务逻辑就可以了。
总结
pc 端统一登录的口子,切记使用浏览器输入 url 的方式去访问,不然对于返回的 302 状态码,前端无法手动跟随重定向,如果你此时对接的单点登录,他们提供了工具类并有方法去检测用户是否登录,登录了则返回票据,未登录返回一个状态码啥的,此时前端就无需考虑这么多了,后端就写本文的那俩个接口就行,前端的逻辑无非就是通过提供的工具类,当用户访问系统 a 的时候,去检测用户是否已经登录,未登录跳转到统一登录页,登录过拿到票据去请求本系统中的接口走如下流程即可。