OAuth2.0协议入门(三):OAuth2.0授权与单点登录(SSO)的区别以及单点登录服务端从设计到实现
一 OAuth2.0授权与单点登录(SSO)的区别
在前两篇文章中我介绍了OAuth2.0协议的基本概念(www.zifangsky.cn/1309.html)以及OAuth2.0授权服务端从设计到实现(www.zifangsky.cn/1313.html)。这篇文章中我将介绍OAuth2.0授权与单点登录的区别,这两个概念看似很相似,实际上却有很大区别,而很多人往往把二者混为一谈。
什么是单点登录?
单点登录的英文名是 Single Sign On,因此一般简称为SSO。它的用途在于,不管多么复杂的应用群,只要在用户权限范围内,那么就可以做到,用户只需要登录一次就可以访问权限范围内的所有应用子系统。对于用户而言,访问多个应用子系统只需要登录一次,同样在需要注销的时候也只需要注销一次。举个简单的例子,你在百度首页登录成功之后,你再访问百度百科、百度知道、百度贴吧等网站也会处于登录状态了,这就是一个单点登录的真实案例。
OAuth2.0授权与单点登录的区别
根据OAuth2.0授权与单点登录的概念,我们可以得知二者至少存在以下几点区别:
- 从信任角度来看。OAuth2.0授权服务端和第三方客户端不属于一个互相信任的应用群(通常都不是同一个公司提供的服务),第三方客户端的用户不属于OAuth2.0授权服务端的官方用户;而单点登录的服务端和接入的客户端都在一个互相信任的应用群(通常是同一个公司提供的服务),各个子系统的用户属于单点登录服务端的官方用户。
- 从资源角度来看。OAuth2.0授权主要是让用户自行决定——“我”在OAuth2.0服务提供方的个人资源是否允许第三方应用访问;而单点登录的资源都在客户端这边,单点登录的服务端主要用于登录,以及管理用户在各个子系统的权限信息。
- 从流程角度来看。OAuth2.0授权的时候,第三方客户端需要拿预先“商量”好的密码去获取Access Token;而单点登录则不需要。
二 单点登录服务端的设计
对于一个接入单点登录的子系统而言,进行单点登录需要以下两个步骤:
- client请求单点登录服务端,获取Access Token;
- client因为不能判断给它的Access Token是单点登录服务端返回还是用户伪造,所以需要再次请求单点登录服务端,校验Access Token是否有效,如果有效则返回用户基本信息以及相应的用户在client上所属的角色、权限等信息。
因此,单点登录服务端的设计主要围绕这两个接口展开,其主要流程是这样的:
数据库的表结构设计
提示:我在下面只介绍一些表的主要字段,这个Demo中使用的完整的表结构可以参考:gitee.com/zifangsky/O…
(1)sso_client_details:
接入单点登录的子系统详情表。类似于百度的百度百科、百度知道、百度贴吧等应用子系统,每个想要接入单点登录的子系统都需要事先在服务端这里“备案”。一方面是为了防止用户在服务端登录成功生成的Access Token
被重定向到非法网站,从而导致用户的Access Token
被窃取;另一方面是记录接入的子系统的注销URL,便于开发单点注销功能。所以主要需要以下几个字段:
client_name
:子系统的名称redirect_url
:获取Access Token
成功后的回调URLlogout_url
:用户在子系统的注销URL(用户登录状态可以分为:全局登录——在单点登录服务端的登录状态;局部登录——在子系统的登录状态,注销的时候需要同时注销用户在单点登录服务端和应用子系统的登录状态)
(2)sso_access_token:
单点登录的Access Token信息表。这个表主要体现出哪个用户在哪个子系统登录,以及生成的令牌的结束日期是哪天。所以主要需要以下几个字段:
access_token
:Access Token字段user_id
:表明是哪个用户登录client_id
:表明是在哪个子系统登录expires_in
:过期时间戳,表明这个Token在哪一天过期
(3)sso_refresh_token:
单点登录的Refresh Token信息表。这个表主要用来记录Refresh Token
,在设计表结构的时候需要关联它对应的sso_access_token
表的记录。所以主要需要以下几个字段:
refresh_token
:Refresh Token字段token_id
:它对应的sso_access_token
表的记录expires_in
:过期时间戳
三 单点登录服务端主要接口的代码实现
这个Demo的单点登录服务端的完整可用源码可以参考:gitee.com/zifangsky/O…
(1)获取Access Token:
client请求单点登录服务端,获取Access Token,获取完成之后重定向到请求中的回调URL。
接口地址:http://127.0.0.1:7000/sso/token?redirect_uri=http://192.168.197.130:6080/login
/**
* 获取Token
* @author zifangsky
* @date 2018/8/30 16:30
* @since 1.0.0
* @param request HttpServletRequest
* @return org.springframework.web.servlet.ModelAndView
*/
@RequestMapping("/token")
public ModelAndView authorize(HttpServletRequest request){
HttpSession session = request.getSession();
User user = (User) session.getAttribute(Constants.SESSION_USER);
//过期时间
Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
//回调URL
String redirectUri = request.getParameter("redirect_uri");
//查询接入客户端
SsoClientDetails ssoClientDetails = ssoService.selectByRedirectUrl(redirectUri);
//获取用户IP
String requestIp = SpringContextUtils.getRequestIp(request);
//生成Access Token
String accessTokenStr = ssoService.createAccessToken(user, expiresIn, requestIp, ssoClientDetails);
//查询已经插入到数据库的Access Token
SsoAccessToken ssoAccessToken = ssoService.selectByAccessToken(accessTokenStr);
//生成Refresh Token
String refreshTokenStr = ssoService.createRefreshToken(user, ssoAccessToken);
logger.info(MessageFormat.format("单点登录获取Token:username:【{0}】,channel:【{1}】,Access Token:【{2}】,Refresh Token:【{3}】"
,user.getUsername(),ssoClientDetails.getClientName(),accessTokenStr,refreshTokenStr));
String params = "?code=" + accessTokenStr;
return new ModelAndView("redirect:" + redirectUri + params);
}
相应地,调用的cn/zifangsky/service/impl/SsoServiceImpl.java
类里面的生成逻辑:
@Override
public String createAccessToken(User user, Long expiresIn, String requestIP, SsoClientDetails ssoClientDetails) {
Date current = new Date();
//过期的时间戳
Long expiresAt = DateUtils.nextDaysSecond(ExpireEnum.ACCESS_TOKEN.getTime(), null);
//1. 拼装待加密字符串(username + 渠道CODE + 当前精确到毫秒的时间戳)
String str = user.getUsername() + ssoClientDetails.getClientName() + String.valueOf(DateUtils.currentTimeMillis());
//2. SHA1加密
String accessTokenStr = "11." + EncryptUtils.sha1Hex(str) + "." + expiresIn + "." + expiresAt;
//3. 保存Access Token
SsoAccessToken savedAccessToken = ssoAccessTokenMapper.selectByUserIdAndClientId(user.getId(), ssoClientDetails.getId());
//如果存在匹配的记录,则更新原记录,否则向数据库中插入新记录
if(savedAccessToken != null){
savedAccessToken.setAccessToken(accessTokenStr);
savedAccessToken.setExpiresIn(expiresAt);
savedAccessToken.setUpdateUser(user.getId());
savedAccessToken.setUpdateTime(current);
ssoAccessTokenMapper.updateByPrimaryKeySelective(savedAccessToken);
}else{
savedAccessToken = new SsoAccessToken();
savedAccessToken.setAccessToken(accessTokenStr);
savedAccessToken.setUserId(user.getId());
savedAccessToken.setUserName(user.getUsername());
savedAccessToken.setIp(requestIP);
savedAccessToken.setClientId(ssoClientDetails.getId());
savedAccessToken.setChannel(ssoClientDetails.getClientName());
savedAccessToken.setExpiresIn(expiresAt);
savedAccessToken.setCreateUser(user.getId());
savedAccessToken.setUpdateUser(user.getId());
savedAccessToken.setCreateTime(current);
savedAccessToken.setUpdateTime(current);
ssoAccessTokenMapper.insertSelective(savedAccessToken);
}
//4. 返回Access Token
return accessTokenStr;
}
@Override
public String createRefreshToken(User user, SsoAccessToken ssoAccessToken) {
Date current = new Date();
//过期时间
Long expiresIn = DateUtils.dayToSecond(ExpireEnum.REFRESH_TOKEN.getTime());
//过期的时间戳
Long expiresAt = DateUtils.nextDaysSecond(ExpireEnum.REFRESH_TOKEN.getTime(), null);
//1. 拼装待加密字符串(username + accessToken + 当前精确到毫秒的时间戳)
String str = user.getUsername() + ssoAccessToken.getAccessToken() + String.valueOf(DateUtils.currentTimeMillis());
//2. SHA1加密
String refreshTokenStr = "12." + EncryptUtils.sha1Hex(str) + "." + expiresIn + "." + expiresAt;
//3. 保存Refresh Token
SsoRefreshToken savedRefreshToken = ssoRefreshTokenMapper.selectByTokenId(ssoAccessToken.getId());
//如果存在tokenId匹配的记录,则更新原记录,否则向数据库中插入新记录
if(savedRefreshToken != null){
savedRefreshToken.setRefreshToken(refreshTokenStr);
savedRefreshToken.setExpiresIn(expiresAt);
savedRefreshToken.setUpdateUser(user.getId());
savedRefreshToken.setUpdateTime(current);
ssoRefreshTokenMapper.updateByPrimaryKeySelective(savedRefreshToken);
}else{
savedRefreshToken = new SsoRefreshToken();
savedRefreshToken.setTokenId(ssoAccessToken.getId());
savedRefreshToken.setRefreshToken(refreshTokenStr);
savedRefreshToken.setExpiresIn(expiresAt);
savedRefreshToken.setCreateUser(user.getId());
savedRefreshToken.setUpdateUser(user.getId());
savedRefreshToken.setCreateTime(current);
savedRefreshToken.setUpdateTime(current);
ssoRefreshTokenMapper.insertSelective(savedRefreshToken);
}
//4. 返回Refresh Tokens
return refreshTokenStr;
}
(2)校验Access Token,并返回用户信息:
client在获取到Access Token后,再次调用单点登录服务端接口,用于“校验Access Token,并返回用户信息”。
接口地址:http://127.0.0.1:7000/sso/verify?access_token=11.ad51132688b5be3f476592356c78aef71d235f07.2592000.1539143183
返回如下:
{
"access_token": "11.ad51132688b5be3f476592356c78aef71d235f07.2592000.1539143183",
"refresh_token": "12.c10cb9001bf0e2c7f808580318715fc089673279.31536000.1568087183",
"expires_in": 2592000,
"user_info": {
"id": 2,
"username": "zifangsky",
"password": "$5$toOBSeX2$hSnSDyhJmVVRpbmKuIY4SxDgyeGRGacQaBYGrtEBnZA",
"mobile": "110",
"email": "admin@zifangsky.cn",
"createTime": "2017-12-31T16:00:00.000+0000",
"updateTime": "2017-12-31T16:00:00.000+0000",
"status": 1,
"roles": [{
"id": 2,
"roleName": "user",
"description": "普通用户",
"funcs": null
}
]
}
}
首先在一个拦截器里校验Access Token是否有效:
package cn.zifangsky.interceptor;
import cn.zifangsky.enums.ErrorCodeEnum;
import cn.zifangsky.model.SsoAccessToken;
import cn.zifangsky.service.SsoService;
import cn.zifangsky.utils.DateUtils;
import cn.zifangsky.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 用于校验Access Token是否为空以及Access Token是否已经失效
*
* @author zifangsky
* @date 2018/8/30
* @since 1.0.0
*/
public class SsoAccessTokenInterceptor extends HandlerInterceptorAdapter{
@Resource(name = "ssoServiceImpl")
private SsoService ssoService;
/**
* 检查Access Token是否已经失效
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String accessToken = request.getParameter("access_token");
if(StringUtils.isNoneBlank(accessToken)){
//查询数据库中的Access Token
SsoAccessToken ssoAccessToken = ssoService.selectByAccessToken(accessToken);
if(ssoAccessToken != null){
Long savedExpiresAt = ssoAccessToken.getExpiresIn();
//过期日期
LocalDateTime expiresDateTime = DateUtils.ofEpochSecond(savedExpiresAt, null);
//当前日期
LocalDateTime nowDateTime = DateUtils.now();
//如果Access Token已经失效,则返回错误提示
return expiresDateTime.isAfter(nowDateTime) || this.generateErrorResponse(response, ErrorCodeEnum.EXPIRED_TOKEN);
}else{
return this.generateErrorResponse(response, ErrorCodeEnum.INVALID_GRANT);
}
}else{
return this.generateErrorResponse(response, ErrorCodeEnum.INVALID_REQUEST);
}
}
/**
* 组装错误请求的返回
*/
private boolean generateErrorResponse(HttpServletResponse response,ErrorCodeEnum errorCodeEnum) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "application/json;charset=UTF-8");
Map<String,String> result = new HashMap<>(2);
result.put("error", errorCodeEnum.getError());
result.put("error_description",errorCodeEnum.getErrorDescription());
response.getWriter().write(JsonUtils.toJson(result));
return false;
}
}
然后返回用户信息:
/**
* 校验Access Token,并返回用户信息
* @author zifangsky
* @date 2018/8/30 16:07
* @since 1.0.0
* @param request HttpServletRequest
* @return java.util.Map<java.lang.String,java.lang.Object>
*/
@RequestMapping(value = "/verify", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> verify(HttpServletRequest request) {
Map<String, Object> result = new HashMap<>(8);
//获取Access Token
String accessToken = request.getParameter("access_token");
try {
//过期时间
Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
//查询Access Token
SsoAccessToken ssoAccessToken = ssoService.selectByAccessToken(accessToken);
//查询Refresh Token
SsoRefreshToken ssoRefreshToken = ssoService.selectByTokenId(ssoAccessToken.getId());
//查询用户信息
UserBo userBo = userService.selectUserBoByUserId(ssoAccessToken.getUserId());
//组装返回信息
result.put("access_token", ssoAccessToken.getAccessToken());
result.put("refresh_token", ssoRefreshToken.getRefreshToken());
result.put("expires_in", expiresIn);
result.put("user_info", userBo);
return result;
}catch (Exception e){
logger.error(e.getMessage());
this.generateErrorResponse(result, ErrorCodeEnum.UNKNOWN_ERROR);
return result;
}
}
(3)通过Refresh Token刷新Access Token接口:
如果客户端的Access Token过期了,那么就可以通过这个接口刷新Access Token。
接口地址:http://127.0.0.1:7000/sso/refreshToken?refresh_token=12.c10cb9001bf0e2c7f808580318715fc089673279.31536000.1568087183
返回如下:
{
"access_token": "11.40f0270697c37db4570e41e0f6f335bf6c2f8902.2592000.1539164947",
"refresh_token": "12.c10cb9001bf0e2c7f808580318715fc089673279.31536000.1568087183",
"expires_in": 2592000,
"user_info": {
"id": 2,
"username": "zifangsky",
"password": "$5$toOBSeX2$hSnSDyhJmVVRpbmKuIY4SxDgyeGRGacQaBYGrtEBnZA",
"mobile": "110",
"email": "admin@zifangsky.cn",
"createTime": "2017-12-31T16:00:00.000+0000",
"updateTime": "2017-12-31T16:00:00.000+0000",
"status": 1,
"roles": [{
"id": 2,
"roleName": "user",
"description": "普通用户",
"funcs": null
}
]
}
}
/**
* 通过Refresh Token刷新Access Token
* @author zifangsky
* @date 2018/8/30 16:07
* @since 1.0.0
* @param request HttpServletRequest
* @return java.util.Map<java.lang.String,java.lang.Object>
*/
@RequestMapping(value = "/refreshToken", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> refreshToken(HttpServletRequest request){
Map<String,Object> result = new HashMap<>(8);
//获取Refresh Token
String refreshTokenStr = request.getParameter("refresh_token");
//获取用户IP
String requestIp = SpringContextUtils.getRequestIp(request);
try {
SsoRefreshToken ssoRefreshToken = ssoService.selectByRefreshToken(refreshTokenStr);
if(ssoRefreshToken != null) {
Long savedExpiresAt = ssoRefreshToken.getExpiresIn();
//过期日期
LocalDateTime expiresDateTime = DateUtils.ofEpochSecond(savedExpiresAt, null);
//当前日期
LocalDateTime nowDateTime = DateUtils.now();
//如果Refresh Token已经失效,则需要重新生成
if (expiresDateTime.isBefore(nowDateTime)) {
this.generateErrorResponse(result, ErrorCodeEnum.EXPIRED_TOKEN);
return result;
} else {
//获取存储的Access Token
SsoAccessToken ssoAccessToken = ssoService.selectByAccessId(ssoRefreshToken.getTokenId());
//查询接入客户端
SsoClientDetails ssoClientDetails = ssoService.selectByPrimaryKey(ssoAccessToken.getClientId());
//获取对应的用户信息
User user = userService.selectByUserId(ssoAccessToken.getUserId());
//新的过期时间
Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
//生成新的Access Token
String newAccessTokenStr = ssoService.createAccessToken(user, expiresIn, requestIp, ssoClientDetails);
//查询用户信息
UserBo userBo = userService.selectUserBoByUserId(ssoAccessToken.getUserId());
logger.info(MessageFormat.format("单点登录重新刷新Token:username:【{0}】,requestIp:【{1}】,old token:【{2}】,new token:【{3}】"
,user.getUsername(),requestIp,ssoAccessToken.getAccessToken(),newAccessTokenStr));
//组装返回信息
result.put("access_token", newAccessTokenStr);
result.put("refresh_token", ssoRefreshToken.getRefreshToken());
result.put("expires_in", expiresIn);
result.put("user_info", userBo);
return result;
}
}else {
this.generateErrorResponse(result, ErrorCodeEnum.INVALID_GRANT);
return result;
}
}catch (Exception e){
e.printStackTrace();
this.generateErrorResponse(result, ErrorCodeEnum.UNKNOWN_ERROR);
return result;
}
}
/**
* 组装错误请求的返回
*/
private void generateErrorResponse(Map<String,Object> result, ErrorCodeEnum errorCodeEnum) {
result.put("error", errorCodeEnum.getError());
result.put("error_description",errorCodeEnum.getErrorDescription());
}
(4)单点注销接口:
在这个Demo项目中,我没有提供单点注销功能的示例代码,但是我可以简单说一下单点注销功能的主要流程,如果需要这个功能可以自行使用代码实现:
- 用户在应用子系统请求注销登录;
- 用户在应用子系统注销完成后,应用子系统后台请求单点登录服务端的注销接口;
- 单点登录服务端的注销接口根据用户的Token在数据库中查询当前用户登录的所有应用子系统的注销接口,然后依次调用注销即可。
四 接入单点登录的子系统的关键代码
这个Demo的单点登录子系统的完整可用源码可以参考:gitee.com/zifangsky/O…
其实,对于接入单点登录的子系统来说,登录模块调用单点登录服务端提供的接口就可以了。
登录校验过滤器:
package cn.zifangsky.interceptor;
import cn.zifangsky.common.Constants;
import cn.zifangsky.common.SpringContextUtils;
import cn.zifangsky.model.bo.UserBo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 定义一些页面需要做登录检查
*
* @author zifangsky
* @date 2018/7/26
* @since 1.0.0
*/
public class LoginInterceptor extends HandlerInterceptorAdapter{
/**
* 检查是否已经登录
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
//获取session中存储的用户信息
UserBo user = (UserBo) session.getAttribute(Constants.SESSION_USER);
if(user != null){
return true;
}else{
//如果token不存在,则跳转等登录页面
response.sendRedirect(request.getContextPath() + "/login?redirectUrl=" + SpringContextUtils.getRequestUrl(request));
return false;
}
}
}
登录相关的代码逻辑:
package cn.zifangsky.controller;
import cn.zifangsky.common.Constants;
import cn.zifangsky.model.SsoResponse;
import cn.zifangsky.model.User;
import cn.zifangsky.utils.CookieUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 登录
* @author zifangsky
* @date 2018/7/9
* @since 1.0.0
*/
@Controller
public class LoginController {
@Autowired
private RestTemplate restTemplate;
@Value("${own.sso.access-token-uri}")
private String accessTokenUri;
@Value("${own.sso.verify-uri}")
private String verifyUri;
/**
* 登录验证(实际登录调用认证服务器)
* @author zifangsky
* @date 2018/8/30 18:02
* @since 1.0.0
* @param request HttpServletRequest
* @return org.springframework.web.servlet.ModelAndView
*/
@RequestMapping("/login")
public ModelAndView login(HttpServletRequest request, HttpServletResponse response){
//当前系统登录成功之后的回调URL
String redirectUrl = request.getParameter("redirectUrl");
//当前系统请求认证服务器成功之后返回的Token
String code = request.getParameter("code");
//最后重定向的URL
String resultUrl = "redirect:";
HttpSession session = request.getSession();
//1. code为空,则说明当前请求不是认证服务器的回调请求,则重定向URL到认证服务器登录
if(StringUtils.isBlank(code)){
//如果存在回调URL,则将这个URL添加到session
if(StringUtils.isNoneBlank(redirectUrl)){
session.setAttribute(Constants.SESSION_LOGIN_REDIRECT_URL,redirectUrl);
}
//拼装请求Token的地址
resultUrl += accessTokenUri;
}else{
//2. 验证Token,并返回用户基本信息、所属角色、访问权限等
SsoResponse verifyResponse = restTemplate.getForObject(verifyUri, SsoResponse.class
,code);
//如果正常返回
if(StringUtils.isNoneBlank(verifyResponse.getAccess_token())){
//2.1 将用户信息存到session
session.setAttribute(Constants.SESSION_USER,verifyResponse.getUser_info());
//2.2 将Access Token和Refresh Token写到cookie
CookieUtils.addCookie(response,Constants.COOKIE_ACCESS_TOKEN, verifyResponse.getAccess_token(),request.getServerName());
CookieUtils.addCookie(response,Constants.COOKIE_REFRESH_TOKEN, verifyResponse.getRefresh_token(),request.getServerName());
}
//3. 从session中获取回调URL,并返回
redirectUrl = (String) session.getAttribute(Constants.SESSION_LOGIN_REDIRECT_URL);
session.removeAttribute("redirectUrl");
if(StringUtils.isNoneBlank(redirectUrl)){
resultUrl += redirectUrl;
}else{
resultUrl += "/user/userIndex";
}
}
return new ModelAndView(resultUrl);
}
}
当然,上面代码中使用到的一些配置就是我们单点登录服务端的接口地址:
own.sso.access-token-uri=http://10.0.5.22:7000/sso/token?redirect_uri=http://192.168.197.130:6080/login
own.sso.verify-uri=http://10.0.5.22:7000/sso/verify?access_token={1}
测试:
- 将
SsoClientDemo项目
部署在跟ServerDemo项目
不同的服务器; - 第一次启动
SsoClientDemo项目
并访问需要登录的页面,比如:http://192.168.197.130:6080/user/userIndex
; - 可以发现这时跳转到
ServerDemo项目
,在服务端登录成功之后,跳转到SsoClientDemo项目
的/user/userIndex
,说明客户端也登录成功了; - 重启
SsoClientDemo项目
,并再次访问http://192.168.197.130:6080/user/userIndex
,可以发现这次是直接登录了(当然也可以把SsoClientDemo项目
部署到多个服务器上面,先后登录查看效果),这说明单点登录功能已经实现。
本篇文章到此结束,感谢大家的阅读。
参考:
转载自:https://juejin.cn/post/6844903677447110663