在本文中,我们将创建一个名为 messages-webapp 的 Spring MVC + Thymeleaf Web 应用,并使用 Keycloak 进行访问控制,使用 Spring Security OAuth 2.0 进行认证。
你可以在 Github 上找到该项目的完整源码。
使用 Docker Compose 安装 Keycloak 在上一篇文章中,我们已经了解了如何使用 Docker Compose 安装 Keycloak。
创建 docker-compose.yml 文件,内容如下:
version: '3.8' name: spring-security-oauth2-microservices-demo services: keycloak: image: quay.io/keycloak/keycloak:22.0.3 command: ['start-dev'] container_name: keycloak hostname: keycloak environment: - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=admin1234 ports: - "9191:8080" 运行以下命令启动 Keycloak 实例:
$ docker compose up -d 现在,你可以访问 Keycloak 管理控制台 http://localhost:9191/,并使用 admin/admin1234 登录。
创建 Keycloak Realm、客户端和用户 在前面的文章中,我们已经学习了如何创建 Realm、客户端和用户。请按照 前文 中提到的步骤创建新的 Realm、客户端和用户,只需更改 “Valid redirect URIs”。
在之前的文章中,我们学习了使用 Web 浏览器、cURL 和 Postman 进行各种 OAuth 2.0 / OpenID Connect Flow。现在是时候学以致用了。
在使用像 Spring Security 这样的安全框架实现基于 OAuth 2.0 / OpenID Connect 的 Security 时,许多活动都是由框架在幕后执行的。了解框架内部发生的情况非常重要,这样我们才能有效地使用框架。
在本文中,我将简要介绍我们将在 Spring Security OAuth 2 教程系列中使用的基于微服务的示例项目。
示例 Spring Boot 微服务项目 我们将构建一个包含以下组件的微服务示例项目:
Authorization Server - 这是 OAuth 2.0 授权服务器,将向客户端发放访问令牌。我们将使用 Keycloak 作为授权服务器。 messages-webapp - 终端用户将使用的基于 Spring MVC 的客户端应用。 messages-service - Spring Boot REST API(资源服务器)用于管理用户数据(消息),并将由 messages-webapp 应用使用。 archival-service - Spring Boot 应用可使用定时任务定期处理消息存档。它还提供了一个 API 端点来触发存档过程。该服务将同时扮演资源服务器和客户端的角色。 让我们来了解一下这些组件各自提供的功能。
messages-webapp(客户端) 这是一个使用 Spring MVC 和 Thymeleaf 渲染用户界面的 Spring Boot Web 应用。该应用将受到授权服务器(Keycloak)的保护,并将使用 “授权码模式” 来认证用户身份并获取访问令牌。
在 “Spring Security OAuth 2 教程 - 4:PKCE 授权码模式” 中,我们学习了如何通过 PKCE 授权码模式获取访问令牌(access_token)。在本文中,我们将了解如何使用隐式模式(Implicit Flow)和资源所有者密码凭证模式(Resource Owner Password Credentials Flow)。
注意
隐式模式和资源所有者密码凭证模式已被淘汰,没有特别理由不应该再使用。
隐式模式 隐式模式(Implicit Flow)是授权码模式(Authorization Code Flow)的一种简化版本,你可以直接通过授权端点(authorization_endpoint)获取访问令牌(access_token)。
如果你一直关注本系列,就会知道如何创建一个启用了特定 “Authentication flow” 的客户端。这里,我们直接为 messages-webapp 客户端启用隐式模式。
要了解如何创建新客户端,请参阅 创建客户端。
在浏览器窗口中打开以下 URL:
http://localhost:9191/realms/sivalabs/protocol/openid-connect/auth? response_type=id_token%20token &client_id=messages-webapp &redirect_uri=http://localhost:8080/callback &scope=openid%20profile &state=randomstring &nonce=another_randomstring 然后,你将被重定向到 Keycloak 的登录页面。 使用凭证 siva/siva1234 登录。 然后,你将被重定向到包含 access_token 和 id_token 查询参数的重定向 URI。 http://localhost:8080/callback# state=randomstring &session_state=51692fdb-8b72-45b7-a341-fa73e97b5139 &id_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU2MTUzOTQsImlhdCI6MTY5NTYxNDQ5NCwiYXV0aF90aW1lIjoxNjk1NjE0NDk0LCJqdGkiOiI2MjFmNTJmMC0wMDBmLTQ1ZmUtYWYzOC1iY2YzZWM2ZDk1MTEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoibWVzc2FnZXMtd2ViYXBwIiwic3ViIjoiY2ExYTJmMzQtMTYxNC00NWRkLTg2YzEtNWVhZmZmMDg1ZDhhIiwidHlwIjoiSUQiLCJhenAiOiJtZXNzYWdlcy13ZWJhcHAiLCJub25jZSI6ImFub3RoZXJfcmFuZG9tc3RyaW5nIiwic2Vzc2lvbl9zdGF0ZSI6IjUxNjkyZmRiLThiNzItNDViNy1hMzQxLWZhNzNlOTdiNTEzOSIsImF0X2hhc2giOiJiamR6MC1NeWltQ0xrSzdqaWRRbHp3IiwiYWNyIjoiMSIsInNfaGFzaCI6IlJtVE5Ld0lYaTNXRFhzRFlObTQtUHciLCJzaWQiOiI1MTY5MmZkYi04YjcyLTQ1YjctYTM0MS1mYTczZTk3YjUxMzkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6IlNpdmEgS2F0YW1yZWRkeSIsInByZWZlcnJlZF91c2VybmFtZSI6InNpdmEiLCJnaXZlbl9uYW1lIjoiU2l2YSIsImZhbWlseV9uYW1lIjoiS2F0YW1yZWRkeSIsImVtYWlsIjoic2l2YUBnbWFpbC5jb20ifQ.TIcmVBti96HuZvrYe_14mVJlfopXI2PhdMdWBtPPASpJc-DKrL9argy08sYZKqJTTcmWwnIwKK2o1vddVxA4zUP2tnqqg6ymz1trN3J8r4h-WSvIp907vnS0R7iHei56L6MQX2DZLJ8pOdSmti8wg_9fu4gQJBE2sHRTlrlOP39dh8yohMGidM-Z5iFbLCIzOQXA6B6ewMZll5iwL3ssJ716Ve9cO4qHGCneRGpb3mO7jclY87YSGM-wqr6ur00ylQ_BCGyCdl-f-xskSeDX09iQKFSTX_acMxB7FNi21BL7dMx8_22XPFOwNkX8ha8Vb7eTRMYEMyB776i33FLu0A &access_token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeVVPTDg4LVBGM3BYQzFpN3BIeGdFZTJwaWZJY3RyTXJiNklHOElmRTlVIn0.eyJleHAiOjE2OTU2MTUzOTQsImlhdCI6MTY5NTYxNDQ5NCwiYXV0aF90aW1lIjoxNjk1NjE0NDk0LCJqdGkiOiI1N2NiYmRkMC0wNGFmLTRlMDctYWZlNC02ZmQ5MmY0YjA2MzAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkxOTEvcmVhbG1zL3NpdmFsYWJzIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImNhMWEyZjM0LTE2MTQtNDVkZC04NmMxLTVlYWZmZjA4NWQ4YSIsInR5cCI6IkJlYXJlciIsImF6cCI6Im1lc3NhZ2VzLXdlYmFwcCIsIm5vbmNlIjoiYW5vdGhlcl9yYW5kb21zdHJpbmciLCJzZXNzaW9uX3N0YXRlIjoiNTE2OTJmZGItOGI3Mi00NWI3LWEzNDEtZmE3M2U5N2I1MTM5IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtc2l2YWxhYnMiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsInNpZCI6IjUxNjkyZmRiLThiNzItNDViNy1hMzQxLWZhNzNlOTdiNTEzOSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiU2l2YSBLYXRhbXJlZGR5IiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2l2YSIsImdpdmVuX25hbWUiOiJTaXZhIiwiZmFtaWx5X25hbWUiOiJLYXRhbXJlZGR5IiwiZW1haWwiOiJzaXZhQGdtYWlsLmNvbSJ9.XsYc69HnM9VaJZFF568nRiZhh8RYEw6Hq2WGnJ4jr3tmZvgMF0QK2RtlBT9BuX4A11XHjyNZqGYNf55x0k4bPXjhPzWI-lC0shhsKrXGYrnhVcComXxMbO_38ypRY_EMeBWRTXu0bvcKInYMjVoItfoLheH-kbcziK6O16yFGftOG-YYw0uVzs_DrOkjQjs1BS2L56yXcRgN72EBXMT-Cv6OLMTSj6WXjfg1nmRl0NRJdeZv0iafSolmqSpJeqXwPzM2hgZ2hPzaq90qipndQrZ05xesMtzXMNlev0ozYPN7xKSa7arHMYky8y4OMpCQDJzkSwekjUEQUSU9Sqg_VA &token_type=Bearer &expires_in=900 如你所见,access_token 和 id_token 是通过浏览器 URL 前端返回的,这并不安全。
资源所有者密码凭证模式 在 “资源所有者密码凭证模式” 中,我们将使用资源所有者(终端用户)凭证获取 access_token 和 id_token。
在 “Spring Security OAuth 2 教程 - 3:客户端凭证模式” 中,我们学习了如何通过客户端凭证模式获取访问令牌(access_token)。在本文中,我们将了解如何使用 PKCE 授权码模式。
PKCE 授权码模式 PKCE 授权码模式是 OpenId Connect Flow,主要用于保护本地、移动应用和单页应用 (SPA) 的安全。PKCE 是代码交换证明密钥(Proof Key for Code Exchange)的首字母缩写。
注意
PKCE 授权码模式也可用于保护服务器上运行的 Web 应用的安全。在这种情况下,PKCE 起到了额外保护层的作用。
创建 “public” 客户端 创建一个名为 messages-spa 的新客户端。
General Settings: Client type:OpenID Connect Client ID:messages-spa Capability config: Client authentication:Off Authorization:Off Authentication flow:选中 Standard flow,取消选中其余复选框 Login settings: Root URL:http://localhost:3000 Home URL:http://localhost:3000 Valid redirect URIs:http://localhost:3000/callback Valid post logout redirect URIs:http://localhost:3000 Web origins:http://localhost:3000 使用上述配置创建客户端后,你将进入新创建的客户端 “Settings” 页面。单击 “Advanced” 选项卡,转到 “Advanced Settings” 部分,将 “Proof Key for Code Exchange Code Challenge Method” 值更新为 S256。
在第二章 “Spring Security OAuth 2 教程 - 2:授权码模式” 中,我们学习了如何通过授权码模式(Authorization Code Flow)对用户进行身份认证。在本文中,我们将了解如何使用客户端凭证模式(Client Credentials Flow),它通常用于服务之间的通信,而无需任何用户(资源所有者)上下文。
客户端凭证模式 有时资源服务器需要在没有任何用户上下文的情况下与另一个资源服务器进行交互。例如,资源服务器 A 可能会运行一个定时任务,该任务将调用资源服务器 B 上的受保护的 REST API 端点。在这些情况下,我们可以通过 “客户端凭证模式” 从授权服务器(Authorization Server)获取访问令牌(access_token)。
创建客户端并启用客户端凭证模式 客户端使用 “客户端凭证模式”,需要启用 “客户端凭证授权支持”。在 Keycloak 中,可以通过启用 Service accounts roles 认证模式来启用 “客户端凭证授权”。
OAuth2 客户端多重授权
OAuth 2.0 客户端可启用多种授权方式,如授权码、客户端凭证、隐式等。
创建一个名为 archival-service 的新客户端。
General Settings: Client type:OpenID Connect Client ID:archival-service Capability config: Client authentication:On Authorization:Off Authentication flow:选中 Service accounts roles,取消选中其余复选框 Login settings: Root URL:http://localhost:8282 Home URL:http://localhost:8282 使用上述配置创建客户端后,你将进入新创建的客户端 “Settings” 页面。单击 “Credentials” 选项卡并复制 “Client secret”值。
在 “Spring Security OAuth 2 教程 - 1:熟悉 OAuth 2 概念” 中,我们学习了如何设置 Keycloak、创建 Realm、启用 Standard flow 的客户端和用户。在本文中,我们将了解如何通过 “授权码模式”(Authorization Code Flow)对用户进行身份认证。
首先,让我们澄清一下 “授权码授权方式”(Authorization Code Grant Type)与 “授权码模式”(Authorization Code Flow)之间的混淆。
正如我之前提到的,OAuth 2.0 规范仅关注授权(Authorization),而 OpenID Connect 规范是在 OAuth 2.0 之上添加的一层,用于处理身份认证(Authentication)。
“授权码授权方式”(Authorization Code Grant Type)是 OAuth 2.0 的术语,而 “授权码模式”(Authorization Code Flow)是 OpenID Connect 的术语。它们的工作方式相同,区别在于 scope。通过本文后面的示例,我们将更清楚地了解 “授权码模式” 的差异。
快速回顾一下,我们在上一教程中,创建的客户端(Client)和用户(User)的详细信息如下。
Client id:messages-webapp Client secret:qVcg0foCUNyYbgF0Sg52zeIhLYyOwXpQ Username:siva Password:siva1234 OAuth 2.0 架构 以下是基于 OAuth 2.0 的系统的架构图:
资源所有者(Resource Owner),也就是终端用户,希望使用客户端(Client)应用访问其存储在资源服务器(Resource Server)上的数据。 资源服务器(Resource Server)数据受保护,需要访问令牌(access_token)才能访问数据。 客户端(Client)应用将管理用户、签发访问令牌(access_token)和认证用户的责任转交给了授权服务器(Authorization Server)。 当你(Resource Owner)试图在客户端(Client)应用上访问受保护的资源时,你将被重定向到授权服务器(Authorization Server),在那里你需要通过提供用户凭证来认证自己的身份。 如果认证成功,授权服务器(Authorization Server)就会向客户端(Client)签发访问令牌(access_token)。 然后,客户端(Client)就可以使用访问令牌(access_token)访问受保护的用户的数据了。 这是终端用户如何使用基于 OAuth 2.
安全是一个需要深入理解的复杂主题。此外,使用 OAuth 2.0 和 OpenID Connect 规范为基于微服务的复杂系统实现认证和授权更加困难。像 Spring Security 这样的框架和库有助于降低复杂性,但要正确实现 Security 仍然需要经历陡峭的学习曲线。
在本 Spring Security OAuth2 系列教程中,我将与大家分享如何使用 Spring Security OAuth2 为基于微服务的应用实现认证和授权。
有许多身份提供商(Identity Provider)解决方案,如 Keycloak、Okta、Auth0 等。在本系列中,我们将使用开源身份和访问管理解决方案 Keycloak。
我不是安全、OAuth2 和 Keycloak 方面的专家。我只是根据自己对这些概念的理解与大家分享我的学习心得。如果你认为其中有任何概念或解释不正确,请指正。
我们不会直接进入 Spring Security OAuth2 的实现,而是先从概念开始学习,循序渐进。
了解 OAuth 2.0 和 OpenID Connect 的基础知识 学习 OAuth 2.0 和 OpenID Connect 的第一步是了解一些核心概念,如 OAuth2 中的各种角色(Role)是什么、各种授权方式(Grant Type)是什么以及何时使用哪种方式。
简而言之,在 OAuth2 系统中,有各种不同的组件扮演着不同的角色,也有不同的方法来验证用户身份。
OAuth2.0 角色(Role) 资源所有者(Resource Owner): 资源所有者通常是终端终用户,他授权应用(客户端)访问他/她的账户。 资源服务器(Resource Server): 托管受保护资源的服务器。这是你要访问的 API。 客户端(Client): 代表资源所有者请求访问受保护资源的应用(资源所有者正在使用的应用)。 授权服务器(Authorization Server): 对资源所有者进行身份验证并在成功授权后发放访问令牌(Access Token)的服务器。 OAuth2.
1、概览 Spring Security 允许通过继承 WebSecurityConfigurerAdapter 类来自定义 HTTP Security,例如端点授权或 Authentication Manager 配置。然而,在最近的版本中,Spring 已经弃用了这种方法,并推荐使用基于组件的 security 配置。
本文将会带你学习如何在 Spring Boot 应用中代替这些已废弃的方法。
2、Spring Security 不使用 WebSecurityConfigurerAdapter 常见的 Spring HTTP Security 配置类都会继承一个 WebSecurityConfigureAdapter 类。
从 5.7.0-M2 起,WebSecurityConfigureAdapter 被废弃了,不推荐使用。
创建一个基于内存验证的 Spring Boot 应用示例来演示如何进行新的配置。
首先,定义配置类:
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class SecurityConfig { // config } 添加 Method Security 注解,根据不同角色进行处理。
2.1、配置 Authentication 使用 WebSecurityConfigureAdapter 时,使用 AuthenticationManagerBuilder 来设置 Authentication Context。
现在,可以定义一个 UserDetailsManager 或 UserDetailsService 来避免组件过时的问题:
1、概览 Web 应用中,有些资源只能被已登录(认证)的的用户访问。有些资源,可以被匿名用户访问。而有些资源,甚至只能被匿名用户访问,已登录的用户不能访问。
本文将带你了解 Spring Security 中 HttpSecurity 的 permitAll() 和 anonymous() 方法之间的区别以及如何通过这两个方法实现上述的权限设计。
2、权限设计 假如,我们有一个电商网站,权限设计如下:
匿名用户和已登录的用户均可查看网站上的商品。 需要审计匿名用户和已登录用户请求。 匿名用户可以访问用户注册页面,已经登录的用户则不能访问。 只有已登录的用户才能查看其购物车。 3、Controller 和 WebSecurity 配置 定义 Controller:
@RestController public class EcommerceController { @GetMapping("/private/showCart") public @ResponseBody String showCart() { return "Show Cart"; } @GetMapping("/public/showProducts") public @ResponseBody String listProducts() { return "List Products"; } @GetMapping("/public/registerUser") public @ResponseBody String registerUser() { return "Register User"; } } 接下来在 EcommerceWebSecruityConfig 类中实现上述权限设计:
@Configuration @EnableWebSecurity public class EcommerceWebSecurityConfig { @Bean public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { UserDetails user = User.
1、概览 使用 Spring Security,可以为应用配置身份认证和授权,以控制方法(如端点)的访问权限。
在 5.6 之前,使用 @EnableGlobalMethodSecurity 注解是标准的做法,在 5.6 之后,@EnableMethodSecurity 引入了一种更灵活的方法来配置方法安全授权(Method Security)。
在本教程中,我们将通过示例代码了解 @EnableMethodSecurity 如何代替 @EnableGlobalMethodSecurity,以及他们之间的区别。
2、@EnableMethodSecurity 和 @EnableGlobalMethodSecurity 让我们来看看方法授权如何与 @EnableMethodSecurity 和 @EnableGlobalMethodSecurity 配合使用。
2.1、@EnableMethodSecurity 通过 @EnableMethodSecurity,可以看到 Spring Security 为授权类型(Authorization Type)采用基于 Bean 的配置。
我们现在为每种类型都设置了一个配置,而不是全局配置。例如,Jsr250MethodSecurityConfiguration:
@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) class Jsr250MethodSecurityConfiguration { // ... @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor jsr250AuthorizationMethodInterceptor() { return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager); } @Autowired(required = false) void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) { this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix()); } } MethodInterceptor 主要包含一个 AuthorizationManager,它现在将 “检查和返回最终决策的 AuthorizationDecision 对象” 的责任委托给适当的实现,这里是 AuthenticatedAuthorizationManager。