分析xxljob登入功能集成OIDC的統(tǒng)一認(rèn)證
前言
xxl-job 是一款 java 開(kāi)發(fā)的、開(kāi)源的分布式任務(wù)調(diào)度系統(tǒng),自帶了登錄認(rèn)證功能,不支持對(duì)接、擴(kuò)展 LDAP 、OIDC 等標(biāo)準(zhǔn)認(rèn)證系統(tǒng),考慮到單獨(dú)維護(hù) xxl-job 自有的用戶(hù)系統(tǒng)不方便,以及存在人員離職、調(diào)崗、權(quán)限變動(dòng)等需要及時(shí)調(diào)整用戶(hù)權(quán)限的情況,需要接入公司統(tǒng)一的 OIDC 認(rèn)證系統(tǒng)
相關(guān)鏈接
XXL-JOB 自身認(rèn)證功能分析
xxl-job 自帶的登錄認(rèn)證用戶(hù)信息維護(hù)在 mysql 的 user 表中,用戶(hù)從登錄頁(yè)提交用戶(hù)名和密碼,后端查詢(xún)用戶(hù)信息、校驗(yàn)密碼,驗(yàn)證成功后設(shè)置登錄信息到 cookie 中,采用 cookie 保持登錄狀態(tài),大致的流程如下:

OIDC 的認(rèn)證流程
OIDC(OpenID Connect) 是一種融合了 OpenID 、Oauth2 的身份認(rèn)證協(xié)議。認(rèn)證流程上和 Oauth2 基本一致,但是,OIDC 在 Oauth2 的 access\_token 基礎(chǔ)上新增了一個(gè)使用 jwt 生成的 idToken,idToken 中攜帶了用戶(hù)基本信息,使用私鑰驗(yàn)簽成功后,可直接使用,省略了通過(guò) access\_token 獲取用戶(hù)信息的步驟。所以 OIDC 的認(rèn)證流程既和 Oauth2 類(lèi)似又有區(qū)別,基本流程如下:
- 客戶(hù)端準(zhǔn)備包含所需請(qǐng)求參數(shù)的身份驗(yàn)證請(qǐng)求。
- 客戶(hù)端將請(qǐng)求發(fā)送到授權(quán)服務(wù)器。
- 授權(quán)服務(wù)器對(duì)終端用戶(hù)進(jìn)行身份驗(yàn)證。
- 授權(quán)服務(wù)器獲得終端用戶(hù)同意/授權(quán)。
- 授權(quán)服務(wù)器將 code 發(fā)送回客戶(hù)端 。
- 客戶(hù)端將 code 發(fā)送到令牌端點(diǎn)獲取 access_token 和 idToken。
- 客戶(hù)端使用私鑰驗(yàn)證 idToken 拿到用戶(hù)標(biāo)識(shí) or 將 access_token 發(fā)送到授權(quán)服務(wù)器獲取用戶(hù)標(biāo)識(shí)。
這里注意最后第 6、7 點(diǎn)操作,這里開(kāi)始 OIDC 和 Oauth2 不一樣了
XXL-JOB 集成 OIDC 后的認(rèn)證流程
從 OIDC 的認(rèn)證流程得知,終端用戶(hù)通過(guò)授權(quán)服務(wù)器授權(quán)認(rèn)證后,授權(quán)服務(wù)器會(huì)攜帶 code 重定向到客戶(hù)端服務(wù),客戶(hù)端通過(guò) code 可以拿到用戶(hù)唯一標(biāo)識(shí),通過(guò)這個(gè)唯一標(biāo)識(shí),可以繼續(xù)完成客戶(hù)端原本的認(rèn)證流程。集成 OIDC 后,xxl-job 登錄的大致流程如下:

集成 OIDC 后,系統(tǒng)認(rèn)證保持用戶(hù)登錄狀態(tài)的機(jī)制沒(méi)有變化,依然使用 Cookie ,需要特殊處理以及關(guān)注地方有:
- 用戶(hù)首次登錄系統(tǒng),由于不存在系統(tǒng)中,需要先創(chuàng)建用戶(hù)
- 如果系統(tǒng)首次投產(chǎn)使用,記得設(shè)計(jì)一個(gè)可以從配置指定管理賬戶(hù)的功能,不然你得手動(dòng)改數(shù)據(jù)庫(kù)了
- 如果系統(tǒng)運(yùn)行很久了,需要考慮好原系統(tǒng)用戶(hù)和 OIDC 授權(quán)用戶(hù)的映射關(guān)系
- 退出操作時(shí),除了清除自身的用戶(hù)登錄狀態(tài),是否退出 OIDC 服務(wù)(實(shí)現(xiàn) sso)的登錄狀態(tài)也需要考慮
XXL-JOB 登錄模塊重新設(shè)計(jì)
考慮開(kāi)發(fā)環(huán)境使用 OIDC 服務(wù)不方便以及解耦對(duì)第三方認(rèn)證授權(quán)服務(wù)的依賴(lài),決定在集成 OIDC 時(shí),兼容本地登錄功能,登錄流程由登錄模式來(lái)控制區(qū)分,登錄模式使用配置驅(qū)動(dòng),設(shè)計(jì)集成 OIDC 后 ,xxl-job 支持的登錄模式如下:
- onlyLocal :只支持 xxl-job 自身用戶(hù)系統(tǒng)登錄認(rèn)證
- onlyOidc : 只支持 Oidc 授權(quán)服務(wù)器授權(quán)登錄認(rèn)證
- mix :混合模式,同時(shí)支持自身用戶(hù)系統(tǒng)登錄認(rèn)證、Oidc 授權(quán)服務(wù)器授權(quán)登錄認(rèn)證
onlyLocal 模式登錄界面:

mix 模式登錄界面:

olnyOidc 模式登錄界面:
olnyOidc 模式特殊,從設(shè)計(jì)上來(lái)說(shuō),如果需要保留用戶(hù)使用習(xí)慣,可以保留一個(gè)跳轉(zhuǎn)到 OIDC 授權(quán)服務(wù)器的鏈接按鈕給用戶(hù)點(diǎn)擊。如果做的干凈利落,在 olnyOidc 模式下,訪問(wèn)登錄頁(yè)可以直接 302 到 OIDC 授權(quán)服務(wù)器。
保留登錄按鈕的界面(實(shí)際這個(gè)頁(yè)面取消了)

編碼環(huán)節(jié)
配置屬性類(lèi),省略了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
}
}對(duì)應(yīng)了如下的配置, 除了 login-mod 、redirect-uri 、admin-Lists 是 xxl-job 自身登錄功能需要,其他的配置均由 OIDC 授權(quán)服務(wù)器提供
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 = chenkailingOidc 服務(wù)類(lèi),使用這個(gè)類(lèi)里的方法和 OIDC 授權(quán)服務(wù)器交互
/**
* @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;
}
/**
* 請(qǐng)求 OIDC 授權(quán)服務(wù)器,獲取 idToken
* idToken 中包含的信息 (非標(biāo)準(zhǔn))
* {
* "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 : 普通用戶(hù)
*/
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 授權(quán)服務(wù)器回調(diào)的接口
/**
* 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;
}這個(gè)接口對(duì)應(yīng)了 xxl-job 集成 OIDC 后的認(rèn)證流程:
- 判斷是否登錄,已經(jīng)登錄則跳轉(zhuǎn)到登錄成功的頁(yè)面
- 獲取 code ,不存在則調(diào)整到登錄頁(yè)面
- 通過(guò) code 請(qǐng)求 OIDC 授權(quán)服務(wù)器獲取 UserInfo
- 處理內(nèi)部登錄邏輯(用戶(hù)是否存在,存在則設(shè)置 Cookie,不存在則先創(chuàng)建用戶(hù)在設(shè)置 Cookie)
- 跳轉(zhuǎn)到登錄成功的頁(yè)面
跳轉(zhuǎn)登錄頁(yè)邏輯做了封裝,因?yàn)?,根?jù)登錄模式的不同,有不同的處理邏輯:
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 ,則跳轉(zhuǎn)登錄頁(yè)時(shí),直接 302 到 OIDC 授權(quán)頁(yè),否則,將登錄模式,和 OIDC 授權(quán)頁(yè)傳遞給前端,由前端控制展示的 UI
以上就是分析xxljob登入功能集成OIDC的統(tǒng)一認(rèn)證的詳細(xì)內(nèi)容,更多關(guān)于xxljob登入集成OIDC統(tǒng)一認(rèn)證的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解在Spring-Boot中實(shí)現(xiàn)通用Auth認(rèn)證的幾種方式
這篇文章主要介紹了詳解在Spring-Boot中實(shí)現(xiàn)通用Auth認(rèn)證的幾種方式,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07
mybatis plus代碼生成工具的實(shí)現(xiàn)代碼
這篇文章主要介紹了mybatis plus代碼生成工具的實(shí)現(xiàn)代碼,需要的朋友可以參考下2021-04-04
Java聊天室之實(shí)現(xiàn)客戶(hù)端群聊功能
這篇文章主要為大家詳細(xì)介紹了Java簡(jiǎn)易聊天室之實(shí)現(xiàn)客戶(hù)端群聊功能,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以了解一下2022-10-10
java枚舉類(lèi)的構(gòu)造函數(shù)實(shí)例詳解
這篇文章主要介紹了java枚舉類(lèi)的構(gòu)造函數(shù)實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05
關(guān)于在IDEA中SpringBoot項(xiàng)目中activiti工作流的使用詳解
這篇文章主要介紹了關(guān)于在IDEA中SpringBoot項(xiàng)目中activiti工作流的使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08

