• 多域名单点登录

    多域名单点登录概述:

    用于集成外部CAS,OAUTH2,SAML,IDM等协议的单点登录,可通过配置多个前端域名对应不同的单点登录协议,访问不同域名时跳转到相应单点登录页进行认证。

    组件介绍:

    hzero-starter-sso 单点登录组件组成如下:

    组件依赖

    如果想使用某个组件,需自行在 oauth 服务中引入相关依赖:

    单点登录启用配置:

    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 单点登录介绍

    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 开发单点登录。

    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服务,在域名配置中配置相应信息,进行测试。