关于spring-security:Growing-账号认证实践

45次阅读

共计 6105 个字符,预计需要花费 16 分钟才能阅读完成。

背景

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 服务获取 token
location /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 的接口,第三方零碎能够拿到受权码之后调用此接口获取 token
location /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
@RequiredArgsConstructor
public 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/…
[2] https://www.ruanyifeng.com/bl…
[3] https://www.apereo.org/projec…

正文完
 0