postman Keycloak保护Spring Boot Restful API接口Spring Security整合实例

postman Keycloak保护Spring Boot Restful API接口Spring Security整合实例

1340发表于2019-12-14

最近在研究企业级统一认证授权方案,找到一个非常好用的开源框架Keycloak,感觉很不错开箱即用,而且还是支持OpenID协议。

现在我已经在Spring boot中整合Spring Security调试成功了。

一般情况有两种情况:

1、Spring Boot写的Restful API,前后端分离(我们公司目前采用的方案)

2、普通的Spring 项目,前后端未分离的情况。登录页面和后台是一个工程。

这两情况的Spring Security, Keycloak都是支持的。我们就先来看看第1种情况,利用Keycloak保护我们的后台API。

使用工具及框架

1、postman

2、JDK1.8

3、Spring boot

4、Spring Security

角色映射:

用户名
拥有角色
lanhu
Librarian,Member
lanhusoft
Mng

url角色映射:

url
拥有角色
/hello
Librarian,Member
/admin
Mng

一、Keycloak服务配置

1、创建两个用户

from clipboard

用户为lanhu和lanhusoft并且密码设置为123456

from clipboard

2、创建三个角色

from clipboard


from clipboard

3、用户添加角色

为用户lanhu添加我们之前新加的角色(Librarian和Member)

from clipboard

为用户lanhusoft添加我们之前新加的角色(Mng)

from clipboard

4、创建客户端app-api1

这个客户端用于前端获取Access_Token:

from clipboard

注意,我们选择的协议是openid-connect,Access Type为public。

5、创建客户端tutorial-backend

这个客户端用于Spring boot接口工程与Keycloak交互。

from clipboard


二、创建Spring Boot项目并整合Spring Security

1、创建Spring Boot项目

from clipboard

pom.xml:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lanhusoft</groupId>
    <artifactId>keycloak-api</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <oauth2-autoconfigure.version>2.1.6.RELEASE</oauth2-autoconfigure.version>
        <dependency.keycloak.version>7.0.1</dependency.keycloak.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-spring-boot-starter -->
        <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>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

</project>


2、整合Spring Security和Keycloak配置类



KeycloakConfig:


package com.lanhusoft.config;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Copyright www.lanhusoft.com
 * Author:Apex Zheng
 * Date:2019/12/11
 * Description:
 */
@Configuration
public class KeycloakConfig {

    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
}
KeycloakSecurityConfigurer:



package com.lanhusoft.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
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.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.method.configuration.EnableGlobalMethodSecurity;
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.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

/**
 * Copyright www.lanhusoft.com
 * Author:Apex Zheng
 * Date:2019/12/12
 * Description:
 */
@KeycloakConfiguration
public class KeycloakSecurityConfigurer  extends KeycloakWebSecurityConfigurerAdapter {
    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     *
     * Since Spring Security requires that role names start with "ROLE_",
     * a SimpleAuthorityMapper is used to instruct the KeycloakAuthenticationProvider
     * to insert the "ROLE_" prefix.
     *
     * e.g. Librarian -> ROLE_Librarian
     *
     * Should you prefer to have the role all in uppercase, you can instruct
     * the SimpleAuthorityMapper to convert it by calling:
     * {@code grantedAuthorityMapper.setConvertToUpperCase(true); }.
     * The result will be: Librarian -> ROLE_LIBRARIAN.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
        grantedAuthorityMapper.setPrefix("ROLE_");

        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     *
     * RegisterSessionAuthenticationStrategy is used because this is a public application
     * from the Keycloak point of view.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    /**
     * Define an HttpSessionManager bean only if missing.
     *
     * This is necessary because since Spring Boot 2.1.0, spring.main.allow-bean-definition-overriding
     * is disabled by default.
     */
    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    /**
     * Define security constraints for the application resources.
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .authorizeRequests()
                .antMatchers("/hello").hasAnyRole("Member", "Librarian")
                .antMatchers("/admin").hasRole("Mng")
                .anyRequest().permitAll()
                .and().csrf().disable();
    }
}
上面指定路径/hello需要角色Member或Librarian,路径/admin需要角色Mng。下面我们创建一个Controller提供这两个url。


HomeController


package com.lanhusoft.controller;

import org.keycloak.KeycloakSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * Copyright www.lanhusoft.com
 * Author:Apex Zheng
 * Date:2019/12/11
 * Description:
 */
@RestController
public class HomeController {
    private final HttpServletRequest request;

    @Autowired
    public HomeController(HttpServletRequest request) {
        this.request = request;
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @GetMapping("/admin")
    public String admin(){
        return "admin";
    }
}


application.properties


server.port=9091
keycloak.realm=master
keycloak.resource=tutorial-backend
keycloak.bearer-only=true
keycloak.credentials.secret=9caafc82-a47c-408f-ac45-73044561a570
keycloak.auth-server-url=http://10.241.89.58:8080/auth
keycloak.ssl-required=external
keycloak.confidential-port: 0


logging.level.org.springframework.security= DEBUG


三、postman测试验证


1、lanhu获取token

使用,用户lanhu来获取token,

http://10.241.89.58:8080/auth/realms/master/protocol/openid-connect/token

from clipboard

返回内容:


{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1MkFzS3B3M3lKdE92dm9rRTgzRF9RWVozb05VMkYtMmNGTV81c2RwbmF3In0.eyJqdGkiOiJlNDlmZDM3OC0zNzEyLTRhN2EtODRlNi00Y2NkYzM5OGY2NTUiLCJleHAiOjE1NzYzMjQ2ODcsIm5iZiI6MCwiaWF0IjoxNTc2MzAzMDg3LCJpc3MiOiJodHRwOi8vMTAuMjQxLjg5LjU4OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjZiMTA0NTJlLTIwZjQtNDM2MC05ZDZkLTIzNjQxOTQ1OGE1OCIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcC1hcGkxIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNDRmM2NjMGEtM2U0Ni00ZTAyLWJlMDEtOTY1OGY4ZTk4ZGFkIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiTGlicmFyaWFuIiwiTWVtYmVyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoi6JOd54uQIOi9r-S7tiIsInByZWZlcnJlZF91c2VybmFtZSI6Imxhbmh1IiwiZ2l2ZW5fbmFtZSI6IuiTneeLkCIsImZhbWlseV9uYW1lIjoi6L2v5Lu2IiwiZW1haWwiOiIyMzUxMzEwNzUxQHFxLmNvbSJ9.PSXT28-hSyBaHUdjhWXXyZl6-1T3K5xE-sp3iSZRsaiNf3ls9V6GJc13llWbsxmN-N_ugzs7WFebbCCZ99ObN6gBlsu4dxDBVaiV1g-ZZEET_NS7RnhOsUrwb3812EjvjZlEpSZPmlEQXOqQAqXnmRtxljYbxFyQVuOvjQjHcJJuV6UBYBzPyUBxgDxbIXyiBHZEnVMtsGANCUqNsi8h1GfBLWtDYLLcGcva-VNw-wUZj0349NYErfscdGxh__Zy98gLk42FUhcPmRF2ompuB91ivgVKbPZtwbuQXbdQAGtf_PCUvckj7VzX7eeol3hDB0z3Y0MAnJJf92POjkIttg",
    "expires_in": 21600,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4M2ViOTVkOC1hYWZhLTQ2YTEtYmQwMC0yYzliZGE5Y2Q0NTIifQ.eyJqdGkiOiJjMDljMDVjYS04OGY0LTRlZmEtYWQxZC1lMGFhMTY1ZGFlNzIiLCJleHAiOjE1NzYzMDQ4ODcsIm5iZiI6MCwiaWF0IjoxNTc2MzAzMDg3LCJpc3MiOiJodHRwOi8vMTAuMjQxLjg5LjU4OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiaHR0cDovLzEwLjI0MS44OS41ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6IjZiMTA0NTJlLTIwZjQtNDM2MC05ZDZkLTIzNjQxOTQ1OGE1OCIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhcHAtYXBpMSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjQ0ZjNjYzBhLTNlNDYtNGUwMi1iZTAxLTk2NThmOGU5OGRhZCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiTGlicmFyaWFuIiwiTWVtYmVyIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIn0.oumvPShobAytPiHhQCvc7j-EIsP3_vuJMU3GEh0hr2o",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "44f3cc0a-3e46-4e02-be01-9658f8e98dad",
    "scope": "profile email"
}


2、lanhu使用token访问接口

http://localhost:9091/hello,使用上面获取到的access_token加到Authorization的bearer后面。

from clipboard


我们再访问http://localhost:9091/admin

from clipboard

可以看到返回403,没有权限。因为/admin需要角色Mng,也就是用户lanhusoft才能访问

from clipboard

3、使用,用户lanhusoft来获取token

http://10.241.89.58:8080/auth/realms/master/protocol/openid-connect/token

from clipboard

返回内容:


{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1MkFzS3B3M3lKdE92dm9rRTgzRF9RWVozb05VMkYtMmNGTV81c2RwbmF3In0.eyJqdGkiOiJlNzFlMTVjNi04OTUzLTQ0ZTEtYWJmOS02ZDY0MGU5Mzc0OWUiLCJleHAiOjE1NzYzMjU2MTQsIm5iZiI6MCwiaWF0IjoxNTc2MzA0MDE0LCJpc3MiOiJodHRwOi8vMTAuMjQxLjg5LjU4OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjExZTkzZjUxLWJlNGItNDAzZC05OTUzLWQyZDI1OWE1Mzk3MSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcC1hcGkxIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiZTNjOGRmNTktN2UyNi00MTFiLTllMjMtNzA3YTFkMDE4YmI0IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsIk1uZyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBwLWFwaTEiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IndlaSB6aGVuZyIsInByZWZlcnJlZF91c2VybmFtZSI6Imxhbmh1c29mdCIsImdpdmVuX25hbWUiOiJ3ZWkiLCJmYW1pbHlfbmFtZSI6InpoZW5nIiwiZW1haWwiOiI2OTE4MDcyMzdAcXEuY29tIn0.aBjHTh6zS7-pntDu7F1iltS5NvwHebxF4Crfn63FwF4fyppgXl9nPqMvl1P7qdnSbDwTq-rZ7buwK0aS-Q2Iy_ZIptojd_SLRbqzXstWFt27VDdoMu7tn1sLefq5m6fSGbV1isZoJVs3AUt5KCrkOHgvdXaIVWOGQFki39JOznqW2hZtjR1jvUq6_hTP6USxtTosudxJZfnFwyZkVmGKcPt_6WFEcXjgWHtQjqz_ridv1Cxr6eGMSBfoCWvnNOOe8v9Y25RPBA6BvlEiTy5JV3AHni4_-7N5JyjaAGZauMIy6pwkkKNBQsdr1mkNvP92Cg7KFV0U5cJw-kdJyICCeA",
    "expires_in": 21600,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4M2ViOTVkOC1hYWZhLTQ2YTEtYmQwMC0yYzliZGE5Y2Q0NTIifQ.eyJqdGkiOiI1YTY1NjY4NC03OWEyLTRmODQtYTU2Ni0xNzE0YzJkZTE0OTEiLCJleHAiOjE1NzYzMDU4MTQsIm5iZiI6MCwiaWF0IjoxNTc2MzA0MDE0LCJpc3MiOiJodHRwOi8vMTAuMjQxLjg5LjU4OjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiaHR0cDovLzEwLjI0MS44OS41ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6IjExZTkzZjUxLWJlNGItNDAzZC05OTUzLWQyZDI1OWE1Mzk3MSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhcHAtYXBpMSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImUzYzhkZjU5LTdlMjYtNDExYi05ZTIzLTcwN2ExZDAxOGJiNCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsIk1uZyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBwLWFwaTEiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCJ9._0qLk9_4u7ZZ56mLrUhVriD8_S8kcNmsiv8cHIv8IKc",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "e3c8df59-7e26-411b-9e23-707a1d018bb4",
    "scope": "profile email"
}
4、lanhusoft用新获取的token访问接口


http://localhost:9091/admin

from clipboard

现在,可以返回正常内容了。那么我们又使用这个token去访问/hello呢?

from clipboard

不出所料,现在这个地址返回403了,因为lanhusoft只有角色Mng,是没有接口/hello权限的。

本文源码:https://github.com/lanhusoft/keycloak-protect-springboot-api


小编蓝狐