Browse Source

[Feature][Authentication] support oauth2 login (#14743)

* support oauth2 login

---------

Co-authored-by: Eric Gao <ericgao.apache@gmail.com>
3.2.1-prepare
yangyang zhong 1 year ago committed by GitHub
parent
commit
70731a1617
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 88
      docs/docs/en/guide/security/authentication-type.md
  2. 88
      docs/docs/zh/guide/security/authentication-type.md
  3. BIN
      docs/img/security/authentication/create-client-credentials-1.png
  4. BIN
      docs/img/security/authentication/create-client-credentials-2.png
  5. BIN
      docs/img/security/authentication/login-with-oauth2.png
  6. 3
      dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/AppConfiguration.java
  7. 53
      dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/OAuth2Configuration.java
  8. 100
      dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/LoginController.java
  9. 23
      dolphinscheduler-api/src/main/resources/application.yaml
  10. 69
      dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/LoginControllerTest.java
  11. 49
      dolphinscheduler-api/src/test/resources/application.yaml
  12. 3
      dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
  13. 26
      dolphinscheduler-standalone-server/src/main/resources/application.yaml
  14. 1
      dolphinscheduler-ui/src/locales/en_US/login.ts
  15. 1
      dolphinscheduler-ui/src/locales/zh_CN/login.ts
  16. 14
      dolphinscheduler-ui/src/service/modules/login/index.ts
  17. 10
      dolphinscheduler-ui/src/service/modules/login/types.ts
  18. 7
      dolphinscheduler-ui/src/views/login/index.module.scss
  19. 24
      dolphinscheduler-ui/src/views/login/index.tsx
  20. 50
      dolphinscheduler-ui/src/views/login/use-login.ts

88
docs/docs/en/guide/security/authentication-type.md

@ -1,6 +1,6 @@
# Authentication Type # Authentication Type
* So far we support three authentication types, Apache DolphinScheduler password, LDAP and Casdoor SSO. * So far we support four authentication types, Apache DolphinScheduler password, LDAP, Casdoor SSO and OAuth2,the OAuth2 authorization login mode can be used with other authentication modes.
## Change Authentication Type ## Change Authentication Type
@ -30,6 +30,29 @@ security:
# jks file absolute path && password # jks file absolute path && password
trust-store: "/ldapkeystore.jks" trust-store: "/ldapkeystore.jks"
trust-store-password: "password" trust-store-password: "password"
oauth2:
enable: false
provider:
github:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: github
google:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: google
``` ```
For detailed explanation of specific fields, please see: [Api-server related configuration](../../architecture/configuration.md) For detailed explanation of specific fields, please see: [Api-server related configuration](../../architecture/configuration.md)
@ -110,3 +133,66 @@ casdoor:
redirect-url: http://localhost:5173/login redirect-url: http://localhost:5173/login
``` ```
## OAuth2
Dolphinscheduler can support multiple OAuth2 providers.
### Step1. Create Client Credentials
![create-client-credentials-1](../../../../img/security/authentication/create-client-credentials-1.png)
![create-client-credentials-2](../../../../img/security/authentication/create-client-credentials-2.png)
### Step2.Enable OAuth2 Login In The Api's Configuration File
```yaml
security:
authentication:
…… # omit
oauth2:
# Set enable to true to enable oauth2 login mode
enable: true
provider:
github:
# Set the provider authorization address, for example:https://github.com/login/oauth/authorize
authorizationUri: ""
# dolphinscheduler backend redirection interface address, for example :http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2
redirectUri: ""
# clientId
clientId: ""
# client secret
clientSecret: ""
# Set the provider's request token address
tokenUri: ""
# Set the provider address for requesting user information
userInfoUri: ""
# Redirect address after successful login, http://{ip}:{port}/login
callbackUrl: ""
# The image url of the login page jump button, if not filled, a text button will be displayed
iconUri: ""
provider: github
google:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: google
gitee:
authorizationUri: "https://gitee.com/oauth/authorize"
redirectUri: "http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2"
clientId: ""
clientSecret: ""
tokenUri: "https://gitee.com/oauth/token?grant_type=authorization_code"
userInfoUri: "https://gitee.com/api/v5/user"
callbackUrl: "http://127.0.0.1:5173/login"
iconUri: ""
provider: gitee
```
### Step.3 Login With OAuth2
![login-with-oauth2](../../../../img/security/authentication/login-with-oauth2.png)

88
docs/docs/zh/guide/security/authentication-type.md

@ -1,6 +1,6 @@
# 认证方式 # 认证方式
* 目前我们支持三种认证方式,Apache DolphinScheduler自身账号密码登录,LDAP和通过Casdoor实现的SSO登录 * 目前我们支持四种认证方式,Apache DolphinScheduler自身账号密码登录,LDAP, 通过Casdoor实现的SSO登录和通过Oauth2授权登录,并且oauth2授权登录方式可以和其他认证方式同时使用
## 修改认证方式 ## 修改认证方式
@ -30,6 +30,29 @@ security:
# jks file absolute path && password # jks file absolute path && password
trust-store: "/ldapkeystore.jks" trust-store: "/ldapkeystore.jks"
trust-store-password: "password" trust-store-password: "password"
oauth2:
enable: false
provider:
github:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: github
google:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: google
``` ```
具体字段解释详见:[Api-server相关配置](../../architecture/configuration.md) 具体字段解释详见:[Api-server相关配置](../../architecture/configuration.md)
@ -106,3 +129,66 @@ casdoor:
redirect-url: http://localhost:5173/login redirect-url: http://localhost:5173/login
``` ```
## 通过OAuth2授权认证登录
dolphinscheduler可以同时支持多种OAuth2的provider,只需要在配置文件中打开Oauth2的开关并进行简单的配置即可。
### 步骤1. 获取OAuth2客户端凭据
![create-client-credentials-1](../../../../img/security/authentication/create-client-credentials-1.png)
![create-client-credentials-2](../../../../img/security/authentication/create-client-credentials-2.png)
### 步骤2. 在api的配置文件中开启oauth2登录
```yaml
security:
authentication:
…… # 省略
oauth2:
# 将enable设置为true 开启oauth2登录模式
enable: true
provider:
github:
# 设置provider的授权地址,例如https://github.com/login/oauth/authorize
authorizationUri: ""
# dolphinscheduler的后端重定向接口地址,例如http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2
redirectUri: ""
# oauth2的 clientId
clientId: ""
# oauth2的 clientSecret
clientSecret: ""
# 设置provider的请求token的地址
tokenUri: ""
# 设置provider的请求用户信息的地址
userInfoUri: ""
# 登录成功后的重定向地址, http://{ip}:{port}/login
callbackUrl: ""
# 登录页跳转按钮的图片url,不填写则会展示一个文字按钮
iconUri: ""
provider: github
google:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: google
gitee:
authorizationUri: "https://gitee.com/oauth/authorize"
redirectUri: "http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2"
clientId: ""
clientSecret: ""
tokenUri: "https://gitee.com/oauth/token?grant_type=authorization_code"
userInfoUri: "https://gitee.com/api/v5/user"
callbackUrl: "http://127.0.0.1:5173/login"
iconUri: ""
provider: gitee
```
### 步骤3.使用oauth2登录
![login-with-oauth2](../../../../img/security/authentication/login-with-oauth2.png)

BIN
docs/img/security/authentication/create-client-credentials-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

BIN
docs/img/security/authentication/create-client-credentials-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

BIN
docs/img/security/authentication/login-with-oauth2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

3
dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/AppConfiguration.java

@ -105,7 +105,8 @@ public class AppConfiguration implements WebMvcConfigurer {
.addPathPatterns(LOGIN_INTERCEPTOR_PATH_PATTERN) .addPathPatterns(LOGIN_INTERCEPTOR_PATH_PATTERN)
.excludePathPatterns(LOGIN_PATH_PATTERN, REGISTER_PATH_PATTERN, .excludePathPatterns(LOGIN_PATH_PATTERN, REGISTER_PATH_PATTERN,
"/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html",
"/doc.html", "/swagger-ui/**", "*.html", "/ui/**", "/error"); "/doc.html", "/swagger-ui/**", "*.html", "/ui/**", "/error", "/oauth2-provider",
"/redirect/login/oauth2", "/cookies");
} }
@Override @Override

53
dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/configuration/OAuth2Configuration.java

@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dolphinscheduler.api.configuration;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
@ConditionalOnProperty(prefix = "security.authentication.oauth2", name = "enable", havingValue = "true")
@ConfigurationProperties(prefix = "security.authentication.oauth2")
public class OAuth2Configuration {
private Map<String, OAuth2ClientProperties> provider = new HashMap<>();
@Getter
@Setter
public static class OAuth2ClientProperties {
private String authorizationUri;
private String clientId;
private String redirectUri;
private String clientSecret;
private String tokenUri;
private String userInfoUri;
private String callbackUrl;
private String iconUri;
private String provider;
}
}

100
dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/LoginController.java

@ -23,27 +23,40 @@ import static org.apache.dolphinscheduler.api.enums.Status.SIGN_OUT_ERROR;
import static org.apache.dolphinscheduler.api.enums.Status.USER_LOGIN_FAILURE; import static org.apache.dolphinscheduler.api.enums.Status.USER_LOGIN_FAILURE;
import org.apache.dolphinscheduler.api.aspect.AccessLogAnnotation; import org.apache.dolphinscheduler.api.aspect.AccessLogAnnotation;
import org.apache.dolphinscheduler.api.configuration.OAuth2Configuration;
import org.apache.dolphinscheduler.api.enums.Status; import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.exceptions.ApiException; import org.apache.dolphinscheduler.api.exceptions.ApiException;
import org.apache.dolphinscheduler.api.security.Authenticator; import org.apache.dolphinscheduler.api.security.Authenticator;
import org.apache.dolphinscheduler.api.security.impl.AbstractSsoAuthenticator; import org.apache.dolphinscheduler.api.security.impl.AbstractSsoAuthenticator;
import org.apache.dolphinscheduler.api.service.SessionService; import org.apache.dolphinscheduler.api.service.SessionService;
import org.apache.dolphinscheduler.api.service.UsersService;
import org.apache.dolphinscheduler.api.utils.Result; import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.constants.Constants;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
import org.apache.dolphinscheduler.common.utils.OkHttpUtils;
import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.entity.User;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestAttribute;
@ -63,6 +76,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "LOGIN_TAG") @Tag(name = "LOGIN_TAG")
@RestController @RestController
@RequestMapping("") @RequestMapping("")
@Slf4j
public class LoginController extends BaseController { public class LoginController extends BaseController {
@Autowired @Autowired
@ -71,6 +85,12 @@ public class LoginController extends BaseController {
@Autowired @Autowired
private Authenticator authenticator; private Authenticator authenticator;
@Autowired(required = false)
private OAuth2Configuration oAuth2Configuration;
@Autowired
private UsersService usersService;
/** /**
* login * login
* *
@ -160,4 +180,84 @@ public class LoginController extends BaseController {
request.removeAttribute(Constants.SESSION_USER); request.removeAttribute(Constants.SESSION_USER);
return success(); return success();
} }
@DeleteMapping("cookies")
public void clearCookieSessionId(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
cookie.setMaxAge(0);
cookie.setValue(null);
response.addCookie(cookie);
}
response.setStatus(HttpStatus.SC_OK);
}
@Operation(summary = "getOauth2Provider", description = "GET_OAUTH2_PROVIDER")
@GetMapping("oauth2-provider")
public Result<List<OAuth2Configuration.OAuth2ClientProperties>> oauth2Provider() {
if (oAuth2Configuration == null) {
return Result.success(new ArrayList<>());
}
Collection<OAuth2Configuration.OAuth2ClientProperties> values = oAuth2Configuration.getProvider().values();
List<OAuth2Configuration.OAuth2ClientProperties> providers = values.stream().map(e -> {
OAuth2Configuration.OAuth2ClientProperties oAuth2ClientProperties =
new OAuth2Configuration.OAuth2ClientProperties();
oAuth2ClientProperties.setAuthorizationUri(e.getAuthorizationUri());
oAuth2ClientProperties.setRedirectUri(e.getRedirectUri());
oAuth2ClientProperties.setClientId(e.getClientId());
oAuth2ClientProperties.setProvider(e.getProvider());
oAuth2ClientProperties.setIconUri(e.getIconUri());
return oAuth2ClientProperties;
}).collect(Collectors.toList());
return Result.success(providers);
}
@SneakyThrows
@Operation(summary = "redirectToOauth2", description = "REDIRECT_TO_OAUTH2_LOGIN")
@GetMapping("redirect/login/oauth2")
public void loginByAuth2(@RequestParam String code, @RequestParam String provider,
HttpServletRequest request, HttpServletResponse response) {
OAuth2Configuration.OAuth2ClientProperties oAuth2ClientProperties =
oAuth2Configuration.getProvider().get(provider);
try {
Map<String, String> tokenRequestHeader = new HashMap<>();
tokenRequestHeader.put("Accept", "application/json");
Map<String, Object> requestBody = new HashMap<>(16);
requestBody.put("client_secret", oAuth2ClientProperties.getClientSecret());
HashMap<String, Object> requestParamsMap = new HashMap<>();
requestParamsMap.put("client_id", oAuth2ClientProperties.getClientId());
requestParamsMap.put("code", code);
requestParamsMap.put("grant_type", "authorization_code");
requestParamsMap.put("redirect_uri",
String.format("%s?provider=%s", oAuth2ClientProperties.getRedirectUri(), provider));
String tokenJsonStr = OkHttpUtils.post(oAuth2ClientProperties.getTokenUri(), tokenRequestHeader,
requestParamsMap, requestBody);
String accessToken = JSONUtils.getNodeString(tokenJsonStr, "access_token");
Map<String, String> userInfoRequestHeaders = new HashMap<>();
userInfoRequestHeaders.put("Accept", "application/json");
Map<String, Object> userInfoQueryMap = new HashMap<>();
userInfoQueryMap.put("access_token", accessToken);
userInfoRequestHeaders.put("Authorization", "Bearer " + accessToken);
String userInfoJsonStr =
OkHttpUtils.get(oAuth2ClientProperties.getUserInfoUri(), userInfoRequestHeaders, userInfoQueryMap);
String username = JSONUtils.getNodeString(userInfoJsonStr, "login");
User user = usersService.getUserByUserName(username);
if (user == null) {
user = usersService.createUser(username, null, null, 0, null, null, 1);
}
String sessionId = sessionService.createSession(user, null);
if (sessionId == null) {
log.error("Failed to create session, userName:{}.", user.getUserName());
}
response.setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
response.sendRedirect(String.format("%s?sessionId=%s&authType=%s", oAuth2ClientProperties.getCallbackUrl(),
sessionId, "oauth2"));
} catch (Exception ex) {
log.error(ex.getMessage(), ex);
response.setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
response.sendRedirect(String.format("%s?authType=%s&error=%s", oAuth2ClientProperties.getCallbackUrl(),
"oauth2", "oauth2 auth error"));
}
}
} }

23
dolphinscheduler-api/src/main/resources/application.yaml

@ -178,6 +178,29 @@ security:
# jks file absolute path && password # jks file absolute path && password
trust-store: "/ldapkeystore.jks" trust-store: "/ldapkeystore.jks"
trust-store-password: "password" trust-store-password: "password"
oauth2:
enable: false
provider:
github:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: github
google:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: google
# Override by profile # Override by profile

69
dolphinscheduler-api/src/test/java/org/apache/dolphinscheduler/api/controller/LoginControllerTest.java

@ -17,6 +17,8 @@
package org.apache.dolphinscheduler.api.controller; package org.apache.dolphinscheduler.api.controller;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -25,14 +27,22 @@ import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.utils.Result; import org.apache.dolphinscheduler.api.utils.Result;
import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.common.constants.Constants;
import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.common.utils.JSONUtils;
import org.apache.dolphinscheduler.common.utils.OkHttpUtils;
import org.apache.http.HttpStatus;
import java.util.Map; import java.util.Map;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@ -79,4 +89,63 @@ public class LoginControllerTest extends AbstractControllerTest {
Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue()); Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue());
logger.info(mvcResult.getResponse().getContentAsString()); logger.info(mvcResult.getResponse().getContentAsString());
} }
@Test
void testClearCookie() throws Exception {
MvcResult mvcResult = mockMvc.perform(delete("/cookies")
.header("sessionId", sessionId)
.cookie(new Cookie("sessionId", sessionId)))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
Cookie[] cookies = response.getCookies();
for (Cookie cookie : cookies) {
Assertions.assertEquals(0, cookie.getMaxAge());
Assertions.assertNull(cookie.getValue());
}
}
@Test
void testGetOauth2Provider() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2-provider"))
.andExpect(status().isOk())
.andReturn();
Result result = JSONUtils.parseObject(mvcResult.getResponse().getContentAsString(), Result.class);
Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue());
}
@Test
void testOauth2Redirect() throws Exception {
String tokenResult = "{\"access_token\":\"test-token\"}";
String userInfoResult = "{\"login\":\"username\"}";
MockedStatic<OkHttpUtils> okHttpUtilsMockedStatic = Mockito.mockStatic(OkHttpUtils.class);
okHttpUtilsMockedStatic
.when(() -> OkHttpUtils.post(Mockito.notNull(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenReturn(tokenResult);
okHttpUtilsMockedStatic.when(() -> OkHttpUtils.get(Mockito.notNull(), Mockito.any(), Mockito.any()))
.thenReturn(userInfoResult);
MvcResult mvcResult = mockMvc.perform(get("/redirect/login/oauth2?code=test&provider=github"))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
Assertions.assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatus());
String redirectedUrl = response.getRedirectedUrl();
Assertions.assertTrue(redirectedUrl != null && redirectedUrl.contains("sessionId"));
okHttpUtilsMockedStatic.close();
}
@Test
void testOauth2RedirectError() throws Exception {
MockedStatic<OkHttpUtils> okHttpUtilsMockedStatic = Mockito.mockStatic(OkHttpUtils.class);
okHttpUtilsMockedStatic.when(() -> OkHttpUtils.post(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
.thenThrow(new RuntimeException("oauth error"));
MvcResult mvcResult = mockMvc.perform(get("/redirect/login/oauth2?code=test&provider=github"))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
Assertions.assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatus());
String redirectedUrl = response.getRedirectedUrl();
Assertions.assertTrue(redirectedUrl != null && redirectedUrl.contains("error"));
okHttpUtilsMockedStatic.close();
}
} }

49
dolphinscheduler-api/src/test/resources/application.yaml

@ -62,4 +62,51 @@ api:
connect-timeout: 0 connect-timeout: 0
# Close each active connection of socket server if python program not active after x milliseconds. Define value is # Close each active connection of socket server if python program not active after x milliseconds. Define value is
# (0 = infinite), and socket server would never close even though no requests accept # (0 = infinite), and socket server would never close even though no requests accept
read-timeout: 0 read-timeout: 0
security:
authentication:
# Authentication types (supported types: PASSWORD,LDAP,CASDOOR_SSO)
type: PASSWORD
# IF you set type `LDAP`, below config will be effective
ldap:
# ldap server config
urls: ldap://ldap.forumsys.com:389/
base-dn: dc=example,dc=com
username: cn=read-only-admin,dc=example,dc=com
password: password
user:
# admin userId when you use LDAP login
admin: read-only-admin
identity-attribute: uid
email-attribute: mail
# action when ldap user is not exist (supported types: CREATE,DENY)
not-exist-action: CREATE
ssl:
enable: false
# jks file absolute path && password
trust-store: "/ldapkeystore.jks"
trust-store-password: "password"
oauth2:
enable: true
provider:
github:
authorizationUri: http://oauth2-test
redirectUri: http://oauth2-test
clientId: ""
clientSecret: ""
tokenUri: http://oauth2-token-url-test
userInfoUri: http://oauth2-user-info-url-test
callbackUrl: ""
iconUri: ""
provider: github
google:
authorizationUri: ""
redirectUri: ""
clientId: ""
clientSecret: ""
tokenUri: ""
userInfoUri: ""
callbackUrl: ""
iconUri: ""
provider: google

3
dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java

@ -38,7 +38,7 @@ public class OkHttpUtils {
private static final OkHttpClient CLIENT = new OkHttpClient.Builder() private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.MINUTES) // connect timeout .connectTimeout(5, TimeUnit.MINUTES) // connect timeout
.writeTimeout(5, TimeUnit.MINUTES) // write timeout .writeTimeout(5, TimeUnit.MINUTES) // write timeout
.readTimeout(5, TimeUnit.MINUTES) // read timeout .readTimeout(5, TimeUnit.MINUTES)
.build(); .build();
public static @NonNull String get(@NonNull String url, public static @NonNull String get(@NonNull String url,
@ -59,6 +59,7 @@ public class OkHttpUtils {
@Nullable Map<String, Object> requestBodyMap) throws IOException { @Nullable Map<String, Object> requestBodyMap) throws IOException {
String finalUrl = addUrlParams(requestParamsMap, url); String finalUrl = addUrlParams(requestParamsMap, url);
Request.Builder requestBuilder = new Request.Builder().url(finalUrl); Request.Builder requestBuilder = new Request.Builder().url(finalUrl);
addHeader(httpHeaders, requestBuilder);
if (requestBodyMap != null) { if (requestBodyMap != null) {
requestBuilder = requestBuilder.post(RequestBody.create(MediaType.parse("application/json"), requestBuilder = requestBuilder.post(RequestBody.create(MediaType.parse("application/json"),
JSONUtils.toJsonString(requestBodyMap))); JSONUtils.toJsonString(requestBodyMap)));

26
dolphinscheduler-standalone-server/src/main/resources/application.yaml

@ -111,6 +111,32 @@ security:
# jks file absolute path && password # jks file absolute path && password
trust-store: "/ldapkeystore.jks" trust-store: "/ldapkeystore.jks"
trust-store-password: "" trust-store-password: ""
oauth2:
enable: false
provider:
github:
authorizationUri: "https://github.com/login/oauth/authorize"
redirectUri: "http://localhost:12345/dolphinscheduler/redirect/login/oauth2"
clientId: ""
clientSecret: ""
tokenUri: "https://github.com/login/oauth/access_token"
userInfoUri: "https://api.github.com/user"
callbackUrl: "http://localhost:5173/login"
iconUri: ""
provider: github
gitee:
authorizationUri: "https://gitee.com/oauth/authorize"
redirectUri: "http://127.0.0.1:12345/dolphinscheduler/redirect/login/oauth2"
clientId: ""
clientSecret: ""
tokenUri: "https://gitee.com/oauth/token?grant_type=authorization_code"
userInfoUri: "https://gitee.com/api/v5/user"
callbackUrl: "http://127.0.0.1:5173/login"
iconUri: ""
provider: gitee
master: master:

1
dolphinscheduler-ui/src/locales/en_US/login.ts

@ -22,5 +22,6 @@ export default {
userPassword: 'Password', userPassword: 'Password',
userPassword_tips: 'Please enter your password', userPassword_tips: 'Please enter your password',
login: 'Login', login: 'Login',
loginWithOAuth2: 'login with OAuth2',
ssoLogin: 'SSO Login' ssoLogin: 'SSO Login'
} }

1
dolphinscheduler-ui/src/locales/zh_CN/login.ts

@ -22,5 +22,6 @@ export default {
userPassword: '密码', userPassword: '密码',
userPassword_tips: '请输入密码', userPassword_tips: '请输入密码',
login: '登录', login: '登录',
loginWithOAuth2: '通过OAuth2登录',
ssoLogin: '单点登录' ssoLogin: '单点登录'
} }

14
dolphinscheduler-ui/src/service/modules/login/index.ts

@ -32,3 +32,17 @@ export function ssoLoginUrl(): any {
method: 'get' method: 'get'
}) })
} }
export function getOauth2Provider(): any {
return axios({
url: '/oauth2-provider',
method: 'get',
})
}
export function clearCookie(): any {
return axios({
url: '/cookies',
method: 'delete',
})
}

10
dolphinscheduler-ui/src/service/modules/login/types.ts

@ -25,4 +25,12 @@ interface LoginRes {
sessionId: string sessionId: string
} }
export { LoginReq, LoginRes } interface OAuth2Provider {
clientId: string,
redirectUri: string,
provider: string,
authorizationUri: string,
iconUri: string
}
export { LoginReq, LoginRes, OAuth2Provider }

7
dolphinscheduler-ui/src/views/login/index.module.scss

@ -53,5 +53,10 @@
.form-model { .form-model {
padding: 30px 20px; padding: 30px 20px;
} }
.oauth2-provider {
margin-top: 10px;
margin-bottom: 30px;
}
} }
} }

24
dolphinscheduler-ui/src/views/login/index.tsx

@ -29,7 +29,10 @@ import {
NSwitch, NSwitch,
NForm, NForm,
NFormItem, NFormItem,
useMessage useMessage,
NSpace,
NDivider,
NImage
} from 'naive-ui' } from 'naive-ui'
import { useForm } from './use-form' import { useForm } from './use-form'
import { useTranslate } from './use-translate' import { useTranslate } from './use-translate'
@ -38,15 +41,15 @@ import { useLocalesStore } from '@/store/locales/locales'
import { useThemeStore } from '@/store/theme/theme' import { useThemeStore } from '@/store/theme/theme'
import cookies from 'js-cookie' import cookies from 'js-cookie'
import { ssoLoginUrl } from '@/service/modules/login' import { ssoLoginUrl } from '@/service/modules/login'
import type { OAuth2Provider } from '@/service/modules/login/types'
const login = defineComponent({ const login = defineComponent({
name: 'login', name: 'login',
setup() { setup() {
window.$message = useMessage() window.$message = useMessage()
const { state, t, locale } = useForm() const { state, t, locale } = useForm()
const { handleChange } = useTranslate(locale) const { handleChange } = useTranslate(locale)
const { handleLogin } = useLogin(state) const { handleLogin, handleGetOAuth2Provider, oauth2Providers, gotoOAuth2Page, handleRedirect } = useLogin(state)
const localesStore = useLocalesStore() const localesStore = useLocalesStore()
const themeStore = useThemeStore() const themeStore = useThemeStore()
@ -73,15 +76,19 @@ const login = defineComponent({
} else { } else {
state.loginForm.ssoLoginUrl = '' state.loginForm.ssoLoginUrl = ''
} }
handleRedirect()
}) })
handleGetOAuth2Provider()
return { return {
t, t,
handleChange, handleChange,
handleLogin, handleLogin,
...toRefs(state), ...toRefs(state),
localesStore, localesStore,
trim trim,
oauth2Providers,
gotoOAuth2Page
} }
}, },
render() { render() {
@ -170,6 +177,15 @@ const login = defineComponent({
</NButton> </NButton>
</a> </a>
</div> </div>
{this.oauth2Providers.length > 0 && <NDivider >
{this.t('login.loginWithOAuth2')}
</NDivider>}
<NSpace class={styles['oauth2-provider']} justify="center">
{this.oauth2Providers?.map((e: OAuth2Provider) => {
return (e.iconUri ? <div onClick={() => this.gotoOAuth2Page(e)}><NImage preview-disabled width="30" src={e.iconUri}></NImage> </div> : <NButton onClick={() => this.gotoOAuth2Page(e)}>{e.provider}</NButton>)
})}
</NSpace>
</div> </div>
</div> </div>
) )

50
dolphinscheduler-ui/src/views/login/use-login.ts

@ -15,24 +15,25 @@
* limitations under the License. * limitations under the License.
*/ */
import { useRouter } from 'vue-router' import { useRouter,useRoute } from 'vue-router'
import { login } from '@/service/modules/login' import { clearCookie, getOauth2Provider, login } from '@/service/modules/login'
import { getUserInfo } from '@/service/modules/users' import { getUserInfo } from '@/service/modules/users'
import { useUserStore } from '@/store/user/user' import { useUserStore } from '@/store/user/user'
import type { Router } from 'vue-router' import type { Router } from 'vue-router'
import type { LoginRes } from '@/service/modules/login/types' import type { LoginRes, OAuth2Provider } from '@/service/modules/login/types'
import type { UserInfoRes } from '@/service/modules/users/types' import type { UserInfoRes } from '@/service/modules/users/types'
import { useRouteStore } from '@/store/route/route' import { useRouteStore } from '@/store/route/route'
import { useTimezoneStore } from '@/store/timezone/timezone' import { useTimezoneStore } from '@/store/timezone/timezone'
import cookies from 'js-cookie' import cookies from 'js-cookie'
import { queryBaseDir } from '@/service/modules/resources' import { queryBaseDir } from '@/service/modules/resources'
import { ref } from 'vue'
export function useLogin(state: any) { export function useLogin(state: any) {
const router: Router = useRouter() const router: Router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const routeStore = useRouteStore() const routeStore = useRouteStore()
const timezoneStore = useTimezoneStore() const timezoneStore = useTimezoneStore()
const route = useRoute()
const handleLogin = () => { const handleLogin = () => {
state.loginFormRef.validate(async (valid: any) => { state.loginFormRef.validate(async (valid: any) => {
if (!valid) { if (!valid) {
@ -63,7 +64,46 @@ export function useLogin(state: any) {
}) })
} }
const handleGetOAuth2Provider = () => {
getOauth2Provider().then((res: Array<OAuth2Provider> | []) => {
oauth2Providers.value = res
})
}
const oauth2Providers = ref<Array<OAuth2Provider> | []>([])
const gotoOAuth2Page = async (oauth2Provider: OAuth2Provider) => {
await clearCookie()
window.location.href = `${oauth2Provider.authorizationUri}?client_id=${oauth2Provider.clientId}` +
`&response_type=code&redirect_uri=${oauth2Provider.redirectUri}?provider=${oauth2Provider.provider}`
}
const handleRedirect = async () => {
const authType = route.query.authType
if (authType && authType === 'oauth2') {
const sessionId = route.query.sessionId
if (sessionId) {
cookies.set('sessionId', String(sessionId), { path: '/' })
const userInfoRes: UserInfoRes = await getUserInfo()
await userStore.setUserInfo(userInfoRes)
const timezone = userInfoRes.timeZone ? userInfoRes.timeZone : 'UTC'
await timezoneStore.setTimezone(timezone)
router.push('home')
}
const error = route.query.error
if (error) {
window.$message.error(error)
}
}
}
return { return {
handleLogin handleLogin,
handleGetOAuth2Provider,
gotoOAuth2Page,
oauth2Providers,
handleRedirect
} }
} }

Loading…
Cancel
Save