写点什么

Growing 账号认证实践

发布于: 3 小时前
Growing 账号认证实践

背景

GrowingIO 作为专业的数据运营解决方案提供商,我们的客户来自不同的行业,但他们都有相同的安全需求。在众多的客户中,许多客户都有自己的账号认证系统。因此我们需要能通过简单的配置接入客户的账号认证系统。目前 GrowingIO 一共支持了 CAS, OAuth2, LDAP 三种不同的接入协议。本文将详细介绍我们是如何支持这三个接入方式的。

不同接入协议的认证流程

OAuth2

一般来说,使用 OAuth2 来实现认证都是使用的授权码模式,我们这里也不例外,下面是 OAuth2 授权码模式的标准流程。



(A)用户访问客户端,后者将前者导向认证服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向 URI"(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的"重定向 URI ",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)认证服务器核对了授权码和重定向 URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌( refresh token)。

LDAP

LDAP 全称 Lightweight Directory Access Protocol,中文名称轻量目录访问协议。认证登录是其主要应用场景之一,下面是 LDAP 认证的流程。



CAS

CAS 是一个开源的 Java 服务器组件,给企业提供 Web 单点登录服务,CAS 服务器是构建在 Spring Framework 上的 Java 应用程序,其主要职责是通过颁发和验证票证来验证用户并授予对启用 CAS 的服务(通常称为 CAS 客户端)的访问权限。当服务器在成功登录后向用户发出票据授予票据 (TGT) 时,将创建 SSO 会话。服务票证 (ST) 根据用户的请求通过浏览器重定向使用 TGT 作为令牌颁发给服务。ST 随后在 CAS 服务器上通过反向通道通信进行验证。



整体架构


总体思路

整体上就是把这三种认证方式都集成到了 OAuth2 授权码模式的流程中,认证中心 IAM 系统在对接不同的认证协议时扮演了不同的角色。


  • 对于账号密码认证, IAM 扮演的是 OAuth Server,用户的信息保存在数据库 users 表中,网关 Gateway 扮演的是 OAuth Client,走的是标准的授权码流程。

  • 对于 LDAP 认证,IAM 扮演的是 LDAP Client,在认证过程中把用户的用户名和密码拿到 LDAP Server 进行查询,如果查询到合法用户则代表认证成功,之后再走后续的授权码流程。

  • 对于 CAS 认证,IAM 扮演的是 CAS Client,在获取授权码的接口处理逻辑中,IAM 会检查用户是否认证过,如果没有认证过,会把用户重定向到 CAS Server 去进行认证。在 CAS 的认证回调接口中,根据 ticket 换取用户信息,之后设置用户的认证状态并且生成自己的授权码,后面就是标准的授权码流程。

  • 对于 OAuth2 认证,IAM 扮演的是 OAuth Client,和 CAS 认证的思路相同,发现用户没有认证时把用户重定向到 OAuth Server 去进行认证或者授权。在 Oauth Server 的授权码回调接口中,根据返回的授权码拿到 token,进而拿到用户信息,之后设置用户的认证状态并且生成自己的授权码,后面就是标准的授权码流程。

登录流程

账号密码 & LDAP


CAS & Oauth2


关键步骤伪代码

Gateway

Gateway 除了作为反向代理之外,还承担了一部分 OAuth Client 的功能,用来帮助前端设置 OAuth Client 的参数和获取 token 以及刷新 token。


# Gateway 作为 OAuth client,前端登陆时需要先访问 /authorize,# Gateway 负责拼接上 OAuth client的参数location /authorize {    local authorize_uri = '/oauth/authorize?client_id=gateway&response_type=code&redirect_uri=xxx/oauth/callback'    redirect to authorize_uri}
# 标准 Oauth2 授权码接口,第三方系统携带自己的参数访问location /oauth/authorize { proxy IAM}
# 接入 CAS 或者 OAuth 认证时,接受认证码( tocket/code )的接口location /sso/callback { proxy IAM}
# Gateway 处理授权码的回调接口,调用 IAM 服务获取 tokenlocation /oauth/callback { local redirect_uri = 'xxx/oauth/callback' local auth_info = { grant_type = 'authorization_code', code = args.code, client_id = oauth2.client_id, client_secret = oauth2.client_secret, redirect_uri = redirect_uri } # 根据 Gateway 的 client_id 等信息和 code 获取token local token = getTokrnByCode(auth_info) # 拿到token可以返回给前端}
# 获取 OAuth Token 的接口,第三方系统可以拿到授权码之后调用此接口获取 tokenlocation /oauth/token { proxy IAM}
复制代码

IAM

IAM 服务使用 spring-security-oauth2 来构建 OAuth Server,依赖了 spring-security-ldap 和 spring-security-cas 来集成 LDAP 和 CAS 的标准流程。

框架依赖

implementation("org.springframework.security.oauth:spring-security-oauth2:2.3.6.RELEASE")implementation("org.springframework.boot:spring-boot-starter-oauth2-client")implementation("org.springframework.security:spring-security-ldap")implementation("org.springframework.security:spring-security-cas")
复制代码

SSO 接入配置

# 接入客户的哪种认证方式grant:  type: LDAP or ORIGIN or CAS or OAUTH# 接入 CAS 认证时,CAS Server 的地址cas:  serverUrl: https://xxx:8443/cas# 接入 LDAP 认证时,链接 LDAP SERVER 的配置ldap:  type: ad or openLdap  domain: xxx.com  url: ldap://xxx:389  rootDn: dc=xxx,dc=com
复制代码

认证方式配置

@Configuration@EnableWebSecurity@RequiredArgsConstructorpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {    @Value("${grant.type}")    private GrantType grantType;        @Bean    public AuthenticationProvider defaultDaoAuthenticationProvider(){        DaoAuthenticationProvider defaultDaoAuthenticationProvider = new DaoAuthenticationProvider();        defaultDaoAuthenticationProvider.setUserDetailsService(userDetailsService);        defaultDaoAuthenticationProvider.setPasswordEncoder(passwordEncoder());        return defaultDaoAuthenticationProvider;    }        //根据配置,创建不同的认证管理器    public AuthenticationProvider grantTypeAuthenticationProvider(){        AuthenticationProvider grantTypeAuthenticationProvider = null;        switch (configs.getGrantType()){            case LDAP:                LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(configs);                ldapAuthenticationProvider.setUserDetailsContextMapper(userDetailsService);                grantTypeAuthenticationProvider = ldapAuthenticationProvider;                break;            case OAUTH:                final DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();                final DefaultOAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();                grantTypeAuthenticationProvider = new OAuth2LoginAuthenticationProvider(tokenResponseClient, oAuth2UserService);                break;            default: break;        }        return grantTypeAuthenticationProvider;    }        @Override    @Bean    public AuthenticationManager authenticationManagerBean() {        List<AuthenticationProvider> providers = new ArrayList<>();        providers.add(defaultDaoAuthenticationProvider());        AuthenticationProvider grantTypeAuthenticationProvider = grantTypeAuthenticationProvider();        if (Objects.nonNull(grantTypeAuthenticationProvider)) {            providers.add(grantTypeAuthenticationProvider);        }        return new ProviderManager(providers);    }        //CAS 认证时,用来验证 ticket    @Bean    @ConditionalOnProperty(prefix = "grant",value = "type",havingValue = "CAS")    public TicketValidator validator(){        return new Cas30ServiceTicketValidator(casServer);    }    }
复制代码

回调接口逻辑

@GetMapping(value = "/sso/callback")public void casCallBack(@RequestParam Map<String, String> parameters, HttpServletResponse response) throws IOException {    String username;    switch (configs.getGrantType()){        case CAS:            String ticket = parameters.get("ticket");            // casServiceUrl 是前面 cas/login 时携带的 service            //getUserNameByTicket            username = xxx;                     break;        case OAUTH:            String code = parameters.get("code");            //getTokenByCode            //getUserNameByToken            username = xxx;            break;    }    //SSO 认证成功,调用 IAM 的认证逻辑    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, "");    Authentication authentication = authenticationManager.authenticate(authRequest);    SecurityContextHolder.getContext().setAuthentication(authentication);    //生成 OAuth 请求,带 code 重定向,让浏览器携带 code 访问 openresty 的 /oauth/callback 接口    //走 IAM 的授权码流程    AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(parameters);    OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);    OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);    String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);    String redirectUrl = String.format("xxx/oauth/callback?code=%s",code);    redirectUrl = response.encodeRedirectURL(redirectUrl);    response.sendRedirect(redirectUrl);}
复制代码


参考:


[1] https://datatracker.ietf.org/doc/html/rfc6749[2] https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html[3] https://www.apereo.org/projects/cas

发布于: 3 小时前阅读数: 2
用户头像

GrowingIO 技术团队经验分享 2020.05.09 加入

GrowingIO(官网网站www.growingio.com)的官方技术专栏,内容涵盖微服务架构,前端技术,数据可视化,DevOps,大数据方面的经验分享。 公众号:GrowingIO技术团队

评论

发布
暂无评论
Growing 账号认证实践