[Feature][Authentication] support oauth2 login (#14743)
* support oauth2 login --------- Co-authored-by: Eric Gao <ericgao.apache@gmail.com>
This commit is contained in:
parent
502bd36fde
commit
70731a1617
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -30,6 +30,29 @@ security:
|
|||
# jks file absolute path && password
|
||||
trust-store: "/ldapkeystore.jks"
|
||||
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)
|
||||
|
|
@ -110,3 +133,66 @@ casdoor:
|
|||
redirect-url: http://localhost:5173/login
|
||||
```
|
||||
|
||||
## OAuth2
|
||||
|
||||
Dolphinscheduler can support multiple OAuth2 providers.
|
||||
|
||||
### Step1. Create Client Credentials
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -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
|
||||
trust-store: "/ldapkeystore.jks"
|
||||
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)
|
||||
|
|
@ -106,3 +129,66 @@ casdoor:
|
|||
redirect-url: http://localhost:5173/login
|
||||
```
|
||||
|
||||
## 通过OAuth2授权认证登录
|
||||
|
||||
dolphinscheduler可以同时支持多种OAuth2的provider,只需要在配置文件中打开Oauth2的开关并进行简单的配置即可。
|
||||
|
||||
### 步骤1. 获取OAuth2客户端凭据
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 步骤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登录
|
||||
|
||||

|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 323 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
|
|
@ -105,7 +105,8 @@ public class AppConfiguration implements WebMvcConfigurer {
|
|||
.addPathPatterns(LOGIN_INTERCEPTOR_PATH_PATTERN)
|
||||
.excludePathPatterns(LOGIN_PATH_PATTERN, REGISTER_PATH_PATTERN,
|
||||
"/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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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.exceptions.ApiException;
|
||||
import org.apache.dolphinscheduler.api.security.Authenticator;
|
||||
import org.apache.dolphinscheduler.api.security.impl.AbstractSsoAuthenticator;
|
||||
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.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.commons.lang3.StringUtils;
|
||||
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.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||
|
|
@ -63,6 +76,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
@Tag(name = "LOGIN_TAG")
|
||||
@RestController
|
||||
@RequestMapping("")
|
||||
@Slf4j
|
||||
public class LoginController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
|
|
@ -71,6 +85,12 @@ public class LoginController extends BaseController {
|
|||
@Autowired
|
||||
private Authenticator authenticator;
|
||||
|
||||
@Autowired(required = false)
|
||||
private OAuth2Configuration oAuth2Configuration;
|
||||
|
||||
@Autowired
|
||||
private UsersService usersService;
|
||||
|
||||
/**
|
||||
* login
|
||||
*
|
||||
|
|
@ -160,4 +180,84 @@ public class LoginController extends BaseController {
|
|||
request.removeAttribute(Constants.SESSION_USER);
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,29 @@ security:
|
|||
# jks file absolute path && password
|
||||
trust-store: "/ldapkeystore.jks"
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
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.result.MockMvcResultMatchers.content;
|
||||
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.common.constants.Constants;
|
||||
import org.apache.dolphinscheduler.common.utils.JSONUtils;
|
||||
import org.apache.dolphinscheduler.common.utils.OkHttpUtils;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
|
@ -79,4 +89,63 @@ public class LoginControllerTest extends AbstractControllerTest {
|
|||
Assertions.assertEquals(Status.SUCCESS.getCode(), result.getCode().intValue());
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,4 +62,51 @@ api:
|
|||
connect-timeout: 0
|
||||
# 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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public class OkHttpUtils {
|
|||
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.MINUTES) // connect timeout
|
||||
.writeTimeout(5, TimeUnit.MINUTES) // write timeout
|
||||
.readTimeout(5, TimeUnit.MINUTES) // read timeout
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
public static @NonNull String get(@NonNull String url,
|
||||
|
|
@ -59,6 +59,7 @@ public class OkHttpUtils {
|
|||
@Nullable Map<String, Object> requestBodyMap) throws IOException {
|
||||
String finalUrl = addUrlParams(requestParamsMap, url);
|
||||
Request.Builder requestBuilder = new Request.Builder().url(finalUrl);
|
||||
addHeader(httpHeaders, requestBuilder);
|
||||
if (requestBodyMap != null) {
|
||||
requestBuilder = requestBuilder.post(RequestBody.create(MediaType.parse("application/json"),
|
||||
JSONUtils.toJsonString(requestBodyMap)));
|
||||
|
|
|
|||
|
|
@ -111,6 +111,32 @@ security:
|
|||
# jks file absolute path && password
|
||||
trust-store: "/ldapkeystore.jks"
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ export default {
|
|||
userPassword: 'Password',
|
||||
userPassword_tips: 'Please enter your password',
|
||||
login: 'Login',
|
||||
loginWithOAuth2: 'login with OAuth2',
|
||||
ssoLogin: 'SSO Login'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ export default {
|
|||
userPassword: '密码',
|
||||
userPassword_tips: '请输入密码',
|
||||
login: '登录',
|
||||
loginWithOAuth2: '通过OAuth2登录',
|
||||
ssoLogin: '单点登录'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,17 @@ export function ssoLoginUrl(): any {
|
|||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getOauth2Provider(): any {
|
||||
return axios({
|
||||
url: '/oauth2-provider',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function clearCookie(): any {
|
||||
return axios({
|
||||
url: '/cookies',
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
|
@ -25,4 +25,12 @@ interface LoginRes {
|
|||
sessionId: string
|
||||
}
|
||||
|
||||
export { LoginReq, LoginRes }
|
||||
interface OAuth2Provider {
|
||||
clientId: string,
|
||||
redirectUri: string,
|
||||
provider: string,
|
||||
authorizationUri: string,
|
||||
iconUri: string
|
||||
}
|
||||
|
||||
export { LoginReq, LoginRes, OAuth2Provider }
|
||||
|
|
|
|||
|
|
@ -53,5 +53,10 @@
|
|||
.form-model {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.oauth2-provider {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,10 @@ import {
|
|||
NSwitch,
|
||||
NForm,
|
||||
NFormItem,
|
||||
useMessage
|
||||
useMessage,
|
||||
NSpace,
|
||||
NDivider,
|
||||
NImage
|
||||
} from 'naive-ui'
|
||||
import { useForm } from './use-form'
|
||||
import { useTranslate } from './use-translate'
|
||||
|
|
@ -38,15 +41,15 @@ import { useLocalesStore } from '@/store/locales/locales'
|
|||
import { useThemeStore } from '@/store/theme/theme'
|
||||
import cookies from 'js-cookie'
|
||||
import { ssoLoginUrl } from '@/service/modules/login'
|
||||
import type { OAuth2Provider } from '@/service/modules/login/types'
|
||||
|
||||
const login = defineComponent({
|
||||
name: 'login',
|
||||
setup() {
|
||||
window.$message = useMessage()
|
||||
|
||||
const { state, t, locale } = useForm()
|
||||
const { handleChange } = useTranslate(locale)
|
||||
const { handleLogin } = useLogin(state)
|
||||
const { handleLogin, handleGetOAuth2Provider, oauth2Providers, gotoOAuth2Page, handleRedirect } = useLogin(state)
|
||||
const localesStore = useLocalesStore()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
|
|
@ -73,15 +76,19 @@ const login = defineComponent({
|
|||
} else {
|
||||
state.loginForm.ssoLoginUrl = ''
|
||||
}
|
||||
handleRedirect()
|
||||
})
|
||||
|
||||
handleGetOAuth2Provider()
|
||||
return {
|
||||
t,
|
||||
handleChange,
|
||||
handleLogin,
|
||||
...toRefs(state),
|
||||
localesStore,
|
||||
trim
|
||||
trim,
|
||||
oauth2Providers,
|
||||
gotoOAuth2Page
|
||||
}
|
||||
},
|
||||
render() {
|
||||
|
|
@ -170,6 +177,15 @@ const login = defineComponent({
|
|||
</NButton>
|
||||
</a>
|
||||
</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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,24 +15,25 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
import { login } from '@/service/modules/login'
|
||||
import { useRouter,useRoute } from 'vue-router'
|
||||
import { clearCookie, getOauth2Provider, login } from '@/service/modules/login'
|
||||
import { getUserInfo } from '@/service/modules/users'
|
||||
import { useUserStore } from '@/store/user/user'
|
||||
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 { useRouteStore } from '@/store/route/route'
|
||||
import { useTimezoneStore } from '@/store/timezone/timezone'
|
||||
import cookies from 'js-cookie'
|
||||
import { queryBaseDir } from '@/service/modules/resources'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useLogin(state: any) {
|
||||
const router: Router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const routeStore = useRouteStore()
|
||||
const timezoneStore = useTimezoneStore()
|
||||
|
||||
const route = useRoute()
|
||||
const handleLogin = () => {
|
||||
state.loginFormRef.validate(async (valid: any) => {
|
||||
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 {
|
||||
handleLogin
|
||||
handleLogin,
|
||||
handleGetOAuth2Provider,
|
||||
gotoOAuth2Page,
|
||||
oauth2Providers,
|
||||
handleRedirect
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue