今日咱们测验Spring Security整合Keycloak,并决定建立一个非常简单的Spring Boot微服务,运用Keycloak作为我的身份验证源,运用Spring Security处理身份验证和授权。

设置Keycloak


  • 首要咱们需求一个Keycloak实例,让咱们发动Jboss供给的Docker容器:
docker run -d \
  --name springboot-security-keycloak-integration \
  -e KEYCLOAK_USER=admin \
  -e KEYCLOAK_PASSWORD=admin \
  -p 9001:8080 \
  jboss/keycloak
  • 在此之后,咱们只需登录到容器并导航到bin文件夹。
docker exec -it springboot-security-keycloak-integration /bin/bash
cd keycloak/bin
  • 首要,咱们需求从CLI客户端登录keycloak服务器,之后咱们不再需求身份验证:
./kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin --password admin

装备realm


  • 首要,咱们需求创立一个realm:
./kcadm.sh create realms -s realm=springboot-security-keycloak-integration -s enabled=true
Created new realm with id 'springboot-security-keycloak-integration'
  • 之后,咱们需求创立2个客户端,这将为咱们的运用程序供给身份验证。首要咱们创立一个cURL客户端,这样咱们就能够经过命令行命令登录:
./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=curl -s enabled=true -s publicClient=true -s baseUrl=http://localhost:8080 -s adminUrl=http://localhost:8080 -s directAccessGrantsEnabled=true
Created new client with id '8f0481cd-3bbb-4659-850f-6088466a4d89'

重要的是要留意2个选项:publicClient=truedirectAccessGrantsEnabled=true。第一个使这个客户端公开,这意味着咱们的cURL客户端能够在不供给任何秘密的情况下发动登录。第二个使咱们能够运用用户名和暗码直接登录。

  • 其次,咱们创立了一个由REST服务运用的客户端:
./kcadm.sh create clients -r springboot-security-keycloak-integration -s clientId=springboot-security-keycloak-integration-client -s enabled=true -s baseUrl=http://localhost:8080 -s bearerOnly=true
Created new client with id 'ab9d404e-6d5b-40ac-9bc3-9e2e26b68213'

这里的重要装备是bearerOnly=true。这告知Keycloak客户端永远不会发动登录过程,可是当它收到Bearer令牌时,它将检查所述令牌的有用性。

咱们应该留意保存这些ID,由于咱们将在接下来的步骤中运用它们。

  • 咱们有两个客户端,接下来是为spring-security-keycloak-example-app客户创立人物

Admin Role:

./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=admin -s 'description=Admin role'
Created new role with id 'admin'

User Role:

./kcadm.sh create clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/roles -r springboot-security-keycloak-integration -s name=user -s 'description=User role'
Created new role with id 'user'

留意client后的id是咱们创立客户端输出的id

  • 最终,咱们应该获取客户端的装备,以便稍后供给给咱们的运用程序:
./kcadm.sh  get clients/ab9d404e-6d5b-40ac-9bc3-9e2e26b68213/installation/providers/keycloak-oidc-keycloak-json -r springboot-security-keycloak-integration

留意client后的id是咱们创立客户端输出的id

应该返回相似于此的内容:

{
  "realm" : "springboot-security-keycloak-integration",
  "bearer-only" : true,
  "auth-server-url" : "http://localhost:8080/auth",
  "ssl-required" : "external",
  "resource" : "springboot-security-keycloak-integration-client",
  "verify-token-audience" : true,
  "use-resource-role-mappings" : true,
  "confidential-port" : 0
}

装备用户


出于演示意图,咱们创立2个具有2个不同人物的用户,以便咱们验证授权是否有用。

  • 首要,让咱们创立一个具有admin人物的用户:

创立admin用户:

./kcadm.sh create users -r springboot-security-keycloak-integration -s username=admin -s enabled=true
Created new user with id '50c11a76-a8ff-42b1-80cb-d82cb3e7616d'

设置admin暗码:

./kcadm.sh update users/50c11a76-a8ff-42b1-80cb-d82cb3e7616d/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -n

value: 用户暗码

追加到admin人物中

./kcadm.sh add-roles -r springboot-security-keycloak-integration --uusername=admin --cclientid springboot-security-keycloak-integration-client --rolename admin

留意:从不在生产中运用此办法,它仅用于演示意图!

  • 然后咱们创立另一个用户,这次有人物user:

创立user用户:

./kcadm.sh create users -r springboot-security-keycloak-integration -s username=user -s enabled=true
Created new user with id '624434c8-bce4-4b5b-b81f-e77304785803'

设置user暗码:

./kcadm.sh update users/624434c8-bce4-4b5b-b81f-e77304785803/reset-password -r springboot-security-keycloak-integration -s type=password -s value=admin -s temporary=false -n

追加到user人物中:

./kcadm.sh add-roles -r springboot-security-keycloak-integration --uusername=user --cclientid springboot-security-keycloak-integration-client --rolename user

Rest服务


咱们已经装备了Keycloak并准备运用,咱们只需求一个运用程序来运用它!所以咱们创立一个简单的Spring Boot运用程序。我会在这里运用maven构建项目:

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.edurt.sski</groupId>
    <artifactId>springboot-security-keycloak-integration</artifactId>
    <packaging>jar</packaging>
    <version>1.0.0</version>
    <name>springboot security keycloak integration</name>
    <description>SpringBoot Security KeyCloak Integration is a open source springboot, spring security, keycloak
        integration example.
    </description>
    <properties>
        <!-- dependency config -->
        <dependency.lombox.version>1.16.16</dependency.lombox.version>
        <dependency.springboot.common.version>1.5.6.RELEASE</dependency.springboot.common.version>
        <dependency.keycloak.version>3.1.0.Final</dependency.keycloak.version>
        <!-- plugin config -->
        <plugin.maven.compiler.version>3.3</plugin.maven.compiler.version>
        <plugin.maven.javadoc.version>2.10.4</plugin.maven.javadoc.version>
        <!-- environment config -->
        <environment.compile.java.version>1.8</environment.compile.java.version>
        <!-- reporting config -->
        <reporting.maven.jxr.version>2.5</reporting.maven.jxr.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${dependency.springboot.common.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${dependency.lombox.version}</version>
        </dependency>
        <!-- springboot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- keycloak -->
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>${dependency.keycloak.version}</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-security-adapter</artifactId>
            <version>${dependency.keycloak.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${plugin.maven.compiler.version}</version>
                <configuration>
                    <source>${environment.compile.java.version}</source>
                    <target>${environment.compile.java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <version>${plugin.maven.javadoc.version}</version>
                <configuration>
                    <aggregate>true</aggregate>
                    <!-- custom tags -->
                    <tags>
                        <tag>
                            <name>Description</name>
                            <placement>test</placement>
                            <head>description</head>
                        </tag>
                    </tags>
                    <!-- close jdoclint check document -->
                    <additionalparam>-Xdoclint:none</additionalparam>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jxr-plugin</artifactId>
                <version>${reporting.maven.jxr.version}</version>
            </plugin>
        </plugins>
    </reporting>
</project>

增加所有必需的依赖项:

  • spring-security 用于维护运用程序
  • keycloak-spring-boot-starter 运用Keycloak和Spring Boot
  • keycloak-spring-security-adapter 与Spring Security集成

一个简单的运用类:

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.edurt.sski;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * <p> SpringBootSecurityKeyCloakIntegration </p>
 * <p> Description : SpringBootSecurityKeyCloakIntegration </p>
 * <p> Author : qianmoQ </p>
 * <p> Version : 1.0 </p>
 * <p> Create Time : 2019-02-18 14:45 </p>
 * <p> Author Email: <a href="mailTo:shichengoooo@163.com">qianmoQ</a> </p>
 */
@SpringBootApplication
public class SpringBootSecurityKeyCloakIntegration {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurityKeyCloakIntegration.class, args);
    }
}

Rest API接口:

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.edurt.sski.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p> HelloController </p>
 * <p> Description : HelloController </p>
 * <p> Author : qianmoQ </p>
 * <p> Version : 1.0 </p>
 * <p> Create Time : 2019-02-18 14:50 </p>
 * <p> Author Email: <a href="mailTo:shichengoooo@163.com">qianmoQ</a> </p>
 */
@RestController
public class HelloController {
    @GetMapping(value = "/admin")
    @Secured("ROLE_ADMIN")
    public String admin() {
        return "Admin";
    }
    @GetMapping("/user")
    @Secured("ROLE_USER")
    public String user() {
        return "User";
    }
}

最终是keycloak装备:

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.edurt.sski.config;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
 * <p> KeycloakSecurityConfigurer </p>
 * <p> Description : KeycloakSecurityConfigurer </p>
 * <p> Author : qianmoQ </p>
 * <p> Version : 1.0 </p>
 * <p> Create Time : 2019-02-18 14:51 </p>
 * <p> Author Email: <a href="mailTo:shichengoooo@163.com">qianmoQ</a> </p>
 */
@Configuration
@EnableWebSecurity
public class KeycloakSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter {
    @Bean
    public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
        SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
        mapper.setConvertToUpperCase(true);
        return mapper;
    }
    @Override
    protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
        final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider();
        provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper());
        return provider;
    }
    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new NullAuthenticatedSessionStrategy();
    }
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/admin").hasRole("ADMIN")
                .antMatchers("/user").hasRole("USER")
                .anyRequest().permitAll();
    }
    @Bean
    KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
            final KeycloakAuthenticationProcessingFilter filter) {
        final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }
    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
            final KeycloakPreAuthActionsFilter filter) {
        final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }
}

KeycloakSecurityConfigurer类扩展 KeycloakWebSecurityConfigurerAdapter,这是Keycloak供给的类,它供给与Spring Security的集成。

然后咱们经过增加SimpleAuthorityMapper装备身份验证管理器,它担任转换来自Keycloak的人物名称以匹配Spring Security的约定。基本上Spring Security期望以ROLE_前缀开头的人物,ROLE_ADMIN能够像Keycloak相同命名咱们的人物,或许咱们能够将它们命名为admin,然后运用此映射器将其转换为大写并增加必要的ROLE_前缀:

@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
  SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
  mapper.setConvertToUpperCase(true);
  return mapper;
}
@Override
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
  final KeycloakAuthenticationProvider provider = super.keycloakAuthenticationProvider();
  provider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper());
  return provider;
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
  auth.authenticationProvider(keycloakAuthenticationProvider());
}

咱们还需求为Keycloak设置会话策略,可是当咱们创立无状况REST服务时,咱们并不真的想要有会话,因此咱们运用NullAuthenticatedSessionStrategy:

@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
  return new NullAuthenticatedSessionStrategy();
}

通常,Keycloak Spring Security集成从keycloak.json文件中解析keycloak装备,可是咱们期望有适当的Spring Boot装备,因此咱们运用Spring Boot掩盖装备解析器:

@Bean
KeycloakConfigResolver keycloakConfigResolver() {
  return new KeycloakSpringBootConfigResolver();
}

然后咱们装备Spring Security来授权所有请求:

@Override
protected void configure(final HttpSecurity http) throws Exception {
  super.configure(http);
  http
      .authorizeRequests()
      .anyRequest().permitAll();
}

最终,依据文档,咱们阻挠两层注册Keycloak的过滤器:

@Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
    final KeycloakAuthenticationProcessingFilter filter) {
  final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
  registrationBean.setEnabled(false);
  return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
    final KeycloakPreAuthActionsFilter filter) {
  final FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
  registrationBean.setEnabled(false);
  return registrationBean;
}

最终,咱们需求application.properties运用之前下载的值装备咱们的运用程序 :

server.port=9002
keycloak.realm=springboot-security-keycloak-integration
keycloak.bearer-only=true
keycloak.auth-server-url=http://localhost:9001/auth
keycloak.ssl-required=external
keycloak.resource=springboot-security-keycloak-integration-client
keycloak.use-resource-role-mappings=true
keycloak.principal-attribute=preferred_username

运用运用程序


  • 运用curl咱们创立的客户端进行身份验证,以获取拜访令牌:
export TOKEN=`curl -ss --data "grant_type=password&client_id=curl&username=admin&password=admin" http://localhost:9001/auth/realms/springboot-security-keycloak-integration/protocol/openid-connect/token | jq -r .access_token`

这将收到的拜访令牌存储在TOKEN变量中。

现在咱们能够检查咱们的管理员是否能够拜访自己的/admin接口

curl -H "Authorization: bearer $TOKEN" http://localhost:9002/admin
Admin

但它无法拜访/user接口:

$ curl -H "Authorization: bearer $TOKEN" http://localhost:9002/user
{"timestamp":1498728302626,"status":403,"error":"Forbidden","message":"Access is denied","path":"/user"}

对于user用户也是如此,user用户无法拜访admin接口。

源码地址:GitHub