单点登录(简化版)示例

本示例比常规的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);
        }
    }

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
底层架构优化 Maven多项目依赖,模块及插件分项目,尽量松耦合,方便模块升级、增减模块。 模块化数据库自动升级程序,当模块升级代码需要更新时,自动对应版本执行SQL更新。 授权模块,支持CAS单点登录简单properties配置即可,不用再写很多的xml。 支持多数据源,简单properties配置即可实现,为了安全性吧,暂不提供界面维护数据源,不存数据库。 数据表主键优化,如分类科目表,采用有意义的主键方式,让客户去自定义,减少后期运维维护成本。 实体@Table注解配置 a. 自动生成增删改通用SQL,不需要在mapper.xml里写重复又费时的SQL了,减少开发和后期维护成本。b. 这里所有定义@Column均放到类头,而不是分布到各个属性或方法上,这是如下原因:c. 一是,放到表头的好处是,可一览熟知该实体类对应的物理表结构是什么样,开发者思维从物理表结构到对象的映射转换,都是基于物理表结构的,@Column指定物理字段名,而不是指定类上的属性名,也是这个原因;d. 二是,生成的SQL和查询条件,列的排列顺序,可方便核查定义,优化查询;e. 三是,方便@JoinTable关联表和其它扩展信息的设置,如果分布到类的属性上就不太好管理列。 用户数据权限优化,不仅仅是角色,颗粒度细化到每个人员的身上,可自定义第三方数据权限,不仅仅是部门和公司,你可以扩展,如:地区,栏目分类,商品分类。 支持SAAS多租户模式,每个租户数据通过corp_code字段进行分离,数据互不干扰。 缓存EhCache统一管理,支持快速切换为Redis缓存,集群Session缓存共享。 安全方面优化 原有JeeSite1.2安全选项及安全考虑保留。 配置文件数据库密码及其它安全密钥自动加密。 所有请求参数获取,均通过XSS跨站脚本过滤方法。 乐观锁简单实现,必要情况下使用,提高数据安全性。 通过properties简单配置,限制是否允许JS跨域操作。 身份认证 a. 登录失败多少次后显示验证码b. 登录失败多少次后锁定账号及锁定账号时间c. 登录账号密码可加密后再提交后台d. 同设备是否允许账号多地登录 密码策略 a. 初始密码修改策略,提醒或强制用户修改初始密码。b. 账号密码修改策略,多长时间内未修改,则提醒或强制修改密码。c. 账号密码安全等级限制策略,很弱密码,弱密码,安全密码,很安全密码 安全审计(后期) a. 查询未修改初始密码的账号,使用简单密码的,定期未修改的,长期未登录的。b. 权限审计,按登录账号查询菜单和权限,按菜单和权限查询登录账号 用户界面优化 平面化界面设计,精细,更加美观、高端、大气、上档次。 支持手机或平板访问,响应式样式,根据屏幕分辨率自适应控件布局。 无刷新设计,除了进入功能页面和新页面,其它情况下全部采用Ajax交互,优化体验和性能。 支持一件换肤,只需在properties里修改下主题名称即可快速切换整个UI的风格,不仅仅是色调和样式,布局也可改变。支持自定义扩展项目独有的主题样式风格。 优化用户功能操作,大众思维模式,功能清晰,更加贴切和友好。 前端开发优化 采用当前比较流行的Beetl模板引擎,它的优点很多,大家可自行查阅看看,这里不多说了。 封装Beetl UI通用组件,简单实现基本表单控件、树选择,列表选择,文件上传,等等很多,总之是简化开发。 数据表格jqGrid组件封装,自动完成分页、排序、列宽、多表头、子表、编辑表、等。 功能及组件优化 工具类Utils封装优化,应有尽有,包分类层次分明,独立工具类项目。 强大的Excel导出导入工具封装,支持大数据量,注解定义,简单配置即可实现。 Job作业调度,界面化在线管理,可新增,编辑、删除、暂停、恢复、运行一次等操作 在线查询在线人员,强踢在线账号。 代码生成工具操作简化及优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值