分析xxljob登入功能集成OIDC的统一认证

 更新时间:2022年02月21日 09:20:18   作者:kl  
这篇文章主要为大家介绍分析xxljob登入功能集成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 类似又有区别,基本流程如下:

  • 客户端准备包含所需请求参数的身份验证请求。
  • 客户端将请求发送到授权服务器。
  • 授权服务器对终端用户进行身份验证。
  • 授权服务器获得终端用户同意/授权。
  • 授权服务器将 code 发送回客户端 。
  • 客户端将 code 发送到令牌端点获取 access_token 和 idToken。
  • 客户端使用私钥验证 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 ListadminLists = 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) {
        ListadminLists = 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 后的认证流程:

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

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

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

以上就是分析xxljob登入功能集成OIDC的统一认证的详细内容,更多关于xxljob登入集成OIDC统一认证的资料请关注脚本之家其它相关文章!

相关文章

  • 理解JDK动态代理为什么必须要基于接口

    理解JDK动态代理为什么必须要基于接口

    这篇文章主要介绍了理解JDK动态代理为什么必须要基于接口,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • java中单例模式讲解

    java中单例模式讲解

    这篇文章主要介绍了java中单例模式,本文通过简单的案例,讲解了该模式在java中的使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • Mybatis一对多和多对一处理的深入讲解

    Mybatis一对多和多对一处理的深入讲解

    Mybatis可以通过关联查询实现,关联查询是几个表联合查询,只查询一次,通过在resultMap里面的association,collection节点配置一对一,一对多的类就可以完成,这篇文章主要给大家介绍了关于Mybatis一对多和多对一处理的相关资料,需要的朋友可以参考下
    2021-09-09
  • 详解Java中自定义注解的使用

    详解Java中自定义注解的使用

    Annontation是Java5开始引入的新特征,中文名称叫注解,它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。本文主要介绍了自定义注解的使用,希望对大家有所帮助
    2023-03-03
  • Java实现定时备份文件

    Java实现定时备份文件

    这篇文章主要为大家详细介绍了Java实现定时备份文件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • 详解Java如何利用数字描述更多的信息

    详解Java如何利用数字描述更多的信息

    在数据库里面 ,通常我们会用数字的递进来描述状态等信息 , 但是如果想进行更复杂的操作 , 就有必要对二进制有一定理解了。本文就来趣味性的探讨一下 , 如何通过更少的空间描述更多的信息
    2022-09-09
  • @RequestParam使用defaultValue属性设置默认值的操作

    @RequestParam使用defaultValue属性设置默认值的操作

    这篇文章主要介绍了@RequestParam使用defaultValue属性设置默认值的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • SpringCloud集成Sleuth和Zipkin的思路讲解

    SpringCloud集成Sleuth和Zipkin的思路讲解

    Zipkin 是 Twitter 的一个开源项目,它基于 Google Dapper 实现,它致力于收集服务的定时数据,以及解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现,这篇文章主要介绍了SpringCloud集成Sleuth和Zipkin,需要的朋友可以参考下
    2022-11-11
  • 通Java接口上传实现SMMS图床

    通Java接口上传实现SMMS图床

    这篇文章主要介绍了通Java接口上传实现SMMS图床,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • Idea 解决 Could not autowire. No beans of ''xxxx'' type found 的错误提示

    Idea 解决 Could not autowire. No beans of ''xxxx'' type found

    这篇文章主要介绍了Idea 解决 Could not autowire. No beans of 'xxxx' type found 的错误提示,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01

最新评论