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并单击"创建"。

image.png

创建该领域时,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拷贝到里面,看到信息如下:

image.png

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容