Skip to content

Commit 96c8d07

Browse files
authored
add openid connect auth support for apollo-portal (#3534)
1 parent b728d42 commit 96c8d07

File tree

9 files changed

+566
-3
lines changed

9 files changed

+566
-3
lines changed

apollo-portal/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
<groupId>com.ctrip.framework.apollo</groupId>
2828
<artifactId>apollo-openapi</artifactId>
2929
</dependency>
30+
<dependency>
31+
<groupId>org.springframework.boot</groupId>
32+
<artifactId>spring-boot-starter-oauth2-client</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
37+
</dependency>
3038
<!-- yml processing -->
3139
<dependency>
3240
<groupId>org.yaml</groupId>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
spring:
2+
security:
3+
oauth2:
4+
client:
5+
provider:
6+
# provider-name 是 oidc 提供者的名称, 任意字符均可, registration 的配置需要用到这个名称
7+
provider-name:
8+
# 必须是 https, oidc 的 issuer-uri, 和 jwt 的 issuer-uri 一致的话直接引用即可, 也可以单独设置
9+
issuer-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
10+
registration:
11+
# registration-name 是 oidc 客户端的名称, 任意字符均可, oidc 登录必须配置一个 authorization_code 类型的 registration
12+
registration-name:
13+
# oidc 登录必须配置一个 authorization_code 类型的 registration
14+
authorization-grant-type: authorization_code
15+
client-authentication-method: basic
16+
# client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider
17+
client-id: apollo-portal
18+
# provider 的名称, 需要和上面配置的 provider 名称保持一致
19+
provider: provider-name
20+
# openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope
21+
scope:
22+
- openid
23+
# client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider
24+
# 从安全角度考虑更推荐使用环境变量来配置, 环境变量的命名规则为: 将配置项的 key 当中的 点(.)、横杠(-)替换为下划线(_), 然后将所有字母改为大写, spring boot 会自动处理符合此规则的环境变量
25+
# 例如 spring.security.oauth2.client.registration.registration-name.client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_NAME_VDISK_CLIENT_SECRET (REGISTRATION_NAME 可以替换为自定义的 oidc 客户端的名称)
26+
client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
27+
# registration-name-client 是 oidc 客户端的名称, 任意字符均可, client_credentials 类型的 registration 为选填项, 可以不配置
28+
registration-name-client:
29+
# client_credentials 类型的 registration 为选填项, 用于 apollo-portal 作为客户端请求其它被 oidc 保护的资源, 可以不配置
30+
authorization-grant-type: client_credentials
31+
client-authentication-method: basic
32+
# client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider
33+
client-id: apollo-portal
34+
# provider 的名称, 需要和上面配置的 provider 名称保持一致
35+
provider: provider-name
36+
# openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope
37+
scope:
38+
- openid
39+
# client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider, 多个 registration 的密码如果一致可以直接引用
40+
client-secret: ${spring.security.oauth2.client.registration.registration-name.client-secret}
41+
resourceserver:
42+
jwt:
43+
# 必须是 https, jwt 的 issuer-uri
44+
# 例如 你的 issuer-uri 是 https://host:port/auth/realms/apollo/.well-known/openid-configuration, 那么此处只需要配置 https://host:port/auth/realms/apollo 即可, spring boot 处理的时候会自动加上 /.well-known/openid-configuration 的后缀
45+
issuer-uri: https://host:port/auth/realms/apollo

apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile;
44
import com.ctrip.framework.apollo.core.utils.StringUtils;
55
import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
6+
import com.ctrip.framework.apollo.portal.repository.UserRepository;
67
import com.ctrip.framework.apollo.portal.spi.LogoutHandler;
78
import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler;
89
import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
@@ -18,10 +19,17 @@
1819
import com.ctrip.framework.apollo.portal.spi.ldap.ApolloLdapAuthenticationProvider;
1920
import com.ctrip.framework.apollo.portal.spi.ldap.FilterLdapByGroupUserSearch;
2021
import com.ctrip.framework.apollo.portal.spi.ldap.LdapUserService;
22+
import com.ctrip.framework.apollo.portal.spi.oidc.ExcludeClientCredentialsClientRegistrationRepository;
23+
import com.ctrip.framework.apollo.portal.spi.oidc.OidcAuthenticationSuccessEventListener;
24+
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserService;
25+
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLogoutHandler;
26+
import com.ctrip.framework.apollo.portal.spi.oidc.OidcUserInfoHolder;
2127
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder;
2228
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
2329
import com.google.common.collect.Maps;
2430
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
31+
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
32+
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
2533
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2634
import org.springframework.boot.web.servlet.FilterRegistrationBean;
2735
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
@@ -44,6 +52,8 @@
4452
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
4553
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
4654
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
55+
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
56+
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
4757
import org.springframework.security.provisioning.JdbcUserDetailsManager;
4858
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
4959

@@ -421,11 +431,95 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
421431
}
422432
}
423433

434+
@Profile("oidc")
435+
@EnableConfigurationProperties({OAuth2ClientProperties.class, OAuth2ResourceServerProperties.class})
436+
@Configuration
437+
static class OidcAuthAutoConfiguration {
438+
439+
@Bean
440+
@ConditionalOnMissingBean(SsoHeartbeatHandler.class)
441+
public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
442+
return new DefaultSsoHeartbeatHandler();
443+
}
444+
445+
@Bean
446+
@ConditionalOnMissingBean(UserInfoHolder.class)
447+
public UserInfoHolder oidcUserInfoHolder() {
448+
return new OidcUserInfoHolder();
449+
}
450+
451+
@Bean
452+
@ConditionalOnMissingBean(LogoutHandler.class)
453+
public LogoutHandler oidcLogoutHandler() {
454+
return new OidcLogoutHandler();
455+
}
456+
457+
@Bean
458+
@ConditionalOnMissingBean(JdbcUserDetailsManager.class)
459+
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth,
460+
DataSource datasource) throws Exception {
461+
return new SpringSecurityAuthAutoConfiguration().jdbcUserDetailsManager(auth, datasource);
462+
}
463+
464+
@Bean
465+
@ConditionalOnMissingBean(UserService.class)
466+
public OidcLocalUserService oidcLocalUserService(JdbcUserDetailsManager userDetailsManager,
467+
UserRepository userRepository) {
468+
return new OidcLocalUserService(userDetailsManager, userRepository);
469+
}
470+
471+
@Bean
472+
public OidcAuthenticationSuccessEventListener oidcAuthenticationSuccessEventListener(OidcLocalUserService oidcLocalUserService) {
473+
return new OidcAuthenticationSuccessEventListener(oidcLocalUserService);
474+
}
475+
}
476+
477+
@Profile("oidc")
478+
@EnableWebSecurity
479+
@EnableGlobalMethodSecurity(prePostEnabled = true)
480+
@Configuration
481+
static class OidcWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
482+
483+
private final InMemoryClientRegistrationRepository clientRegistrationRepository;
484+
485+
private final OAuth2ResourceServerProperties oauth2ResourceServerProperties;
486+
487+
public OidcWebSecurityConfigurerAdapter(
488+
InMemoryClientRegistrationRepository clientRegistrationRepository,
489+
OAuth2ResourceServerProperties oauth2ResourceServerProperties) {
490+
this.clientRegistrationRepository = clientRegistrationRepository;
491+
this.oauth2ResourceServerProperties = oauth2ResourceServerProperties;
492+
}
493+
494+
@Override
495+
protected void configure(HttpSecurity http) throws Exception {
496+
http.csrf().disable();
497+
http.authorizeRequests(requests -> requests.antMatchers(BY_PASS_URLS).permitAll());
498+
http.authorizeRequests(requests -> requests.anyRequest().authenticated());
499+
http.oauth2Login(configure ->
500+
configure.clientRegistrationRepository(
501+
new ExcludeClientCredentialsClientRegistrationRepository(
502+
this.clientRegistrationRepository)));
503+
http.oauth2Client();
504+
http.logout(configure -> {
505+
OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(
506+
this.clientRegistrationRepository);
507+
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
508+
configure.logoutSuccessHandler(logoutSuccessHandler);
509+
});
510+
// make jwt optional
511+
String jwtIssuerUri = this.oauth2ResourceServerProperties.getJwt().getIssuerUri();
512+
if (!StringUtils.isBlank(jwtIssuerUri)) {
513+
http.oauth2ResourceServer().jwt();
514+
}
515+
}
516+
}
517+
424518
/**
425519
* default profile
426520
*/
427521
@Configuration
428-
@ConditionalOnMissingProfile({"ctrip", "auth", "ldap"})
522+
@ConditionalOnMissingProfile({"ctrip", "auth", "ldap", "oidc"})
429523
static class DefaultAuthAutoConfiguration {
430524

431525
@Bean
@@ -453,7 +547,7 @@ public UserService defaultUserService() {
453547
}
454548
}
455549

456-
@ConditionalOnMissingProfile({"auth", "ldap"})
550+
@ConditionalOnMissingProfile({"auth", "ldap", "oidc"})
457551
@Configuration
458552
@EnableWebSecurity
459553
@EnableGlobalMethodSecurity(prePostEnabled = true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.ctrip.framework.apollo.portal.spi.oidc;
2+
3+
import java.util.Collections;
4+
import java.util.Iterator;
5+
import java.util.List;
6+
import java.util.Objects;
7+
import java.util.Spliterator;
8+
import java.util.Spliterators;
9+
import java.util.stream.Collectors;
10+
import java.util.stream.StreamSupport;
11+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
12+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
13+
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
14+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
15+
16+
/**
17+
* @author vdisk <[email protected]>
18+
*/
19+
public class ExcludeClientCredentialsClientRegistrationRepository implements
20+
ClientRegistrationRepository, Iterable<ClientRegistration> {
21+
22+
/**
23+
* origin clientRegistrationRepository
24+
*/
25+
private final InMemoryClientRegistrationRepository delegate;
26+
27+
/**
28+
* exclude client_credentials
29+
*/
30+
private final List<ClientRegistration> clientRegistrationList;
31+
32+
public ExcludeClientCredentialsClientRegistrationRepository(
33+
InMemoryClientRegistrationRepository delegate) {
34+
Objects.requireNonNull(delegate, "clientRegistrationRepository cannot be null");
35+
this.delegate = delegate;
36+
this.clientRegistrationList = Collections.unmodifiableList(StreamSupport
37+
.stream(Spliterators.spliteratorUnknownSize(delegate.iterator(), Spliterator.ORDERED),
38+
false)
39+
.filter(clientRegistration -> !AuthorizationGrantType.CLIENT_CREDENTIALS
40+
.equals(clientRegistration.getAuthorizationGrantType()))
41+
.collect(Collectors.toList()));
42+
}
43+
44+
@Override
45+
public ClientRegistration findByRegistrationId(String registrationId) {
46+
ClientRegistration clientRegistration = this.delegate.findByRegistrationId(registrationId);
47+
if (clientRegistration == null) {
48+
return null;
49+
}
50+
if (AuthorizationGrantType.CLIENT_CREDENTIALS
51+
.equals(clientRegistration.getAuthorizationGrantType())) {
52+
return null;
53+
}
54+
return clientRegistration;
55+
}
56+
57+
@Override
58+
public Iterator<ClientRegistration> iterator() {
59+
return this.clientRegistrationList.iterator();
60+
}
61+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.ctrip.framework.apollo.portal.spi.oidc;
2+
3+
import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import java.util.concurrent.ConcurrentMap;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.context.ApplicationListener;
9+
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
10+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
11+
import org.springframework.security.oauth2.jwt.Jwt;
12+
13+
/**
14+
* @author vdisk <[email protected]>
15+
*/
16+
public class OidcAuthenticationSuccessEventListener implements
17+
ApplicationListener<AuthenticationSuccessEvent> {
18+
19+
private static final Logger log = LoggerFactory
20+
.getLogger(OidcAuthenticationSuccessEventListener.class);
21+
22+
private final OidcLocalUserService oidcLocalUserService;
23+
24+
private final ConcurrentMap<String, String> userIdCache = new ConcurrentHashMap<>();
25+
26+
public OidcAuthenticationSuccessEventListener(
27+
OidcLocalUserService oidcLocalUserService) {
28+
this.oidcLocalUserService = oidcLocalUserService;
29+
}
30+
31+
@Override
32+
public void onApplicationEvent(AuthenticationSuccessEvent event) {
33+
Object principal = event.getAuthentication().getPrincipal();
34+
if (principal instanceof OidcUser) {
35+
this.oidcUserLogin((OidcUser) principal);
36+
return;
37+
}
38+
if (principal instanceof Jwt) {
39+
this.jwtLogin((Jwt) principal);
40+
return;
41+
}
42+
log.warn("principal is neither oidcUser nor jwt, principal=[{}]", principal);
43+
}
44+
45+
private void oidcUserLogin(OidcUser oidcUser) {
46+
if (this.contains(oidcUser.getSubject())) {
47+
return;
48+
}
49+
UserInfo newUserInfo = new UserInfo();
50+
newUserInfo.setUserId(oidcUser.getSubject());
51+
newUserInfo.setName(oidcUser.getPreferredUsername());
52+
newUserInfo.setEmail(oidcUser.getEmail());
53+
this.oidcLocalUserService.createLocalUser(newUserInfo);
54+
}
55+
56+
private boolean contains(String userId) {
57+
if (this.userIdCache.containsKey(userId)) {
58+
return true;
59+
}
60+
UserInfo userInfo = this.oidcLocalUserService.findByUserId(userId);
61+
if (userInfo != null) {
62+
this.userIdCache.put(userId, userId);
63+
return true;
64+
}
65+
return false;
66+
}
67+
68+
private void jwtLogin(Jwt jwt) {
69+
if (this.contains(jwt.getSubject())) {
70+
return;
71+
}
72+
UserInfo newUserInfo = new UserInfo();
73+
newUserInfo.setUserId(jwt.getSubject());
74+
this.oidcLocalUserService.createLocalUser(newUserInfo);
75+
}
76+
}

0 commit comments

Comments
 (0)