SpringBoot Security集成Keycloak
Keycloak 是一种开源身份和访问管理解决方案,它使使用很少或根本没有代码的现代应用程序和服务变得容易。之前我也有很多文章介绍过如何使用和配置keycloak以及如何在Nginx里来集成Keycloak,这篇文章主要介绍如何在SpringBoot Security集成Keycloak来对微服务的API进行认证和授权。
环境准备
- Keycloak 9.0.2版本
- SpringBoot Security 2.3.4.RELEASE
注意:在Keycloak 8.0.2版本以及使用keycloak-spring-boot-starter 8.0.2版本上运行文中的实例有问题,之上的版本也没有测试。如何安装Keycloak可以到官网上去下载,或者使用Docker方式安装。
Keycloak配置
首先,让我们在 Keycloak中进行一下配置,详细的方法可以参考我之前的博文。
创建领域
Keycloak里的领域是管理一组用户、凭据、角色和组和客户端。用户属于并登录到一个领域。领域彼此隔离,只能管理和验证他们控制的用户。
转到 http://localhost:8080/auth/admin/ 并使用管理员登录到管理控制台,管理员身份可以在启动时设置。
从主下拉菜单中,单击"添加领域"。当您登录到主领域时,此下拉菜单列出了所有的领域。
在名称字段中键入Demo-Realm并单击"创建"。
创建该领域时,master领域的管理员控制台页面将打开。切换到新创建的Demo-Realm领域。项目中尽量避免使用Master领域,应该自己创建一个领域。
创建客户端
客户端是可以请求Keycloak对用户进行身份验证的实体。大多数情况下,客户端是希望使用 Keycloak 来保护自己并提供单个登录解决方案的应用程序和服务,比如一个微服务程序,Nginx客户端等都可以作为一个客户端。客户端也可以是只想要请求身份信息或访问令牌的实体,以便他们能够安全地调用Keycloak保护的网络上的其他服务。
第一步,单击左窗格中的"client"菜单。所选领域下的所有可用客户端都会呈现在列表中。如下图,
image.png
第二步,要创建新客户端,请单击"Create"按钮。您将被提示为客户ID、客户协议和ROOT URL。客户ID的Y一般是是你的应用程序的名称(比如myMicroservice),客户端协议应设置为openid-connect,root URL应设置为应用程序URL。
image.png
第三步,保存后,您将返回到客户端配置页面,如果需要,您可以填写客户端名字和描述。
将Access Type设置为confidential、Authorization Enabled设置为on、启用Service Account,并单击"保存"。
Credentials选项卡将显示客户端密钥,这个在后面的微服务程序里需要配置。
image.png
第四步,转到Client Roles选项卡来创建客户端角色。想象一下,您正在微服务具有不同类型的用户,具有不同的用户权限。比如,user和admin,一些读取API 仅可供user访问。管理员可能会访问更多的API。根据示例,让我们创建两个角色:Uer和Admin,单击"添加角色"按钮。
image.png
创建领域角色
Keycloak分为client role 和realm role,realm role可以组合多个client role。应用程序通常将访问权限和权限分配给特定角色,而不是单个用户,因为与用户打交道可能过于精细,难以管理。
第一步,我们创建两个realm角色app-user和app-admin,并赋予相应的client role(user和admin)单击左窗格中的"Role"菜单。所选领域的所有可用角色都会显示在列表中。
image.png
第二步,要创建realm角色app-user,请单击"Add Role"。您将被提示为角色名称和描述。提供以下详细信息并保存。
保存后,启用Composite Roles,并在"Client roles"字段下搜索刚创建的client。选择user角色,然后单击"Add Selected"
image.png
此配置将赋予app-user领域角色到客户端的user角色。也可以添加多个客户端角色到此realm角色。
第三步,按照上面的步骤,创建app-admin用户,并赋予客户端admin角色。
创建用户
Keycloak中的用户是能够登录到你的系统的实体,user是整个领域生效,不是在客户端中。他们可以拥有与自己相关的属性,如电子邮件、用户名、地址、电话号码和出生日。他们可以被分配到group,并分配给他们特定的角色。
让我们创建以下用户,并授予他们用于app-user和app-admin角色。
- employee1拥有app-user realm 角色
- employee2拥有app-admin realm角色
-
employee3拥有app-admin和app-user两个角色
第一步,从菜单中,单击"user"以打开用户列表页面。
第二步,在空用户列表的右侧,单击"add user"以打开添加的用户页面。
第三步,在用户名字段中输入名称employee1,然后单击"保存"以保存数据并为新用户打开管理页面。其他字段可以输入也可以不输,邮件也可以作为登录的唯一标志使用。因此,如果输入邮件,必须保证唯一性。
image.png
第四步,点击Credentials标签页,为这个用户设置临时密码。
image.png
第五步,输入一个新密码并确认新密码,点击"Reset Password"设置用户的新密码。
第六步,单击"Role mapping"选项卡,向用户分配领域角色。领域角色列表将在可用角色列表中提供。选择一个所需的角色,然后单击"已添加已选>将其分配给用户。
角色分配后,分配的角色将在分配的角色列表下可用。员工1、员工2和员工的角色分配如下。
image.png
完成上面的realm,client,user和role的创建和配置后,Keycloak的基本配置功能都已经完成,更高级的配置可以去官网学习。
产生tokens
Keycloak提供了一系列的endpoint来供用户访问,包括如何为用户产生access token。
第一步,选择Realm settings,点击"OpenID Endpoint Configuration"能看到OpenID Endpoint的详细信息。如下图,
image.png
第二步,从OpenID Endpoint Configuration拷贝token_endpoint,URL大致结构如下:
<KEYCLOAK_SERVER_URL>/auth/realms/<REALM_NAME>/protocol/openid-connect/token
例如:http://localhost:8080/auth/realms/Demo-Realm/protocol/openid-connect/token
第三步,使用postman或者curl来访问token endpoint来获取access token。如下:
curl -X POST '<KEYCLOAK_SERVER_URL>/auth/realms/<REALM_NAME>/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>' \
--data-urlencode 'username=<USERNAME>' \
--data-urlencode 'password=<PASSWORD>'
例如:
curl -X POST 'http://localhost:8080/auth/realms/Demo-Realm/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=springboot-microservice' \
--data-urlencode 'client_secret=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx' \
--data-urlencode 'username=employee1' \
--data-urlencode 'password=xxxxxx'
使用之前创建的client的client_secret和密码替换里面相应的信息。获取到的结果如下图:
image.png
access token是一个JWT token,因此可以使用jwt工具来查看里面的信息。可以访问https://jwt.io,将access token拷贝到里面,看到信息如下:
SpingBoot里集成Keycloak
创建一个SpingBoot模板创建一个SpringBoot security项目。
Step1,添加keycloak-spring-boot-starter依赖。
<properties>
<java.version>11</java.version>
<keycloak.version>9.0.2</keycloak.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${keycloak.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak.version}</version>
</dependency>
</dependencies>
注意版本,在8.0.2上不能使用下面的代码,需要配置keycloak.json文件。
第二步,增加Keycloak配置
在application.properties中增加如下配置,
keycloak.realm = Demo-Realm
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.ssl-required = external
keycloak.resource = springboot-microservice
keycloak.credentials.secret = XXXXXXXXXXXXXXXXXXXXXXXXX
keycloak.use-resource-role-mappings = true
keycloak.bearer-only = true
使用你的client secret替换keycloak.credentials.secret 属性和client id替换keycloak.resource。
第三步,增加KeycloakSecurityConfig.java配置属性类。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.anyRequest()
.permitAll();
http.csrf().disable();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
- configureGlobal:在authentication manager中注册KeycloakAuthenticationProvider。
- SessionAuthenticationStrategy:定义会话身份验证策略。
- KeycloakConfigResolver:默认情况下,Spring Security Adapter 寻找 keycloak.json配置文件。您可以通过此bean来确保使用SpringBoot adapter提供的的配置
- @EnableGlobalMethodSecurity:jsr250Enabled 属性允许我们使用@RoleAllowed注释。
使用@RolesAllowed注解来实现Role-base的访问控制
在一个API方法上增加RolesAllowed注解就可以轻松的实现role based的API访问控制。如果不加此注解,则默认所有用户都可以访问。如下实例:
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping(value = "/anonymous", method = RequestMethod.GET)
public ResponseEntity<String> getAnonymous() {
return ResponseEntity.ok("Hello Anonymous");
}
@RolesAllowed("user")
@RequestMapping(value = "/user", method = RequestMethod.GET)
public ResponseEntity<String> getUser(@RequestHeader String Authorization) {
return ResponseEntity.ok("Hello User");
}
@RolesAllowed("admin")
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public ResponseEntity<String> getAdmin(@RequestHeader String Authorization) {
return ResponseEntity.ok("Hello Admin");
}
@RolesAllowed({ "admin", "user" })
@RequestMapping(value = "/all-user", method = RequestMethod.GET)
public ResponseEntity<String> getAllUser(@RequestHeader String Authorization) {
return ResponseEntity.ok("Hello All User");
}
}
访问这些API前,需要使用token endpoint来获取access token,再访问这些API.访问的结果应该如下:
curl -X GET 'http://localhost:8000/test/user' \
--header 'Authorization: bearer <ACCESS_TOKEN>'
Outputs:
anonymous: 403 Forbidden
employee1: Hello User
employee2: 403 Forbidden
employee3: Hello User
curl -X GET 'http://localhost:8000/test/admin' \
--header 'Authorization: bearer <ACCESS_TOKEN>'
Outputs:
anonymous: 403 Forbidden
employee1: 403 Forbidden
employee2: Hello Admin
employee3: Hello Admin
curl -X GET 'http://localhost:8000/test/all-user' \
--header 'Authorization: bearer <ACCESS_TOKEN>'
Outputs:
anonymous: 403 Forbidden
employee1: Hello All User
employee2: Hello All User
employee3: Hello All User
如果token设置为5分钟的过期时间,则会出现401 unauthorized 错误
Security开启和关闭
有时为了开发和调试方便,需要关闭security的相关功能,网上有很多的方法因为SpringBoot的版本不同不太好用,下面是我经过测试和验证的方法,使用@ConditionalOnProperty注解来注入不同的bean类。在上面的代码中
@ConditionalOnProperty(prefix = "security", name = "enabled", havingValue = "true")
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
增加了该条件注解,当security.enableld=true时才会注入KeycloakSecurityConfig这个Bean类。此外,需要增加下面的注解类来disable web security。
@Configuration
@ConditionalOnProperty(prefix = "security", name = "enabled", havingValue = "false", matchIfMissing = true)
public class KeycloakSecurityDisable extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web){
web.ignoring().antMatchers("/**");
}
}
在application.properties中增加如下的配置来关闭security。
security.enabled = false
security.ignored=/**
security.basic.enabled=false
management.security.enabled=false
在application.properties中增加如下的配置来开启security。
security.enabled = true
security.ignored=/**
security.basic.enabled=false
management.security.enabled=false