xxl-job 登入功能集成 OIDC 统一认证

简介: xxl-job 是一款 java 开发的、开源的分布式任务调度系统,自带了登录认证功能,不支持对接、扩展 LDAP 、OIDC 等标准认证系统,考虑到单独维护 xxl-job 自有的用户系统不方便,以及存在人员离职、调岗、权限变动等需要及时调整用户权限的情况,需要接入公司统一的 OIDC 认证系统

前言

xxl-job 是一款 java 开发的、开源的分布式任务调度系统,自带了登录认证功能,不支持对接、扩展 LDAP 、OIDC 等标准认证系统,考虑到单独维护 xxl-job 自有的用户系统不方便,以及存在人员离职、调岗、权限变动等需要及时调整用户权限的情况,需要接入公司统一的 OIDC 认证系统

相关链接

xxl-job 自身认证功能分析

xxl-job 自带的登录认证用户信息维护在 mysql 的 user 表中,用户从登录页提交用户名和密码,后端查询用户信息、校验密码,验证成功后设置登录信息到 cookie 中,采用 cookie 保持登录状态,大致的流程如下:

OIDC 的认证流程

OIDC(OpenID Connect) 是一种融合了 OpenID 、Oauth2 的身份认证协议。认证流程上和 Oauth2 基本一致,但是,OIDC 在 Oauth2 的 access\_token 基础上新增了一个使用 jwt 生成的 idToken,idToken 中携带了用户基本信息,使用私钥验签成功后,可直接使用,省略了通过 access\_token 获取用户信息的步骤。所以 OIDC 的认证流程既和 Oauth2 类似又有区别,基本流程如下:

  1. 客户端准备包含所需请求参数的身份验证请求。
  2. 客户端将请求发送到授权服务器。
  3. 授权服务器对终端用户进行身份验证。
  4. 授权服务器获得终端用户同意/授权。
  5. 授权服务器将 code 发送回客户端 。
  6. 客户端将 code 发送到令牌端点获取 access_token 和 idToken。
  7. 客户端使用私钥验证 idToken 拿到用户标识 or 将 access_token 发送到授权服务器获取用户标识。

这里注意最后第 6、7 点操作,这里开始 OIDC 和 Oauth2 不一样了

xxl-job 集成 OIDC 后的认证流程

从 OIDC 的认证流程得知,终端用户通过授权服务器授权认证后,授权服务器会携带 code 重定向到客户端服务,客户端通过 code 可以拿到用户唯一标识,通过这个唯一标识,可以继续完成客户端原本的认证流程。集成 OIDC 后,xxl-job 登录的大致流程如下:

集成 OIDC 后,系统认证保持用户登录状态的机制没有变化,依然使用  Cookie ,需要特殊处理以及关注地方有:

  • 用户首次登录系统,由于不存在系统中,需要先创建用户
  • 如果系统首次投产使用,记得设计一个可以从配置指定管理账户的功能,不然你得手动改数据库了
  • 如果系统运行很久了,需要考虑好原系统用户和 OIDC 授权用户的映射关系
  • 退出操作时,除了清除自身的用户登录状态,是否退出 OIDC 服务(实现 sso)的登录状态也需要考虑

xxl-job 登录模块重新设计

考虑开发环境使用 OIDC 服务不方便以及解耦对第三方认证授权服务的依赖,决定在集成 OIDC 时,兼容本地登录功能,登录流程由登录模式来控制区分,登录模式使用配置驱动,设计集成 OIDC 后 ,xxl-job 支持的登录模式如下:

  • onlyLocal :只支持 xxl-job 自身用户系统登录认证
  • onlyOidc : 只支持 Oidc 授权服务器授权登录认证
  • mix :混合模式,同时支持自身用户系统登录认证、Oidc 授权服务器授权登录认证

onlyLocal 模式登录界面:

mix 模式登录界面:

olnyOidc 模式登录界面:

olnyOidc 模式特殊,从设计上来说,如果需要保留用户使用习惯,可以保留一个跳转到 OIDC 授权服务器的链接按钮给用户点击。如果做的干净利落,在 olnyOidc 模式下,访问登录页可以直接 302 到 OIDC 授权服务器。

保留登录按钮的界面(实际这个页面取消了)

编码环节

配置属性类,省略了get、set

/**
 * @author kl (http://kailing.pub)
 * @since 2021/6/21
 */
@ConfigurationProperties(prefix = "oidc")
@Configuration
public class OidcProperties {

    private static final LoginMod DEFAULT_LOGIN_MOD = LoginMod.onlyLocal;

    private LoginMod loginMod = DEFAULT_LOGIN_MOD;
    private String clientId;
    private String clientSecret;
    private String accessTokenUrl;
    private String profileUrl;
    private String redirectUri;
    private String logoutUrl;
    private String loginUrl;
    private List<String> adminLists = new ArrayList<>();

    public enum LoginMod {
        mix,
        onlyOidc,
        onlyLocal
    }
}

对应了如下的配置, 除了 login-mod  、redirect-uri 、admin-Lists 是 xxl-job 自身登录功能需要,其他的配置均由 OIDC 授权服务器提供

oidc.login-mod=onlyOidc
oidc.client-id = xxl-job-dev
oidc.client-secret = xx
oidc.base-url = https://sso.security.oidc.com
oidc.access-token-url = ${oidc.base-url}/cas/oidc/accessToken
oidc.login-url = ${oidc.base-url}/cas/oidc/authorize?response_type=code&client_id=${oidc.client-id}&redirect_uri=${oidc.redirect-uri}&scope=openid
oidc.redirect-uri = http://172.26.203.103:8071/oidc/tokenLogin
oidc.logout-url =${oidc.base-url}/cas/logout?service=${oidc.redirect-uri}
oidc.admin-Lists = chenkailing

Oidc 服务类,使用这个类里的方法和 OIDC 授权服务器交互

/**
 * @author kl (http://kailing.pub)
 * @since 2021/6/21
 */
@Service
public class OidcService {

    private final OidcProperties oidcProperties;
    private final RestTemplate restTemplate;
    
    public OidcService(OidcProperties oidcProperties, RestTemplate restTemplate) {
        this.oidcProperties = oidcProperties;
        this.restTemplate = restTemplate;
    }

    /**
     * 请求 OIDC 授权服务器,获取 idToken
     * idToken 中包含的信息 (非标准)
     * {
     * "sub": "248289761001",
     * "name": "Jane Doe",
     * "given_name": "Jane",
     * "family_name": "Doe",
     * "preferred_username": "j.doe",
     * "email": "janedoe@example.com",
     * "picture": "http://example.com/janedoe/me.jpg"
     * }
     */
    public String getUsernameByCode(String code) {
        URI uri = UriComponentsBuilder.fromUriString(oidcProperties.getAccessTokenUrl())
                .queryParam("client_id", oidcProperties.getClientId())
                .queryParam("client_secret", oidcProperties.getClientSecret())
                .queryParam("redirect_uri", oidcProperties.getRedirectUri())
                .queryParam("code", code)
                .queryParam("grant_type", "authorization_code")
                .build()
                .toUri();
        AuthorizationEntity auth = restTemplate.getForObject(uri, AuthorizationEntity.class);
        Assert.notNull(auth, "AccessToken is null");
        String idToken = auth.getIdToken();
        int i = idToken.lastIndexOf('.');
        String withoutSignatureToken = idToken.substring(0, i+1);
        return Jwts.parserBuilder()
                .build()
                .parseClaimsJwt(withoutSignatureToken)
                .getBody()
                .get("sub", String.class);
    }

    /**
     * @return 1 : 管理员 、0 : 普通用户
     */
    public int getUserRole(XxlJobUser user) {
        List<String> adminLists = oidcProperties.getAdminLists();
        if (adminLists.contains(user.getUsername())) {
            return 1;
        }
        return 0;
    }

    public String getOidcLoginUrl() {
        return oidcProperties.getLoginUrl();
    }

    public OidcProperties.LoginMod getLoginMod() {
        return oidcProperties.getLoginMod();
    }

    public boolean isRedirectOidcLoginUrl() {
        return oidcProperties.getLoginMod().equals(OidcProperties.LoginMod.onlyOidc);
    }

    public String getLogoutUrl() {
        return oidcProperties.getLogoutUrl();
    }

    static class AuthorizationEntity {

        @JsonProperty("access_token")
        private String accessToken;
        @JsonProperty("id_token")
        private String idToken;
        @JsonProperty("refresh_token")
        private String refreshToken;
        @JsonProperty("expires_in")
        private String expiresIn;
        @JsonProperty("token_type")
        private String tokenType;

        private String scope;
    }

}

OIDC 登录接口,也就是提供给 OIDC 授权服务器回调的接口

/**
 * OIDC登录
 */
@RequestMapping(value = "/oidc/tokenLogin", method = {RequestMethod.POST, RequestMethod.GET})
@PermissionLimit(limit = false)
public ModelAndView loginByOidc(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView) {
    if (loginService.ifLogin(request, response) != null) {
        modelAndView.setView(new RedirectView("/", true, false));
        return modelAndView;
    }
    String code = request.getParameter("code");
    if (Objects.isNull(code)) {
        return this.loginPageView();
    }
    String username = oidcService.getUsernameByCode(code);
    loginService.oidcLogin(username, response);

    modelAndView.setView(new RedirectView("/", true, false));
    return modelAndView;
}

这个接口对应了 xxl-job 集成 OIDC 后的认证流程:

  1. 判断是否登录,已经登录则跳转到登录成功的页面
  2. 获取 code ,不存在则调整到登录页面
  3. 通过 code 请求 OIDC 授权服务器获取 UserInfo
  4. 处理内部登录逻辑(用户是否存在,存在则设置 Cookie,不存在则先创建用户在设置 Cookie)
  5. 跳转到登录成功的页面

跳转登录页逻辑做了封装,因为,根据登录模式的不同,有不同的处理逻辑:

private ModelAndView loginPageView() {
    ModelAndView modelAndView = new ModelAndView(LOGIN_PAGE);
    if (oidcService.isRedirectOidcLoginUrl()) {
        modelAndView.setView(new RedirectView(oidcService.getOidcLoginUrl(), true, false));
    } else {
        modelAndView.addObject("loginMod", oidcService.getLoginMod().name());
        modelAndView.addObject("oidcLoginUrl", oidcService.getOidcLoginUrl());
    }
    return modelAndView;
}

目前的策略,如果配置了登录模式为 onlyOidc ,则跳转登录页时,直接 302 到 OIDC 授权页,否则,将登录模式,和 OIDC 授权页传递给前端,由前端控制展示的 UI

目录
相关文章
|
27天前
|
XML 测试技术 API
利用C#开发ONVIF客户端和集成RTSP播放功能
利用C#开发ONVIF客户端和集成RTSP播放功能
857 122
|
5月前
|
测试技术 数据处理 调度
Dataphin功能Tips系列(57)「预览」vs「运行」:离线集成的神奇按钮
在数据开发过程中,使用Dataphin处理离线集成任务时,可能遇到数据过滤和字段计算组件配置正确性的验证问题。通过「预览」功能,可快速验证处理逻辑而不影响目标表;对于需要调度的任务,担心资源占用和耗时超出预期时,可使用「运行」功能进行全流程测试,评估实际耗时与资源消耗。「预览」适合逻辑验证,「运行」用于真实环境模拟,两者结合助力高效开发与调试。
124 5
|
5月前
|
SQL Java 关系型数据库
Dataphin功能Tips系列(53)-离线集成任务如何合理配置JVM资源
本文探讨了将MySQL数据同步至Hive时出现OOM问题的解决方案。
123 5
|
1月前
|
SQL 运维 关系型数据库
【产品升级】Dataphin V5.0版本发布:助力出海业务、增全量一体集成、异步调用API等更多功能等你发现
Dataphin是瓴羊推出的智能数据建设与治理平台,基于阿里巴巴内部实践,提供一站式数据建设与治理能力。V5.0版本研发新增支持Databricks作为离线计算引擎、支持MySQL数据库一键增全量同步、支持管理Hudi、Delta Lake表等;资产运营与消费持续提效,支持批量编辑目录名称及描述、Quick BI仪表板的上架管理等功能,增加资产可用性与盘点效率。
141 8
|
7月前
|
安全 Java Apache
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
331 0
|
8月前
|
存储 NoSQL 关系型数据库
PolarDB开源数据库进阶课17 集成数据湖功能
本文介绍了如何在PolarDB数据库中接入pg_duckdb、pg_mooncake插件以支持数据湖功能, 可以读写对象存储的远程数据, 支持csv, parquet等格式, 支持delta等框架, 并显著提升OLAP性能。
465 2
|
2月前
|
存储 JSON 前端开发
Django集成图片验证码功能:基于django-simple-captcha实现
在Web应用开发中,验证码是防止恶意攻击、自动化脚本滥用的重要手段。本文将介绍如何使用django-simple-captcha库在Django项目中快速集成图片验证码功能,包括安装配置、核心实现代码及使用方法。
81 0
|
5月前
|
传感器 供应链 物联网
农业单亩价值创造功能技术集成的概念与内涵
农业单亩价值创造的技术集成,通过系统性创新打破传统单一模式,融合现代科技与生态理念,提升资源效率、经济效益和生态价值。其核心在于技术协同,实现精准农业、智能装备和生物强化等多维联动,推动经济、生态和社会价值统一。同时,注重资源集约化与循环化利用,延伸产业链并升级价值链,从短期高产转向长期可持续发展。政策与制度创新支撑技术普惠,未来需因地制宜解决技术适配性和成本收益平衡问题,重塑农业评价体系,实现高质量发展。
|
4月前
|
监控 安全 Java
Java 开发中基于 Spring Boot 3.2 框架集成 MQTT 5.0 协议实现消息推送与订阅功能的技术方案解析
本文介绍基于Spring Boot 3.2集成MQTT 5.0的消息推送与订阅技术方案,涵盖核心技术栈选型(Spring Boot、Eclipse Paho、HiveMQ)、项目搭建与配置、消息发布与订阅服务实现,以及在智能家居控制系统中的应用实例。同时,详细探讨了安全增强(TLS/SSL)、性能优化(异步处理与背压控制)、测试监控及生产环境部署方案,为构建高可用、高性能的消息通信系统提供全面指导。附资源下载链接:[https://pan.quark.cn/s/14fcf913bae6](https://pan.quark.cn/s/14fcf913bae6)。
587 0
|
5月前
|
存储 分布式计算 供应链
Dataphin功能Tips系列(51)-支持增全量一体实时集成
本文介绍了基于增全量一体实时集成的库存管理与分析解决方案。通过将业务中台的库存表同步至MaxCompute Delta表,实现离线与实时分析的统一支持。相比传统方案,该方法确保数据一致性,优化存储成本,降低维护复杂度,并大幅提升实时性,满足高效库存管理需求。
127 5