本示例比常规的SSO单点登录少了授权码的相关逻辑。
服务端生成token令牌,调用业务系统(客户端)单点登录接口,同时跳转到业务系统页面。
本示例的代码框架基于 jeesite。
整体逻辑如下:
图中的“门户系统”即为服务端,业务系统即为客户端。
服务端代码:
@ApiOperation(value = "单点登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "clientId", value = "业务系统接入客户端id", required = true, type="String")
})
@GetMapping("/ssoLogin/{clientId}")
public void ssoLogin(@PathVariable String clientId, HttpServletRequest request, HttpServletResponse response) {
logger.info("单点登录-开始.clientId:{}", clientId);
if (StringUtils.isBlank(clientId)) {
logger.info("单点登录,clientId为空");
throw new ServiceException("clientId不能为空");
}
try {
// 1.查询业务系统接入配置信息
BizSysSsoConfig bizSysSsoConfig = bizSysSsoConfigService.getByClientId(clientId);
if (bizSysSsoConfig == null) {
logger.info("单点登录,业务系统接入配置信息不存在.clientId:{}", clientId);
throw new ServiceException("业务系统接入配置信息不存在");
}
if (!StatusEnum.NORMAL.getCode().equals(bizSysSsoConfig.getStatus())) {
String statusMsg = StatusEnum.getNameByValue(bizSysSsoConfig.getStatus());
logger.info("单点登录,业务系统接入配置信息{}.clientId:{}", statusMsg, clientId);
throw new ServiceException("业务系统接入配置信息" + statusMsg);
}
if (StringUtils.isBlank(bizSysSsoConfig.getBizSysLoginUrl())) {
logger.info("单点登录,业务系统登录接口url为空.clientId:{}", clientId);
throw new ServiceException("业务系统登录接口url为空");
}
// 获取当前登录用户
LoginUserInfo loginUser = LoginContextUtil.getLoginUserInfo();
if (loginUser == null || StringUtils.isBlank(loginUser.getUserCode())) {
logger.info("单点登录,用户尚未登录.clientId:{}", clientId);
throw new ServiceException("您尚未登录");
}
// 生成令牌
String randomToken = UUID.randomUUID().toString().replace("-", "");
String accessToken = Md5Utils.md5(randomToken);
// 缓存
redisUtils.set(OPEN_API_ACCESS_TOKEN_KEY_PREFIX + accessToken, loginUser, 60 * 60 * 12);
logger.info("单点登录,生成令牌.clientId:{}, accessToken:{}", clientId, accessToken);
// 业务系统登录接口url(如:http://127.0.0.1:3100/js/xxx/api/sso/login)
String bizSysLoginUrl = bizSysSsoConfig.getBizSysLoginUrl();
bizSysLoginUrl += "?token=" + accessToken;
logger.info("单点登录,重定向-开始.clientId:{}, accessToken:{}, 重定向地址:{}", clientId, accessToken, bizSysLoginUrl);
response.sendRedirect(bizSysLoginUrl);
logger.info("单点登录-结束.clientId:{}", clientId);
} catch (Exception e) {
logger.error(String.format("单点登录-失败.clientId:%s", clientId), e);
throw new ServiceException("重定向异常");
}
}
/**
* 获取当前登录用户信息
* @param accessToken 访问令牌
*/
@GetMapping("/getLoginUser")
public OpenApiWebResult<?> loginUserInfo(@RequestParam("accessToken") String accessToken) {
try {
// 请求合法性验证
if (StringUtils.isBlank(accessToken)) {
return OpenApiWebResult.errorWith(OpenApiResponseEnum.INVALID_REQUEST.getCode(), OpenApiResponseEnum.INVALID_REQUEST.getMessage());
}
// 查找登录用户信息
String cacheKey = OPEN_API_ACCESS_TOKEN_KEY_PREFIX + accessToken;
Object o = redisUtils.get(cacheKey);
if (o == null) {
return OpenApiWebResult.errorWith(OpenApiResponseEnum.EXPIRED_TOKEN.getCode(), OpenApiResponseEnum.EXPIRED_TOKEN.getMessage());
}
// 返回用户信息
LoginUserInfo loginUserInfo = JSON.toJavaObject((JSONObject)o, LoginUserInfo.class);
LoginUserRespData data = new LoginUserRespData();
data.setLoginCode(loginUserInfo.getLoginCode());
data.setUserName(loginUserInfo.getUserName());
return OpenApiWebResult.successWith(OpenApiResponseEnum.SUCCESS.getCode(), OpenApiResponseEnum.SUCCESS.getMessage(), data);
} catch (Exception e) {
return OpenApiWebResult.errorWith(OpenApiResponseEnum.SYSTEM_BUSY.getCode(), OpenApiResponseEnum.SYSTEM_BUSY.getMessage());
}
}
客户端代码:
api: # 第三方系统接口
hb_portal: # 门户
# 成功调用接口返回值
success_code: "00000"
# 获取登录用户信息
get_login_user_url: http://127.0.0.1:8980/js/openapi/getLoginUser?accessToken=%s
/**
* 【接入门户系统单点登录】单点登录
*
* @date 2024-05-16
* @param token
* @return String
* @since JDK 1.8
* @author wenjianhai
*/
@ApiOperation("单点登录")
@GetMapping("/login")
public String ssoLogin(@RequestParam String token, Model model) {
log.info("接入门户系统单点登录-开始.token:{}", token);
if (StringUtils.isBlank(token)) {
return "";
}
try {
return migrateSsoLoginService.ssoLogin(token, model);
} catch (Exception e) {
log.error(String.format("接入门户系统单点登录-失败.token:%s", token), e);
return "";
}
}
/** 门户系统--成功调用接口返回值 */
@Value("${api.hb_portal.success_code:00000}")
private String hbPortalSuccesCode;
/** 门户系统--获取登录用户信息 接口url */
@Value("${api.hb_portal.get_login_user_url}")
private String getLoginUserUrl;
/**
* 【接入门户系统单点登录】单点登录
*
* @date 2024-05-16
* @param token
* @since JDK 1.8
* @author wenjianhai
*/
@Override
public String ssoLogin(String token, Model model) throws Exception {
if (StringUtils.isBlank(token)) {
log.info("接入门户系统单点登录,token为空");
return "";
}
String portalUrl = String.format(getLoginUserUrl, token);
log.info("接入门户系统单点登录,调用门户接口,获取当前登录用户信息-开始.url:{}", portalUrl);
// 调用门户接口,获取当前登录用户信息
String portalResult = restTemplateUtil.get(portalUrl, null);
log.info("接入门户系统单点登录,调用门户接口,获取当前登录用户信息-结束.url:{}, 返回结果:{}", portalUrl, portalResult);
// 接口返回码
String portalRetCode = JsonUtils.getJsonValue(portalResult, "code");
if (!hbPortalSuccesCode.equalsIgnoreCase(portalRetCode)) {
log.info("接入门户系统单点登录,调用门户接口,获取当前登录用户信息-失败.url:{}, 返回结果:{}", portalUrl, portalResult);
String message = StringUtils.isBlank(portalRetCode) ? "获取当前登录用户信息失败"
: HbPortalEnum.ApiRetCode.getMessage(portalRetCode);
return "";
}
String portalData = JsonUtils.getJsonValue(portalResult, "data");
if (StringUtils.isBlank(portalData)) {
log.info("接入门户系统单点登录,调用门户接口,获取当前登录用户信息,data为空.url:{}, 返回结果:{}", portalUrl, portalResult);
return "";
}
// 登录账号
String loginCode = JsonUtils.getJsonValue(portalData, "loginCode");
if (StringUtils.isBlank(loginCode)) {
log.info("接入门户系统单点登录,调用门户接口,获取当前登录用户信息,loginCode为空.url:{}, 返回结果:{}", portalUrl, portalResult);
return "";
}
// 原始的HTTP请求和响应的信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
HttpServletResponse response = attributes.getResponse();
JsSysUser user = jsSysUserDao.queryByLoginCode(loginCode);
if (user == null) {
log.info("接入门户系统单点登录,业务系统的系统用户信息为空.loginCode:{}", loginCode);
return "";
}
if (!StatusEnum.NORMAL.getCode().equals(user.getStatus())) {
String statusMsg = StatusEnum.getNameByValue(user.getStatus());
log.info("接入门户系统单点登录,业务系统的系统用户{}.loginCode:{}", statusMsg, loginCode);
return "";
}
UserUtils.getSubject().login(new FormToken(loginCode, true, request));
// FormFilter.onLoginSuccess(request, response);
Session session = UserUtils.getSession();
model.addAttribute("user", user);
model.addAttribute("demoMode", Global.isDemoMode());
model.addAttribute("useCorpModel", Global.isUseCorpModel());
model.addAttribute("currentCorpCode", CorpUtils.getCurrentCorpCode());
model.addAttribute("currentCorpName", CorpUtils.getCurrentCorpName());
model.addAttribute("sysCode", session.getAttribute("sysCode"));
model.addAttribute("officeCode", session.getAttribute("officeCode"));
model.addAttribute("captchaType", Global.getConfig("sys.login.captchaType", "default"));
model.addAttribute("loginType", Global.getConfig("sys.login.loginType", "phone"));
model.addAttribute("isValidCodeLogin", Global.getConfigToBoolean("sys.login.isValidCodeLogin", "false"));
// 由于单点登录默认页面是业务系统的前端页面首页,所以需要在 application.yml 中配置 shiro.successUrl
// FormFilter.onLoginSuccess 中,跳转页面默认取的是配置项 shiro.successUrl,所以只能配置 shiro.successUrl,不能在此写死页面路径
String successUrl = Global.getProperty("shiro.successUrl");
log.info("接入门户系统单点登录-结束.token:{}, 返回结果:{}", token, successUrl);
response.sendRedirect(domain + "/homepage/homepage");
return "/homepage/homepage";
// return "redirect:" + successUrl;
// successUrl = domain + successUrl;
// return successUrl;
}
客户端代码:
(1)服务端调用的时候,不需要传 Model,model绑定的数据,会自动存储到浏览器的 storege 中;
(2)UserUtils.getSubject().login(new FormToken(loginCode, true, request)); 即为模拟登录的代码,使用的是jeesite框架。也可自己写模拟登录的代码。
(3)/homepage/homepage 即为单点登录成功后,跳转的业务系统页面。
BizSysSsoConfig.ajva
public class BizSysSsoConfig {
private static final long serialVersionUID = 1L;
private String bizSysCode; // 业务系统标识
private String bizSysName; // 业务系统名称
private String clientId; // 接入客户端id 申请对接时分配
private String clientSecret; // 接入客户端秘钥 申请对接时分配
private String bizSysPageUrl; // 业务系统页面url
private String bizSysLoginUrl; // 业务系统登录接口url
}
业务系统接入配置信息表
CREATE TABLE t_biz_sys_sso_config (
id bigint NOT NULL PRIMARY KEY comment '主键id',
biz_sys_code varchar(64) NOT NULL comment '业务系统标识',
biz_sys_name varchar(50) NOT NULL comment '业务系统名称',
client_id varchar(64) NOT NULL comment '接入客户端id 申请对接时分配',
client_secret varchar(256) NOT NULL comment '接入客户端秘钥 申请对接时分配',
biz_sys_page_url varchar(300) NOT NULL comment '业务系统页面url',
biz_sys_login_url varchar(300) NOT NULL comment '业务系统登录接口url',
status varchar(1) not null DEFAULT '0' comment '状态(0:正常, 1:已删除, 2:已停用)',
create_date NOT NULL DEFAULT now() comment '创建时间',
create_by varchar(64) comment '创建人',
update_date NOT NULL DEFAULT now() comment '更新时间',
update_by varchar(64) comment '更新人',
remarks varchar(255) comment '备注'
) engine=innodb default charset=utf8mb4 comment = '业务系统接入配置信息表';
INSERT INTO t_biz_sys_sso_config
(
id, biz_sys_code, biz_sys_name, client_id, client_secret, status, create_date, create_by, update_date, update_by, remarks, biz_sys_page_url, biz_sys_login_url
)
VALUES
(
2, 'migrate', 'xxx系统', '1002', 'xxx', '0', '2024-05-16 20:43:33', 'system', '2024-05-16 20:43:38', 'system', NULL, 'http://IP:前端页面端口号/homepage/homepage', 'http://IP:前端页面端口号/js/xxx/api/sso/login'
);
commit;
LoginUserInfo.java
public class LoginUserInfo {
private String userId;
private String userCode;
private String loginCode;
private String userName;
}
RedisUtils.java
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
*/
public void set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
} catch (Exception e) {
log.error(String.format("普通缓存放入并设置时间-失败.key:%s, value:%s, time:%d", key, value.toString(), time), e);
}
}