多域名单点登录
多域名单点登录概述:
用于集成外部CAS,OAUTH2,SAML,IDM等协议的单点登录,可通过配置多个前端域名对应不同的单点登录协议,访问不同域名时跳转到相应单点登录页进行认证。
组件介绍:
hzero-starter-sso 单点登录组件组成如下:
- hzero-starter-sso-core : 单点登录核心组件,负责域名对应跳转控制等
- hzero-starter-sso-cas : cas类型单点登录
- hzero-starter-sso-oauth : oauth类型单点登录
- hzero-starter-sso-saml : saml类型单点登录
- hzero-starter-sso-idm : idm类型单点登录
- hzero-starter-sso-azure : 微软云AD单点登录
组件依赖
如果想使用某个组件,需自行在 oauth 服务中引入相关依赖:
-
CAS
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-cas</artifactId> <version>${hzero.starter.version}</version> </dependency>
-
Oauth
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-oauth</artifactId> <version>${hzero.starter.version}</version> </dependency>
-
saml
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-saml</artifactId> <version>${hzero.starter.version}</version> </dependency>
-
idm
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-idm</artifactId> <version>${hzero.starter.version}</version> </dependency>
-
azure
<dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-azure</artifactId> <version>${hzero.starter.version}</version> </dependency>
单点登录启用配置:
hzero:
### SSO配置 ###
oauth:
sso:
# 是否启用二级域名单点登录
enabled: true
provider:
# cas类型provider Key
key: hzero
service:
# oauth服务基础地址,用于CAS类型单点登录校验票据
baseUrl: http://dev.hzero.org:8080/oauth
# saml类型相关参数
saml:
entity-id: hzero:org:sp
passphrase: secret
private-key:
certificate:
多域名单点登录效果:
1.在系统管理-》配置管理-》域名配置中,点击 创建
按钮,弹出创建页面,创建单点登录配置
2.浏览器打开配置的域名 http://cas.hzero.org
,自动跳转到单点登录页面,并附带回调地址
3.单点登录页登录成功后,跳转回 http://cas.hzero.org
4.退出Hzero平台,重新回到单点登录界面
Cas 单点登录集成
Cas 单点登录流程
Cas 单点登录配置如下:
1.单点域名:平台域名的代理地址;
2.单点登录类型选择:CAS协议;
3.单点登录服务器地址:CAS服务器地址;
4.单点登录地址:CAS认证地址;
5.客户端地址:登录成功后的回调地址;
Cas 单点登录效果:
参见上一小节 多域名单点登录效果,此处不再赘述。
Oauth 单点登录集成
Oauth 单点登录介绍:
登录时,判断二级域名对应的单点登录类型为Oauth2单点登录,跳转到OAUTH2单点登录系统,登录成功后调用回调地址并附带授权码参数,回调api处理逻辑中,会根据单点授权码获取对应单点登录token,再根据单点登录token获取单点登录用户名,根据单点用户名获取平台用户信息、校验权限、生成平台认证存入SecurityContextHolder,并再次重定向/oauth/authorize获取token返回给前端。
oauth 单点登录流程
Oauth 单点登录配置如下:
1.单点域名:平台域名的代理地址;
2.单点登录类型选择:OAUTH2协议;
3.单点登录服务器地址:OAUTH2服务器地址;
4.单点登录地址:OAUTH2认证地址;
5.客户端地址:登录成功后的回调地址;
6.clientId:OAUTH2认证客户端ID;
7.client密码:OAUTH2认证客户端密码;
8.Oauth认证用户信息:OAUTH2根据token获取用户信息地址;
Oauth 单点登录效果:
1.在系统管理-》配置管理-》域名配置中,创建单点登录配置
2.浏览器打开配置的域名 http://auth.hzero.org
,自动跳转到单点登录页面,并附带回调地址
3.单点登录页登录成功后,跳转回 http://auth.hzero.org
Azure-AD 单点登录集成
oauth 单点登录介绍
- Azure-AD 本质是Oauth2协议,具体配置可参照上一小节:Oauth2 单点登录集成
- 区别之处在于Azure-AD 已提供SDK根据token查询用户信息,无需配置Oauth2获取用户信息地址。
oauth 单点登录流程
saml 单点登录集成
saml 单点登录流程
saml 单点登录配置:
1.下载hzero saml元数据,下载地址为oauth服务+/saml/metadata,例如:http://hzeronb.saas.hand-china.com/oauth/saml/metadata
2.将hzero元数据提供给saml单点登录供应商,不同的saml单点登录供应商会有所区别,有些可能只需要回调地址。
3.获取saml 身份提供方的元数据地址,配置在域名配置->saml元数据地址字段中。 注意,此处只支持http协议地址,若原地址为https协议,可将元数据文件下载,再转存到http协议的文件服务器中,可考虑hzero自带的文件上传功能:
再将对应http协议地址配置在 saml元数据地址
字段。
saml 单点登录效果:
1.在系统管理-》配置管理-》域名配置中,创建单点登录配置
2.浏览器打开配置的域名 http://saml.hzero.org:8003
,自动跳转到单点登录页面,并附带回调地址
3.单点登录页登录成功后,跳转回 http://saml.hzero.org:8003
idm 单点登录集成
idm 单点登录介绍
idm类型单点登录跟其他单点登录类型的区别点在于,单点登录提供方在整个http层面进行了单点登录控制,简单来讲,相当于nginx在进行url代理前进行了单点登录判断,有权限的才通过代理访问具体资源,无权限的跳转到单点登录页面进行登录。而其他单点登录协议都是访问了Hzero前端静态资源,调用了oauth接口后在oauth服务里进行登录跳转,跳转到单点登录界面登录成功后,再由单点登录方重定向回oauth服务提供的回调地址,在oauth服务过滤器中进行用户信息,用户权限二次校验。
idm 单点登录流程
访问域名时,webgate会自动判断是否通过单点登录,如果没有会自动跳转到单点登录页面进行登录。单点登录后,会将用户名放入Cookie中,访问Oauth服务时,可从接口请求Header中获取到用户名,过滤器中根据用户名获取用户信息、校验权限、生成认证存入SecurityContextHolder,并再次重定向/oauth/authorize获取token返回给前端。
idm 单点登录配置:
1.单点域名:平台域名的代理地址;
2.单点登录类型选择:IDM协议;
3.客户端地址:登录成功后的回调地址;
idm 单点登录效果:
1.在系统管理-》配置管理-》域名配置中,创建单点登录配置
2.浏览器打开配置的域名 http://idm.hzero.org:8003
,自动重定向到回调地址,过滤器获取Header中的用户名,根据用户名获取用户信息、校验权限、生成认证存入SecurityContextHolder,并再次重定向/oauth/authorize获取token返回给前端,登录成功。
客制化开发
HZERO 目前已支持市场上主流的单点登录协议:cas、oauth、saml、idm等,如果项目上需要开发其它的单点登录,可按照如下步骤开发。
1. 创建单点登录组件
开发时,建议新建一个项目或模块,开发完成后在 oauth 服务中依赖该组件即可。parent 依赖 hzero-starter-sso-parent
,引入 hzero-starter-sso-core
组件。下面以oauth类型单点登录开发的流程为例讲解如何基于 hzero-starter-sso-core 开发单点登录。
- oauth pom:
<parent> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-parent</artifactId> <version>1.0.0.RELEASE</version> </parent> <artifactId>hzero-starter-sso-oauth</artifactId> <dependencies> <dependency> <groupId>org.hzero.starter</groupId> <artifactId>hzero-starter-sso-core</artifactId> <version>${hzero.starter.version}</version> </dependency> </dependencies>
- 项目结构:
2. 自定义Token
自定义项目token,主要用于匹配provider。
public class Auth2AuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
//构建认证标记为false的token
public Auth2AuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
//构建认证标记为true的token
public Auth2AuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
3. Filter过滤器
自定义过滤器,过滤匹配url即为单点登录完成后的回调地址,主要目的是获取单点登录的用户信息,进行用户验证。过滤器继承于org.springframework.security.web.AbstractAuthenticationProcessingFilter
,主要重写其attemptAuthentication方法。
public class Auth2AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public Auth2AuthenticationFilter(DomainRepository domainRepository) {
//过滤器匹配地址
super("/login/auth2/**");
this.domainRepository = domainRepository;
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
}
@Override
protected final void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
//验证成功后跳转逻辑
}
@Override
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response)
throws AuthenticationException {
Domain domain = getSsoDomain(request);
Assert.notNull(domain, "Domain is Not Exists");
//获取单点登录返回的授权码
String code = request.getParameter("code");
//根据授权码获取token
String url = domain.getSsoServerUrl() + "/oauth/token?grant_type=authorization_code&code=" + code
+ "&redirect_uri=" + domain.getClientHostUrl();
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
HttpHeaders headers = new HttpHeaders();
String authStr = domain.getSsoClientId() + ":" + domain.getSsoClientPwd();
String authorization = "Basic " + new String(Base64.encodeBase64(authStr.getBytes(StandardCharsets.UTF_8)),
StandardCharsets.UTF_8);
headers.add("Authorization", authorization);
HttpEntity<MultiValueMap<String, String>> formEntity = new HttpEntity<MultiValueMap<String, String>>(params,
headers);
String result = restTemplate.postForObject(url, formEntity, String.class);
Map<String, Object> authRes = jsonMapper.convertToMap(result);
String username = "";
//根据token获取登录用户名
if (authRes.get("access_token") != null) {
HttpHeaders userHeaders = new HttpHeaders();
userHeaders.add("Authorization", "bearer " + authRes.get("access_token"));
HttpEntity<MultiValueMap<String, String>> userFormEntity = new HttpEntity<MultiValueMap<String, String>>(
params, userHeaders);
ResponseEntity<String> userResult = restTemplate.exchange(domain.getSsoUserInfo(), HttpMethod.GET,
userFormEntity, String.class, new Object());
Map<String, Object> userInfo = jsonMapper.convertToMap(userResult.getBody());
if (userInfo.get("username") != null) {
username = String.valueOf(userInfo.get("username"));
} else {
for (String key : userInfo.keySet()) {
if (key.contains("name")) {
username = String.valueOf(userInfo.get(key));
break;
}
}
}
}
//将登录用户名放入自定义token,后续token对应provider中进行用户验证
Auth2AuthenticationToken authRequest = new Auth2AuthenticationToken(username, domain.getTenantId());
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
4. provider验证器
验证器继承于 org.springframework.security.authentication.AuthenticationProvider
抽象类,在 supports
方法中匹配支持验证的token类型,在 authenticate
方法中,进行用户验证具体实现。
public class Auth2AuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(Auth2AuthenticationToken.class, authentication,
"Only Auth2AuthenticationToken is supported");
Assert.notNull(authentication.getPrincipal(), "User account is Not Exists");
String userName = authentication.getName();
final Auth2AuthenticationToken authenticationToken = new Auth2AuthenticationToken(userName, authentication.getCredentials());
//根据自定义token获取用户信息
UserDetails user = this.authenticationUserDetailsService.loadUserDetails(authenticationToken);
Assert.notNull(user, "User account is Not Exists");
//认证通过,返回已认证token
return createSuccessAuthentication(user, authentication, user);
}
//构建认证标记为true的token
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
Auth2AuthenticationToken result =
new Auth2AuthenticationToken(principal,authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
//仅支持Auth2AuthenticationToken类型token
return (Auth2AuthenticationToken.class.isAssignableFrom(authentication));
}
}
5. UserDetailsService用户信息服务类
自定义UserDetailsService用户信息服务类,主要用户根据token获取用户信息,判断用户是否有登录权限等,继承于 org.springframework.security.core.userdetails.AuthenticationUserDetailsService<T>
抽象类,在 loadUserDetails
方法中,进行用户信息,用户校验具体实现。
public class Auth2UserDetailsService implements AuthenticationUserDetailsService<Auth2AuthenticationToken> {
private static final Logger LOGGER = LoggerFactory.getLogger(Auth2UserDetailsService.class);
private SsoUserAccountService userAccountService;
private SsoUserDetailsBuilder userDetailsBuilder;
public Auth2UserDetailsService(SsoUserAccountService userAccountService,
SsoUserDetailsBuilder userDetailsBuilder) {
this.userAccountService = userAccountService;
this.userDetailsBuilder = userDetailsBuilder;
}
@Override
public UserDetails loadUserDetails(Auth2AuthenticationToken token) throws UsernameNotFoundException {
//获取用户名
String username = token.getName();
//获取单点登录域名所属租户
Long tenantId = Long.valueOf(String.valueOf(token.getCredentials()));
LOGGER.debug("load auth2 user, username={}, tenantId={},token={}", username, tenantId, token);
//获取用户信息
SsoUser user = userAccountService.findLoginUser(username, UserType.ofDefault());
Assert.notNull(user, "User is Not Exists");
//获取用户有权限登录的租户
List<Long> organizationIdList = userAccountService.findUserLegalOrganization(user.getId());
//判断用户登录权限是否匹配
if (!organizationIdList.contains(tenantId)){
throw new UsernameNotFoundException(LoginExceptions.USERNAME_NOT_FOUND.value());
}
//构建UserDetails
return userDetailsBuilder.buildUserDetails(user);
}
}
6. WebSecurityConfigurerAdapter自定义配置类
自定义配置类主要用于构建过滤器链路,需继承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
类。
//注意Order唯一性
@Order(org.springframework.boot.autoconfigure.security.SecurityProperties.BASIC_AUTH_ORDER - 1)
@Configuration
public class OauthWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Autowired(required = false)
private DomainRepository domainRepository;
@Autowired(required = false)
private Auth2AuthenticationProvider auth2AuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/login/auth2/**")
.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.addFilterAt(auth2AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf()
.disable()
;
}
private Auth2AuthenticationFilter auth2AuthenticationFilter() {
Auth2AuthenticationFilter auth2AuthenticationFilter = new Auth2AuthenticationFilter(domainRepository);
ProviderManager providerManager = new ProviderManager(Collections.singletonList(auth2AuthenticationProvider));
auth2AuthenticationFilter.setAuthenticationManager(providerManager);
return auth2AuthenticationFilter;
}
}
7. AutoConfiguration自定义自动配置类
自定义自动配置类,主要用于注册Bean。
@Configuration
@ComponentScan(value = {
"org.hzero.sso.oauth",
})
public class OauthAutoConfiguration {
@Bean
@ConditionalOnMissingBean(Auth2UserDetailsService.class)
public Auth2UserDetailsService auth2UserDetailsService(SsoUserAccountService userAccountService,
SsoUserDetailsBuilder userDetailsBuilder) {
return new Auth2UserDetailsService(userAccountService, userDetailsBuilder);
}
@Bean
@ConditionalOnMissingBean(Auth2AuthenticationProvider.class)
public Auth2AuthenticationProvider auth2AuthenticationProvider(Auth2UserDetailsService auth2UserDetailsService) {
return new Auth2AuthenticationProvider(auth2UserDetailsService);
}
}
8. 添加配置
在 resources 资源目录下,新建 META-INF
目录,添加 spring.factories
文件,并将OauthAutoConfiguration自定义自动配置类添加到自动配置。内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.hzero.sso.oauth.autoconfigure.OauthAutoConfiguration
9. 测试
开发完成后,打包发布,然后在 oauth 服务中引入依赖,启动oauth服务,在域名配置中配置相应信息,进行测试。