feat(系统设置): 单点登录支持OAuth2.0
--story=1010427 --user=李玉号 【系统设置】单点登录支持OAuth2.0 https://www.tapd.cn/55049933/s/1294825
This commit is contained in:
parent
f88e2f69cb
commit
00fda9cbcf
|
@ -40,6 +40,14 @@ public class SSOController {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("callback/oauth2")
|
||||||
|
@MsAuditLog(module = OperLogModule.AUTH_TITLE, type = OperLogConstants.LOGIN, title = "登录")
|
||||||
|
public Rendering callbackOauth(@RequestParam("code") String code, @RequestParam("state") String authId, WebSession session, Locale locale) throws Exception {
|
||||||
|
Optional<SessionUser> sessionUser = ssoService.exchangeOauth2Token(code, authId, session, locale);
|
||||||
|
return Rendering.redirectTo("/#/?_token=" + CodingUtil.base64Encoding(session.getId()) + "&_csrf=" + sessionUser.get().getCsrfToken())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/callback/cas/{authId}")
|
@GetMapping("/callback/cas/{authId}")
|
||||||
@MsAuditLog(module = OperLogModule.AUTH_TITLE, type = OperLogConstants.LOGIN, title = "登录")
|
@MsAuditLog(module = OperLogModule.AUTH_TITLE, type = OperLogConstants.LOGIN, title = "登录")
|
||||||
public Rendering casCallback(@RequestParam("ticket") String ticket, @PathVariable("authId") String authId, WebSession session, Locale locale) throws Exception {
|
public Rendering casCallback(@RequestParam("ticket") String ticket, @PathVariable("authId") String authId, WebSession session, Locale locale) throws Exception {
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
package io.metersphere.gateway.service;
|
package io.metersphere.gateway.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import io.metersphere.base.domain.AuthSource;
|
import io.metersphere.base.domain.AuthSource;
|
||||||
import io.metersphere.base.domain.User;
|
import io.metersphere.base.domain.User;
|
||||||
import io.metersphere.commons.constants.SessionConstants;
|
|
||||||
import io.metersphere.commons.exception.MSException;
|
import io.metersphere.commons.exception.MSException;
|
||||||
import io.metersphere.commons.user.SessionUser;
|
import io.metersphere.commons.user.SessionUser;
|
||||||
import io.metersphere.commons.utils.CodingUtil;
|
import io.metersphere.commons.utils.*;
|
||||||
import io.metersphere.commons.utils.IOUtils;
|
import io.metersphere.i18n.Translator;
|
||||||
import io.metersphere.commons.utils.JSON;
|
|
||||||
import io.metersphere.commons.utils.SessionUtils;
|
|
||||||
import io.metersphere.request.LoginRequest;
|
import io.metersphere.request.LoginRequest;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
|
@ -43,9 +41,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Locale;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
||||||
|
@ -229,4 +225,120 @@ public class SSOService {
|
||||||
SessionUtils.kickOutUser(name);
|
SessionUtils.kickOutUser(name);
|
||||||
stringRedisTemplate.delete(ticket);
|
stringRedisTemplate.delete(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<SessionUser> exchangeOauth2Token(String code, String authId, WebSession session, Locale locale) throws Exception {
|
||||||
|
AuthSource authSource = authSourceService.getAuthSource(authId);
|
||||||
|
Map<String, String> config = JSON.parseObject(authSource.getConfiguration(), new TypeReference<HashMap<String, String>>() {});
|
||||||
|
String url = config.get("tokenUrl")
|
||||||
|
+ "?client_id=" + config.get("clientId")
|
||||||
|
+ "&client_secret=" + config.get("secret")
|
||||||
|
+ "&redirect_uri=" + config.get("redirectUrl")
|
||||||
|
+ "&code=" + code
|
||||||
|
+ "&grant_type=authorization_code";
|
||||||
|
|
||||||
|
Map<String, String> resultObj = null;
|
||||||
|
try {
|
||||||
|
RestTemplate restTemplate = getRestTemplateIgnoreSSL();
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
HttpEntity<String> param = new HttpEntity<>(headers);
|
||||||
|
ResponseEntity<String> response = restTemplate.postForEntity(url, param, String.class);
|
||||||
|
String content = response.getBody();
|
||||||
|
resultObj = JSON.parseObject(content, new TypeReference<HashMap<String, String>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.error("fail to get access_token", e);
|
||||||
|
MSException.throwException("fail to get access_token!");
|
||||||
|
}
|
||||||
|
|
||||||
|
String accessToken = resultObj.get("access_token");
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(accessToken)) {
|
||||||
|
MSException.throwException("access_token is empty!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return doOauth2Login(authSource, accessToken, session, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<SessionUser> doOauth2Login(AuthSource authSource, String accessToken, WebSession session, Locale locale) throws Exception {
|
||||||
|
Map<String, String> oauth2Config = null;
|
||||||
|
Map<String, String> resultObj = null;
|
||||||
|
try {
|
||||||
|
oauth2Config = JSON.parseObject(authSource.getConfiguration(), new TypeReference<HashMap<String, String>>() {});
|
||||||
|
String userInfoUrl = oauth2Config.get("userInfoUrl");
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Authorization", "Bearer " + accessToken);
|
||||||
|
RestTemplate restTemplate = getRestTemplateIgnoreSSL();
|
||||||
|
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(headers);
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(userInfoUrl, HttpMethod.GET, httpEntity, String.class);
|
||||||
|
resultObj = JSON.parseObject(response.getBody(), new TypeReference<HashMap<String, String>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.error("fail to get user info", e);
|
||||||
|
MSException.throwException("fail to get user info!");
|
||||||
|
}
|
||||||
|
|
||||||
|
String attrMapping = oauth2Config.get("mapping");
|
||||||
|
Map<String, String> mapping = this.getOauth2AttrMapping(attrMapping);
|
||||||
|
|
||||||
|
String userid = resultObj.get(mapping.get("userid"));
|
||||||
|
String username = resultObj.get(mapping.get("username"));
|
||||||
|
String email = resultObj.get(mapping.get("email"));
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(userid)) {
|
||||||
|
MSException.throwException("userid is empty!");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(username)) {
|
||||||
|
username = userid;
|
||||||
|
}
|
||||||
|
if (!StringUtils.contains(email, "@")) {
|
||||||
|
email = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
User u = userLoginService.selectUser(userid, email);
|
||||||
|
if (u == null) {
|
||||||
|
//
|
||||||
|
User user = new User();
|
||||||
|
user.setId(userid);
|
||||||
|
user.setName(username);
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setSource(authSource.getType());
|
||||||
|
userLoginService.createOssUser(user);
|
||||||
|
} else {
|
||||||
|
if (StringUtils.equals(u.getEmail(), email) && !StringUtils.equals(u.getId(), userid)) {
|
||||||
|
MSException.throwException("email already exists!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
|
loginRequest.setUsername(userid);
|
||||||
|
loginRequest.setPassword("nothing");
|
||||||
|
loginRequest.setAuthenticate(authSource.getType());
|
||||||
|
Optional<SessionUser> userOptional = userLoginService.login(loginRequest, session, locale);
|
||||||
|
session.getAttributes().put("authenticate", authSource.getType());
|
||||||
|
session.getAttributes().put("authId", authSource.getId());
|
||||||
|
return userOptional;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getOauth2AttrMapping(String mappingStr) {
|
||||||
|
Map<String, String> mapping = new HashMap<>();
|
||||||
|
try {
|
||||||
|
mapping = JSON.parseObject(mappingStr, new TypeReference<HashMap<String, String>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.error("get oauth2 mapping config error!", e);
|
||||||
|
MSException.throwException(Translator.get("oauth_mapping_config_error"));
|
||||||
|
}
|
||||||
|
String userid = mapping.get("userid");
|
||||||
|
if (StringUtils.isBlank(userid)) {
|
||||||
|
MSException.throwException(Translator.get("oauth_mapping_value_null") + ": userid");
|
||||||
|
}
|
||||||
|
String username = mapping.get("username");
|
||||||
|
if (StringUtils.isBlank(username)) {
|
||||||
|
MSException.throwException(Translator.get("oauth_mapping_value_null") + ": username");
|
||||||
|
}
|
||||||
|
String email = mapping.get("email");
|
||||||
|
if (StringUtils.isBlank(email)) {
|
||||||
|
MSException.throwException(Translator.get("oauth_mapping_value_null") + ": email");
|
||||||
|
}
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ public class UserLoginService {
|
||||||
switch (request.getAuthenticate()) {
|
switch (request.getAuthenticate()) {
|
||||||
case "OIDC":
|
case "OIDC":
|
||||||
case "CAS":
|
case "CAS":
|
||||||
|
case "OAuth2":
|
||||||
userDTO = loginSsoMode(request.getUsername(), request.getAuthenticate());
|
userDTO = loginSsoMode(request.getUsername(), request.getAuthenticate());
|
||||||
break;
|
break;
|
||||||
case "LDAP":
|
case "LDAP":
|
||||||
|
|
|
@ -254,7 +254,7 @@ export default {
|
||||||
|
|
||||||
sessionStorage.setItem('redirectUrl', redirectUrl);
|
sessionStorage.setItem('redirectUrl', redirectUrl);
|
||||||
sessionStorage.setItem('lastUser', getCurrentUserId());
|
sessionStorage.setItem('lastUser', getCurrentUserId());
|
||||||
this.$router.push({ name: "login_redirect", path: redirectUrl || '/', query: this.otherQuery});
|
this.$router.push({name: "login_redirect", path: redirectUrl || '/', query: this.otherQuery});
|
||||||
},
|
},
|
||||||
doLogin() {
|
doLogin() {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
@ -310,6 +310,14 @@ export default {
|
||||||
url = config.authUrl + "?client_id=" + config.clientId + "&redirect_uri=" + redirectUrl +
|
url = config.authUrl + "?client_id=" + config.clientId + "&redirect_uri=" + redirectUrl +
|
||||||
"&response_type=code&scope=openid+profile+email&state=" + authId;
|
"&response_type=code&scope=openid+profile+email&state=" + authId;
|
||||||
}
|
}
|
||||||
|
if (source.type === 'OAuth2') {
|
||||||
|
url = config.authUrl
|
||||||
|
+ "?client_id=" + config.clientId
|
||||||
|
+ "&scope=" + config.scope
|
||||||
|
+ "&response_type=code"
|
||||||
|
+ "&redirect_uri=" + redirectUrl
|
||||||
|
+ "&state=" + authId;
|
||||||
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,8 @@ login_fail_ou_error=Login failed, please check the user OU
|
||||||
login_fail_filter_error=Login failed, please check the user filter
|
login_fail_filter_error=Login failed, please check the user filter
|
||||||
check_ldap_mapping=Check LDAP attribute mapping
|
check_ldap_mapping=Check LDAP attribute mapping
|
||||||
ldap_mapping_value_null=LDAP user attribute mapping field is empty
|
ldap_mapping_value_null=LDAP user attribute mapping field is empty
|
||||||
|
oauth_mapping_config_error=OAuth2 attribute mapping misconfiguration
|
||||||
|
oauth_mapping_value_null=OAuth2 user attribute mapping field is empty
|
||||||
#quota
|
#quota
|
||||||
quota_project_excess_ws_api=The total number of interface tests for a project cannot exceed the workspace quota
|
quota_project_excess_ws_api=The total number of interface tests for a project cannot exceed the workspace quota
|
||||||
quota_project_excess_ws_performance=The total number of performance tests for a project cannot exceed the workspace quota
|
quota_project_excess_ws_performance=The total number of performance tests for a project cannot exceed the workspace quota
|
||||||
|
|
|
@ -181,6 +181,8 @@ login_fail_ou_error=登录失败,请检查用户OU
|
||||||
login_fail_filter_error=登录失败,请检查用户过滤器
|
login_fail_filter_error=登录失败,请检查用户过滤器
|
||||||
check_ldap_mapping=检查LDAP属性映射
|
check_ldap_mapping=检查LDAP属性映射
|
||||||
ldap_mapping_value_null=LDAP用户属性映射字段为空值
|
ldap_mapping_value_null=LDAP用户属性映射字段为空值
|
||||||
|
oauth_mapping_config_error=OAuth2属性映射配置错误
|
||||||
|
oauth_mapping_value_null=OAuth2用户属性映射字段为空值
|
||||||
#quota
|
#quota
|
||||||
quota_project_excess_ws_api=项目的接口测试数量总和不能超过工作空间的配额
|
quota_project_excess_ws_api=项目的接口测试数量总和不能超过工作空间的配额
|
||||||
quota_project_excess_ws_performance=项目的性能测试数量总和不能超过工作空间的配额
|
quota_project_excess_ws_performance=项目的性能测试数量总和不能超过工作空间的配额
|
||||||
|
|
|
@ -181,6 +181,8 @@ login_fail_ou_error=登錄失敗,請檢查用戶OU
|
||||||
login_fail_filter_error=登錄失敗,請檢查用戶過濾器
|
login_fail_filter_error=登錄失敗,請檢查用戶過濾器
|
||||||
check_ldap_mapping=檢查LDAP屬性映射
|
check_ldap_mapping=檢查LDAP屬性映射
|
||||||
ldap_mapping_value_null=LDAP用戶屬性映射字段為空值
|
ldap_mapping_value_null=LDAP用戶屬性映射字段為空值
|
||||||
|
oauth_mapping_config_error=OAuth2屬性映射配置錯誤
|
||||||
|
oauth_mapping_value_null=OAuth2用戶屬性映射字段為空值
|
||||||
#quota
|
#quota
|
||||||
quota_project_excess_ws_api=項目的接口測試數量總和不能超過工作空間的配額
|
quota_project_excess_ws_api=項目的接口測試數量總和不能超過工作空間的配額
|
||||||
quota_project_excess_ws_performance=項目的性能測試數量總和不能超過工作空間的配額
|
quota_project_excess_ws_performance=項目的性能測試數量總和不能超過工作空間的配額
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
<span v-if="scope.row.type === 'CAS'">CAS</span>
|
<span v-if="scope.row.type === 'CAS'">CAS</span>
|
||||||
<span v-if="scope.row.type === 'OIDC'">OIDC</span>
|
<span v-if="scope.row.type === 'OIDC'">OIDC</span>
|
||||||
|
<span v-if="scope.row.type === 'OAuth2'">OAuth2</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" :label="$t('test_resource_pool.enable_disable')">
|
<el-table-column prop="status" :label="$t('test_resource_pool.enable_disable')">
|
||||||
|
@ -74,6 +75,7 @@
|
||||||
@change="changeAuthType(form.type)">
|
@change="changeAuthType(form.type)">
|
||||||
<el-option key="CAS" value="CAS" label="CAS"/>
|
<el-option key="CAS" value="CAS" label="CAS"/>
|
||||||
<el-option key="OIDC" value="OIDC" label="OIDC"/>
|
<el-option key="OIDC" value="OIDC" label="OIDC"/>
|
||||||
|
<el-option key="OAuth2" value="OAuth2" label="OAuth2"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
@ -176,6 +178,76 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="node-line" v-if="form.type === 'OAuth2'">
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Auth Endpoint"
|
||||||
|
:rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.authUrl"
|
||||||
|
placeholder="eg: http://example.com/login/oauth/authorize"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Token Endpoint"
|
||||||
|
:rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.tokenUrl"
|
||||||
|
placeholder="eg: https://example.com/login/oauth/access_token"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Userinfo Endpoint"
|
||||||
|
:rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.userInfoUrl"
|
||||||
|
placeholder="eg: https://example.com/user"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Redirect URL"
|
||||||
|
:rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.redirectUrl"
|
||||||
|
placeholder="eg: http://<metersphere-endpoint>/sso/callback/oauth2"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Client ID"
|
||||||
|
:rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.clientId" placeholder="eg: metersphere"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Secret"
|
||||||
|
:rules="requiredRules">
|
||||||
|
<el-input type="password" v-model="form.configuration.secret" show-password autocomplete="new-password"
|
||||||
|
placeholder="oauth2 client secret"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Scope" :rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.scope"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col>
|
||||||
|
<el-form-item label="Property Mapping" :rules="requiredRules">
|
||||||
|
<el-input v-model="form.configuration.mapping"
|
||||||
|
:placeholder="mappingTip"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template v-slot:footer>
|
<template v-slot:footer>
|
||||||
|
@ -211,6 +283,7 @@ export default {
|
||||||
dialogLoading: false,
|
dialogLoading: false,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
infoList: [],
|
infoList: [],
|
||||||
|
mappingTip: '{"userid": "uid", "username": "name", "email": "email"}',
|
||||||
queryPath: "authsource/list",
|
queryPath: "authsource/list",
|
||||||
condition: {},
|
condition: {},
|
||||||
items: [],
|
items: [],
|
||||||
|
|
Loading…
Reference in New Issue