Skip to content

Commit

Permalink
add openid connect auth support for apollo-portal (#3534)
Browse files Browse the repository at this point in the history
  • Loading branch information
vdiskg authored Feb 6, 2021
1 parent b728d42 commit 96c8d07
Show file tree
Hide file tree
Showing 9 changed files with 566 additions and 3 deletions.
8 changes: 8 additions & 0 deletions apollo-portal/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-openapi</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- yml processing -->
<dependency>
<groupId>org.yaml</groupId>
Expand Down
45 changes: 45 additions & 0 deletions apollo-portal/src/main/config/application-oidc-sample.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
spring:
security:
oauth2:
client:
provider:
# provider-name 是 oidc 提供者的名称, 任意字符均可, registration 的配置需要用到这个名称
provider-name:
# 必须是 https, oidc 的 issuer-uri, 和 jwt 的 issuer-uri 一致的话直接引用即可, 也可以单独设置
issuer-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
registration:
# registration-name 是 oidc 客户端的名称, 任意字符均可, oidc 登录必须配置一个 authorization_code 类型的 registration
registration-name:
# oidc 登录必须配置一个 authorization_code 类型的 registration
authorization-grant-type: authorization_code
client-authentication-method: basic
# client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider
client-id: apollo-portal
# provider 的名称, 需要和上面配置的 provider 名称保持一致
provider: provider-name
# openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope
scope:
- openid
# client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider
# 从安全角度考虑更推荐使用环境变量来配置, 环境变量的命名规则为: 将配置项的 key 当中的 点(.)、横杠(-)替换为下划线(_), 然后将所有字母改为大写, spring boot 会自动处理符合此规则的环境变量
# 例如 spring.security.oauth2.client.registration.registration-name.client-secret -> SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_NAME_VDISK_CLIENT_SECRET (REGISTRATION_NAME 可以替换为自定义的 oidc 客户端的名称)
client-secret: d43c91c0-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# registration-name-client 是 oidc 客户端的名称, 任意字符均可, client_credentials 类型的 registration 为选填项, 可以不配置
registration-name-client:
# client_credentials 类型的 registration 为选填项, 用于 apollo-portal 作为客户端请求其它被 oidc 保护的资源, 可以不配置
authorization-grant-type: client_credentials
client-authentication-method: basic
# client-id 是在 oidc 提供者处配置的客户端ID, 用于登录 provider
client-id: apollo-portal
# provider 的名称, 需要和上面配置的 provider 名称保持一致
provider: provider-name
# openid 为 oidc 登录的必须 scope, 此处可以添加其它自定义的 scope
scope:
- openid
# client-secret 是在 oidc 提供者处配置的客户端密码, 用于登录 provider, 多个 registration 的密码如果一致可以直接引用
client-secret: ${spring.security.oauth2.client.registration.registration-name.client-secret}
resourceserver:
jwt:
# 必须是 https, jwt 的 issuer-uri
# 例如 你的 issuer-uri 是 https://host:port/auth/realms/apollo/.well-known/openid-configuration, 那么此处只需要配置 https://host:port/auth/realms/apollo 即可, spring boot 处理的时候会自动加上 /.well-known/openid-configuration 的后缀
issuer-uri: https://host:port/auth/realms/apollo
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import com.ctrip.framework.apollo.portal.repository.UserRepository;
import com.ctrip.framework.apollo.portal.spi.LogoutHandler;
import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler;
import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
Expand All @@ -18,10 +19,17 @@
import com.ctrip.framework.apollo.portal.spi.ldap.ApolloLdapAuthenticationProvider;
import com.ctrip.framework.apollo.portal.spi.ldap.FilterLdapByGroupUserSearch;
import com.ctrip.framework.apollo.portal.spi.ldap.LdapUserService;
import com.ctrip.framework.apollo.portal.spi.oidc.ExcludeClientCredentialsClientRegistrationRepository;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcAuthenticationSuccessEventListener;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLocalUserService;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcLogoutHandler;
import com.ctrip.framework.apollo.portal.spi.oidc.OidcUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
import com.google.common.collect.Maps;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
Expand All @@ -44,6 +52,8 @@
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

Expand Down Expand Up @@ -421,11 +431,95 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
}

@Profile("oidc")
@EnableConfigurationProperties({OAuth2ClientProperties.class, OAuth2ResourceServerProperties.class})
@Configuration
static class OidcAuthAutoConfiguration {

@Bean
@ConditionalOnMissingBean(SsoHeartbeatHandler.class)
public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
return new DefaultSsoHeartbeatHandler();
}

@Bean
@ConditionalOnMissingBean(UserInfoHolder.class)
public UserInfoHolder oidcUserInfoHolder() {
return new OidcUserInfoHolder();
}

@Bean
@ConditionalOnMissingBean(LogoutHandler.class)
public LogoutHandler oidcLogoutHandler() {
return new OidcLogoutHandler();
}

@Bean
@ConditionalOnMissingBean(JdbcUserDetailsManager.class)
public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth,
DataSource datasource) throws Exception {
return new SpringSecurityAuthAutoConfiguration().jdbcUserDetailsManager(auth, datasource);
}

@Bean
@ConditionalOnMissingBean(UserService.class)
public OidcLocalUserService oidcLocalUserService(JdbcUserDetailsManager userDetailsManager,
UserRepository userRepository) {
return new OidcLocalUserService(userDetailsManager, userRepository);
}

@Bean
public OidcAuthenticationSuccessEventListener oidcAuthenticationSuccessEventListener(OidcLocalUserService oidcLocalUserService) {
return new OidcAuthenticationSuccessEventListener(oidcLocalUserService);
}
}

@Profile("oidc")
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
static class OidcWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

private final InMemoryClientRegistrationRepository clientRegistrationRepository;

private final OAuth2ResourceServerProperties oauth2ResourceServerProperties;

public OidcWebSecurityConfigurerAdapter(
InMemoryClientRegistrationRepository clientRegistrationRepository,
OAuth2ResourceServerProperties oauth2ResourceServerProperties) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.oauth2ResourceServerProperties = oauth2ResourceServerProperties;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests(requests -> requests.antMatchers(BY_PASS_URLS).permitAll());
http.authorizeRequests(requests -> requests.anyRequest().authenticated());
http.oauth2Login(configure ->
configure.clientRegistrationRepository(
new ExcludeClientCredentialsClientRegistrationRepository(
this.clientRegistrationRepository)));
http.oauth2Client();
http.logout(configure -> {
OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(
this.clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
configure.logoutSuccessHandler(logoutSuccessHandler);
});
// make jwt optional
String jwtIssuerUri = this.oauth2ResourceServerProperties.getJwt().getIssuerUri();
if (!StringUtils.isBlank(jwtIssuerUri)) {
http.oauth2ResourceServer().jwt();
}
}
}

/**
* default profile
*/
@Configuration
@ConditionalOnMissingProfile({"ctrip", "auth", "ldap"})
@ConditionalOnMissingProfile({"ctrip", "auth", "ldap", "oidc"})
static class DefaultAuthAutoConfiguration {

@Bean
Expand Down Expand Up @@ -453,7 +547,7 @@ public UserService defaultUserService() {
}
}

@ConditionalOnMissingProfile({"auth", "ldap"})
@ConditionalOnMissingProfile({"auth", "ldap", "oidc"})
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.ctrip.framework.apollo.portal.spi.oidc;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;

/**
* @author vdisk <[email protected]>
*/
public class ExcludeClientCredentialsClientRegistrationRepository implements
ClientRegistrationRepository, Iterable<ClientRegistration> {

/**
* origin clientRegistrationRepository
*/
private final InMemoryClientRegistrationRepository delegate;

/**
* exclude client_credentials
*/
private final List<ClientRegistration> clientRegistrationList;

public ExcludeClientCredentialsClientRegistrationRepository(
InMemoryClientRegistrationRepository delegate) {
Objects.requireNonNull(delegate, "clientRegistrationRepository cannot be null");
this.delegate = delegate;
this.clientRegistrationList = Collections.unmodifiableList(StreamSupport
.stream(Spliterators.spliteratorUnknownSize(delegate.iterator(), Spliterator.ORDERED),
false)
.filter(clientRegistration -> !AuthorizationGrantType.CLIENT_CREDENTIALS
.equals(clientRegistration.getAuthorizationGrantType()))
.collect(Collectors.toList()));
}

@Override
public ClientRegistration findByRegistrationId(String registrationId) {
ClientRegistration clientRegistration = this.delegate.findByRegistrationId(registrationId);
if (clientRegistration == null) {
return null;
}
if (AuthorizationGrantType.CLIENT_CREDENTIALS
.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}
return clientRegistration;
}

@Override
public Iterator<ClientRegistration> iterator() {
return this.clientRegistrationList.iterator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.ctrip.framework.apollo.portal.spi.oidc;

import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;

/**
* @author vdisk <[email protected]>
*/
public class OidcAuthenticationSuccessEventListener implements
ApplicationListener<AuthenticationSuccessEvent> {

private static final Logger log = LoggerFactory
.getLogger(OidcAuthenticationSuccessEventListener.class);

private final OidcLocalUserService oidcLocalUserService;

private final ConcurrentMap<String, String> userIdCache = new ConcurrentHashMap<>();

public OidcAuthenticationSuccessEventListener(
OidcLocalUserService oidcLocalUserService) {
this.oidcLocalUserService = oidcLocalUserService;
}

@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof OidcUser) {
this.oidcUserLogin((OidcUser) principal);
return;
}
if (principal instanceof Jwt) {
this.jwtLogin((Jwt) principal);
return;
}
log.warn("principal is neither oidcUser nor jwt, principal=[{}]", principal);
}

private void oidcUserLogin(OidcUser oidcUser) {
if (this.contains(oidcUser.getSubject())) {
return;
}
UserInfo newUserInfo = new UserInfo();
newUserInfo.setUserId(oidcUser.getSubject());
newUserInfo.setName(oidcUser.getPreferredUsername());
newUserInfo.setEmail(oidcUser.getEmail());
this.oidcLocalUserService.createLocalUser(newUserInfo);
}

private boolean contains(String userId) {
if (this.userIdCache.containsKey(userId)) {
return true;
}
UserInfo userInfo = this.oidcLocalUserService.findByUserId(userId);
if (userInfo != null) {
this.userIdCache.put(userId, userId);
return true;
}
return false;
}

private void jwtLogin(Jwt jwt) {
if (this.contains(jwt.getSubject())) {
return;
}
UserInfo newUserInfo = new UserInfo();
newUserInfo.setUserId(jwt.getSubject());
this.oidcLocalUserService.createLocalUser(newUserInfo);
}
}
Loading

0 comments on commit 96c8d07

Please sign in to comment.