這是一個Java 數據庫應用原型,使用 Spring Boot 和容器進行測試、Keycloak 提供安全、PostgreSQL 提供數據持久化的,帶有 REST 和安全功能。
在工作中開發時,我多次需要一個簡單應用的模板,以便基於此模板開始為手頭的項目添加特定代碼。
在本文中,我將創建一個簡單的 Java 應用程序,它連接到數據庫,暴露一些 REST 端點,並使用基於角色的訪問來保護這些端點。
目的是擁有一個最小化且功能齊全的應用程序,然後可以針對特定任務進行定製。
對於數據庫,我們將使用 PostgreSQL;對於安全,我們將使用 Keycloak,兩者都通過容器部署。在開發過程中,我使用 podman 來測試容器是否正確創建(作為 docker 的替代品——它們在大多數情況下可以互換)作為一次學習體驗。
應用程序本身是使用 Spring Boot 框架開發的,並使用 Flyway 進行數據庫版本管理。
所有這些技術都是 Java EE 領域業界標準,在項目中被使用的可能性很高。
我們構建原型的核心需求是一個圖書館應用程序,它暴露 REST 端點,允許創建作者、書籍以及它們之間的關係。這將使我們能夠實現一個多對多關係,然後可以將其擴展用於任何可以想象的目的。
完整可用的應用程序可以在 https://github.com/ghalldev/db_proto 找到。
本文中的代碼片段取自該代碼庫。
在創建容器之前,請確保使用您偏好的值定義以下環境變量(教程中故意省略了它們,以避免傳播多個用户使用的默認值):
DOCKER_POSTGRES_PASSWORD
DOCKER_KEYCLOAK_ADMIN_PASSWORD
DOCKER_GH_USER1_PASSWORD
配置 PostgreSQL:
docker container create --name gh_postgres --env POSTGRES_PASSWORD=$DOCKER_POSTGRES_PASSWORD --env POSTGRES_USER=gh_pguser --env POSTGRES_INITDB_ARGS=--auth=scram-sha-256 --publish 5432:5432 postgres:17.5-alpine3.22
docker container start gh_postgres
配置 Keycloak:
首先是容器的創建並啓動:
docker container create --name gh_keycloak --env DOCKER_GH_USER1_PASSWORD=$DOCKER_GH_USER1_PASSWORD --env KC_BOOTSTRAP_ADMIN_USERNAME=gh_admin --env KC_BOOTSTRAP_ADMIN_PASSWORD=$DOCKER_KEYCLOAK_ADMIN_PASSWORD --publish 8080:8080 --publish 8443:8443 --publish 9000:9000 keycloak/keycloak:26.3 start-dev
docker container start gh_keycloak
在容器啓動並運行後,我們可以繼續創建領域、用户和角色(這些命令必須在正在運行的容器內部執行):
cd $HOME/bin
./kcadm.sh config credentials --server http://localhost:8080 --realm master --user gh_admin --password $KC_BOOTSTRAP_ADMIN_PASSWORD
./kcadm.sh create realms -s realm=gh_realm -s enabled=true
./kcadm.sh create users -s username=gh_user1 -s email="gh_user1@email.com" -s firstName="gh_user1firstName" -s lastName="gh_user1lastName" -s emailVerified=true -s enabled=true -r gh_realm
./kcadm.sh set-password -r gh_realm --username gh_user1 --new-password $DOCKER_GH_USER1_PASSWORD
./kcadm.sh create roles -r gh_realm -s name=viewer -s 'description=Realm role to be used for read-only features'
./kcadm.sh add-roles --uusername gh_user1 --rolename viewer -r gh_realm
./kcadm.sh create roles -r gh_realm -s name=creator -s 'description=Realm role to be used for create/update features'
./kcadm.sh add-roles --uusername gh_user1 --rolename creator -r gh_realm
ID_ACCOUNT_CONSOLE=$(./kcadm.sh get clients -r gh_realm --fields id,clientId | grep -B 1 '"clientId" : "account-console"' | grep -oP '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}')
./kcadm.sh update clients/$ID_ACCOUNT_CONSOLE -r gh_realm -s 'fullScopeAllowed=true' -s 'directAccessGrantsEnabled=true'
用户 gh_user1 在領域 gh_realm 中被創建,並擁有 viewer 和 creator 角色。
您可能已經注意到,我們沒有創建新的客户端,而是使用了 Keycloak 自帶的一個默認客户端:account-console。這是為了方便起見,在實際場景中,您會創建一個特定的客户端,然後將其更新為具有 fullScopeAllowed(這會導致領域角色被添加到令牌中——默認情況下不添加)和 directAccessGrantsEnabled(允許通過 Keycloak 的 openid-connect/token 端點生成令牌,在我們的例子中使用 curl)。
創建的角色隨後可以在 Java 應用程序內部使用,以根據我們約定的契約來限制對某些功能的訪問——viewer 只能訪問只讀操作,而 creator 可以執行創建、更新和刪除操作。當然,同樣地,可以根據任何原因創建各種角色,只要約定的契約被明確定義並被所有人理解。
角色還可以進一步添加到組中,但本教程不包含這部分內容。
但是,在實際使用這些角色之前,我們必須告訴 Java 應用程序如何提取角色——這是必須的,因為 Keycloak 將角色添加到 JWT 的方式是其特有的,所以我們必須編寫一段自定義代碼,將其轉換為 Spring Security 可以使用的東西:
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
// 遵循與 org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter 相同的模式
Converter<Jwt, Collection<GrantedAuthority>> keycloakRolesConverter = new Converter<>() {
private static final String DEFAULT_AUTHORITY_PREFIX = "ROLE_";
//https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java#L901
private static final String KEYCLOAK_REALM_ACCESS_CLAIM_NAME = "realm_access";
private static final String KEYCLOAK_REALM_ACCESS_ROLES = "roles";
@Override
public Collection<GrantedAuthority> convert(Jwt source) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Map<String, List<String>> realmAccess = source.getClaim(KEYCLOAK_REALM_ACCESS_CLAIM_NAME);
if (realmAccess == null) {
logger.warn("No " + KEYCLOAK_REALM_ACCESS_CLAIM_NAME + " present in the JWT");
return grantedAuthorities;
}
List<String> roles = realmAccess.get(KEYCLOAK_REALM_ACCESS_ROLES);
if (roles == null) {
logger.warn("No " + KEYCLOAK_REALM_ACCESS_ROLES + " present in the JWT");
return grantedAuthorities;
}
roles.forEach(
role -> grantedAuthorities.add(new SimpleGrantedAuthority(DEFAULT_AUTHORITY_PREFIX + role)));
return grantedAuthorities;
}
};
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(keycloakRolesConverter);
return jwtAuthenticationConverter;
}
在 AppConfiguration 類中還完成了其他重要配置,例如啓用方法安全性和禁用 CSRF。
現在我們可以在 REST 控制器中使用 @org.springframework.security.access.prepost.PreAuthorize 註解來限制訪問:
@PostMapping("/author")
@PreAuthorize("hasRole('creator')")
public void addAuthor(@RequestParam String name, @RequestParam String address) {
authorService.add(new AuthorDto(name, address));
}
@GetMapping("/author")
@PreAuthorize("hasRole('viewer')")
public String getAuthors() {
return authorService.allInfo();
}
通過這種方式,只有成功通過身份驗證且擁有 hasRole 中列出的角色的用户才能調用端點,否則他們將收到 HTTP 403 Forbidden 錯誤。
在容器啓動並配置完成後,Java 應用程序可以啓動了,但在啓動之前需要添加數據庫密碼——這可以通過環境變量完成(下面是一個 Linux shell 示例):
export SPRING_DATASOURCE_PASSWORD=$DOCKER_POSTGRES_PASSWORD
現在,如果一切正常啓動並運行,我們可以使用 curl 來測試我們的應用程序(以下所有命令均為 Linux shell 命令)。
使用之前創建的用户 gh_user1 登錄並提取身份驗證令牌:
KEYCLOAK_ACCESS_TOKEN=$(curl -d 'client_id=account-console' -d 'username=gh_user1' -d "password=$DOCKER_GH_USER1_PASSWORD" -d 'grant_type=password' 'http://localhost:8080/realms/gh_realm/protocol/openid-connect/token' | grep -oP '"access_token":"\K[^"]*')
創建一個新作者(這將測試 creator 角色是否有效):
curl -X POST --data-raw 'name="GH_name1"&address="GH_address1"' -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'
檢索庫中的所有作者(這將測試 viewer 角色是否有效):
curl -X GET -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'
至此,您應該擁有了創建自己的 Java 應用程序所需的一切,可以根據需要對其進行擴展和配置。
【注】本文譯自:Java Spring Boot Template With PostgreSQL, Keycloak Securit