13-SpringSecurity:OpenID与Keycloak

简介: 13-SpringSecurity:OpenID与Keycloak

背景


本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验 SpringSecurity 的各项Feature。


目前 SpringSecurity 新版本除了实现对 OAuth2.0 的支持外,还支持 OpenIDSAML


果然,Spring Security不仅是一个功能强大且可高度自定义的身份验证和访问控制框架,它还是保护基于Spring的应用程序的事实标准。


SpringSecurity 本身提供了 GOOGLE  GITHUB  FACEBOOK  OKTAOAuth2.0 接入支持,具体源码在枚举类 CommonOAuth2Provider 中。上一篇文章:12-SpringSecurity:通过OAuth2集成Github登录实现了 GithubOAuth2.0 接入,这次实现对 OIDC 提供方(eg: Okta , Keycloak , Authing )的集成, 这里主要使用由 Keycloak 提供的 OIDC (OpenID Connect) 服务,实现 Spring Security 5 集成 OIDC 单点登录。。


  • OIDC (OpenID Connect)是什么?

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, to request and receive information about authenticated sessions and end-users. The specification suite is extensible, allowing participants to use optional features such as encryption of identity data, discovery of OpenID Providers, and session management, when it makes sense for them.

OAuth2.0 是一种授权协议, OIDC 是一个基于 OAuth2.0 授权协议的身份认证层:


(Identity, Authentication) + OAuth2.0 = OpenID Connect

  • Keycloak 是什么?
    Open Source Identity and Access Management for Modern Applications and Services。


即: Keycloak 是一种面向现代应用和服务的开源 IAM (身份识别与访问管理:Identity and Access Management)解决方案。我们可以使用 Keycloak 来搭建一个属于自己的 OIDC 认证-授权服务器。


实验0:Keycloak使用


Keycloak 的使用可参Keycloak官方文档,官方文档简洁明了,没有一句废话。

主要步骤是这样的:下载安装-启动-创建admin用户-登录admin控制台-创建域-创建用户-创建应用。


我创建了一个 admin 用户,登录之后是下图这样,可见 Keycloak 提供了全面的认证、授权服务,功能很强大;然后创建了一个 Heartsuit 域。

image.png

Heartsuit 域下创建了一个用户: auth 

image.png

Heartsuit 域下创建了一个应用: springsecurity 

image.png

需要注意的是,由于我们后续使用授权码模式进行登录,所以应用配置中:


  • Access Type 选择 confidential ;然后在 Credential 或者 Installation 可以看到 Secret


  • Valid Redirect URIs 配置为: http://localhost:8000/login/oauth2/code/keycloak

image.png

image.png

至此,我们得到了一个应用:

{
  "realm": "heartsuit",
  "auth-server-url": "http://localhost:8080/auth/",
  "ssl-required": "external",
  "resource": "springsecurity",
  "credentials": {
    "secret": "6b532289-4c11-4e62-acc0-5c67e13e4736"
  },
  "confidential-port": 0
}


实验1:Keycloak登录


image.png

(1) 注册应用


在前面注册的应用,生成了 client-id (resource), client-secret

(secret)。


(2) 配置 application.yml

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: http://localhost:8080/auth/realms/heartsuit
        registration:
          keycloak:
            client-id: springsecurity
            client-secret: 6b532289-4c11-4e62-acc0-5c67e13e4736
            clientName: Keycloak
          scope:
            - openid
            - profile
            - email
server:
  port: 8000

(3) 启动应用


为了看到登录成功后的效果,这里增加一个 Controller ;然后运行应用。

@GetMapping(value = "/")
    public String index(Principal principal) {
        return "Welcome " + principal;
    }

在浏览器键入: http://localhost:8000/login ,返回一个页面,其中包含了 Keycloak 登录链接:

image.png

点击 Keycloak 登录链接,会自动跳转至 我们创建的 Keycloak 服务认证页:

image.png

输入在 Heartsuit 域下创建的用户: auth ,会进入我们之前配置的Home页,显示用户信息。


Note: 如果我们直接在浏览器中输入 http://localhost:8000 则会自动跳转到http://localhost:8080/auth/realms/heartsuit/protocol/openid-connect/auth?response_type=code&client_id=springsecurity&state=Gd5Xj0PyueFcDtoQ6zC6w2wSVc4XjAbAFn8q_uu0qes%3D&redirect_uri=http://localhost:8000/login/oauth2/code/keycloak 链接。


可通过链接退出: http://localhost:8000/logout

借助 SpringSecurityOpenID 的支持,我们几乎不用写什么代码就实现了 Keycloak 登录集成。下面简单了解下登录成功后的 RegistrationAccessToken


实验2:查看Keycloak在我们应用中的注册信息


为了方便调试或查看 registration ,这里新增一个接口端点:

@GetMapping(value = "/user/reg")
    public String registration() {
        ClientRegistration keycloakRegistration = this.clientRegistrationRepository.findByRegistrationId("keycloak");
        log.info(keycloakRegistration.toString());
        return keycloakRegistration.toString();
    }

访问之后会返回 registration 信息,其中包含了 clientIdclientSecretauthorizationGrantTyperedirectUriscopes 等。

image.png


实验3:查看获取到的AccessToken


@GetMapping(value = "/user/token")
    public OAuth2AccessToken accessToken(OAuth2AuthenticationToken authentication) {
        OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
                authentication.getAuthorizedClientRegistrationId(), authentication.getName());
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
        return accessToken;
    }

image.png

显然,这里的 tokenValue 是一个 JWT 字符串,我们到 https://jwt.io 解析一下:

image.png


实验4:通过AccessToken请求Keycloak的用户信息接口


定义抽象 API 绑定类,通过拦截器将获取到的 AccessToken 设置到后续请求头中,通过 RestTemplate 实现对 API 的请求:

public abstract class ApiBinding {
    protected RestTemplate restTemplate;
    public ApiBinding(String accessToken) {
        this.restTemplate = new RestTemplate();
        if (accessToken != null) {
            this.restTemplate.getInterceptors().add(getBearerTokenInterceptor(accessToken));
        } else {
            this.restTemplate.getInterceptors().add(getNoTokenInterceptor());
        }
    }
    private ClientHttpRequestInterceptor getBearerTokenInterceptor(String accessToken) {
        return new ClientHttpRequestInterceptor() {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
                request.getHeaders().add("Authorization", "Bearer " + accessToken);
                return execution.execute(request, bytes);
            }
        };
    }
    private ClientHttpRequestInterceptor getNoTokenInterceptor() {
        return new ClientHttpRequestInterceptor() {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
                throw new IllegalStateException("Can't access the Keycloak API without an access token");
            }
        };
    }
}

将获取 AccessToken 的过程进行封装:

@Configuration
@Slf4j
public class SocialConfig {
    @Bean
    @RequestScope
    public Keycloak keycloak(OAuth2AuthorizedClientService clientService) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String accessToken = null;
        String userInfoEndpointUri = null;
        if (authentication.getClass().isAssignableFrom(OAuth2AuthenticationToken.class)) {
            OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
            String clientRegistrationId = oauthToken.getAuthorizedClientRegistrationId();
            if (clientRegistrationId.equals("keycloak")) {
                OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(clientRegistrationId, oauthToken.getName());
                if (client != null) {
                    accessToken = client.getAccessToken().getTokenValue();
                    userInfoEndpointUri = client.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();
                }
                log.info(accessToken);
                log.info(userInfoEndpointUri);
            }
        }
        return new Keycloak(accessToken, userInfoEndpointUri);
    }
}
public class Keycloak extends ApiBinding {
    private String userInfoEndpointUri;
    public Keycloak(String accessToken, String userInfoEndpointUri) {
        super(accessToken);
        this.userInfoEndpointUri = userInfoEndpointUri;
    }
    public String getProfile() {
        return restTemplate.getForObject(userInfoEndpointUri, String.class);
    }
}

Controller 中新增接口:通过 AccessToken 获取 keycloak 用户信息:

@GetMapping(value = "/user/info")
    public String info() {
        String profile = keycloak.getProfile();
        log.info(keycloak.getProfile());
        return profile;
    }

image.png

Controller 的完整代码:

@RestController
@Slf4j
public class HelloController {
    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;
    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;
    @Autowired
    Keycloak keycloak;
    @GetMapping(value = "/")
    public String index(Principal principal) {
        return "Welcome " + principal;
    }
    @GetMapping(value = "/user/reg")
    public String registration() {
        ClientRegistration keycloakRegistration = this.clientRegistrationRepository.findByRegistrationId("keycloak");
        log.info(keycloakRegistration.toString());
        return keycloakRegistration.toString();
    }
    @GetMapping(value = "/user/token")
    public OAuth2AccessToken accessToken(OAuth2AuthenticationToken authentication) {
        OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
                authentication.getAuthorizedClientRegistrationId(), authentication.getName());
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
        return accessToken;
    }
    @GetMapping(value = "/user/info")
    public String info() {
        String profile = keycloak.getProfile();
        log.info(keycloak.getProfile());
        return profile;
    }
}

最后,有两个端点需要说明一下,这在我们对接一些支持 OpenID 登录其他第三方应用时要用到:


  1. http://localhost:8080/auth/realms/heartsuit/, 我们在 application.yml 配置中使用过;


  1. http://localhost:8080/auth/realms/heartsuit/.well-known/openid-configuration,第三方应用会自动请求该链接,这在 SpringSecurity 官方文档中也有说明。

image.png

image.png


目录
相关文章
|
4月前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
4月前
|
安全 Java 数据库
实现基于Spring Security的权限管理系统
实现基于Spring Security的权限管理系统
|
4月前
|
安全 Java 数据安全/隐私保护
解析Spring Security中的权限控制策略
解析Spring Security中的权限控制策略
|
5月前
|
JSON 安全 Java
Spring Security 6.x 微信公众平台OAuth2授权实战
上一篇介绍了OAuth2协议的基本原理,以及Spring Security框架中自带的OAuth2客户端GitHub的实现细节,本篇以微信公众号网页授权登录为目的,介绍如何在原框架基础上定制开发OAuth2客户端。
204 4
Spring Security 6.x 微信公众平台OAuth2授权实战
|
5月前
|
存储 安全 Java
Spring Security 6.x OAuth2登录认证源码分析
上一篇介绍了Spring Security框架中身份认证的架构设计,本篇就OAuth2客户端登录认证的实现源码做一些分析。
229 2
Spring Security 6.x OAuth2登录认证源码分析
|
5月前
|
安全 Java 数据安全/隐私保护
Spring Security 6.x 一文快速搞懂配置原理
本文主要对整个Spring Security配置过程做一定的剖析,希望可以对学习Spring Sercurity框架的同学所有帮助。
266 5
Spring Security 6.x 一文快速搞懂配置原理
|
5月前
|
安全 Java API
Spring Security 6.x 图解身份认证的架构设计
【6月更文挑战第1天】本文主要介绍了Spring Security在身份认证方面的架构设计,以及主要业务流程,及核心代码的实现
83 1
Spring Security 6.x 图解身份认证的架构设计
|
4月前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
4月前
|
安全 Java 数据安全/隐私保护
使用Java和Spring Security实现身份验证与授权
使用Java和Spring Security实现身份验证与授权
|
4月前
|
存储 安全 Java
Spring Security在企业级应用中的应用
Spring Security在企业级应用中的应用