Java數據庫應用原型
這是一個(gè)Java 數據庫應用原型,使用 Spring Boot 和容器進(jìn)行測試、Keycloak 提供安全、PostgreSQL 提供數據持久化的,帶有 REST 和安全功能。

在工作中開(kāi)發(fā)時(shí),我多次需要一個(gè)簡(jiǎn)單應用的模板,以便基于此模板開(kāi)始為手頭的項目添加特定代碼。
在本文中,我將創(chuàng )建一個(gè)簡(jiǎn)單的 Java 應用程序,它連接到數據庫,暴露一些 REST 端點(diǎn),并使用基于角色的訪(fǎng)問(wèn)來(lái)保護這些端點(diǎn)。
目的是擁有一個(gè)最小化且功能齊全的應用程序,然后可以針對特定任務(wù)進(jìn)行定制。
對于數據庫,我們將使用 PostgreSQL;對于安全,我們將使用 Keycloak,兩者都通過(guò)容器部署。在開(kāi)發(fā)過(guò)程中,我使用 podman 來(lái)測試容器是否正確創(chuàng )建(作為 docker 的替代品——它們在大多數情況下可以互換)作為一次學(xué)習體驗。
應用程序本身是使用 Spring Boot 框架開(kāi)發(fā)的,并使用 Flyway 進(jìn)行數據庫版本管理。
所有這些技術(shù)都是 Java EE 領(lǐng)域業(yè)界標準,在項目中被使用的可能性很高。
我們構建原型的核心需求是一個(gè)圖書(shū)館應用程序,它暴露 REST 端點(diǎn),允許創(chuàng )建作者、書(shū)籍以及它們之間的關(guān)系。這將使我們能夠實(shí)現一個(gè)多對多關(guān)系,然后可以將其擴展用于任何可以想象的目的。
完整可用的應用程序可以在 https://github.com/ghalldev/db_proto 找到。
本文中的代碼片段取自該代碼庫。

在創(chuàng )建容器之前,請確保使用您偏好的值定義以下環(huán)境變量(教程中故意省略了它們,以避免傳播多個(gè)用戶(hù)使用的默認值):
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:
首先是容器的創(chuàng )建并啟動(dòng):
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
在容器啟動(dòng)并運行后,我們可以繼續創(chuàng )建領(lǐng)域、用戶(hù)和角色(這些命令必須在正在運行的容器內部執行):
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'
用戶(hù) gh_user1 在領(lǐng)域 gh_realm 中被創(chuàng )建,并擁有 viewer 和 creator 角色。
您可能已經(jīng)注意到,我們沒(méi)有創(chuàng )建新的客戶(hù)端,而是使用了 Keycloak 自帶的一個(gè)默認客戶(hù)端:account-console。這是為了方便起見(jiàn),在實(shí)際場(chǎng)景中,您會(huì )創(chuàng )建一個(gè)特定的客戶(hù)端,然后將其更新為具有 fullScopeAllowed(這會(huì )導致領(lǐng)域角色被添加到令牌中——默認情況下不添加)和 directAccessGrantsEnabled(允許通過(guò) Keycloak 的 openid-connect/token 端點(diǎn)生成令牌,在我們的例子中使用 curl)。
創(chuàng )建的角色隨后可以在 Java 應用程序內部使用,以根據我們約定的契約來(lái)限制對某些功能的訪(fǎng)問(wèn)——viewer 只能訪(fǎng)問(wèn)只讀操作,而 creator 可以執行創(chuàng )建、更新和刪除操作。當然,同樣地,可以根據任何原因創(chuàng )建各種角色,只要約定的契約被明確定義并被所有人理解。
角色還可以進(jìn)一步添加到組中,但本教程不包含這部分內容。
但是,在實(shí)際使用這些角色之前,我們必須告訴 Java 應用程序如何提取角色——這是必須的,因為 Keycloak 將角色添加到 JWT 的方式是其特有的,所以我們必須編寫(xiě)一段自定義代碼,將其轉換為 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#L901private static final String KEYCLOAK_REALM_ACCESS_CLAIM_NAME = "realm_access";private static final String KEYCLOAK_REALM_ACCESS_ROLES = "roles";@Overridepublic 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 類(lèi)中還完成了其他重要配置,例如啟用方法安全性和禁用 CSRF。
現在我們可以在 REST 控制器中使用 @org.springframework.security.access.prepost.PreAuthorize 注解來(lái)限制訪(fǎng)問(wèn):
@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();
}
通過(guò)這種方式,只有成功通過(guò)身份驗證且擁有 hasRole 中列出的角色的用戶(hù)才能調用端點(diǎn),否則他們將收到 HTTP 403 Forbidden 錯誤。
在容器啟動(dòng)并配置完成后,Java 應用程序可以啟動(dòng)了,但在啟動(dòng)之前需要添加數據庫密碼——這可以通過(guò)環(huán)境變量完成(下面是一個(gè) Linux shell 示例):
export SPRING_DATASOURCE_PASSWORD=$DOCKER_POSTGRES_PASSWORD
現在,如果一切正常啟動(dòng)并運行,我們可以使用 curl 來(lái)測試我們的應用程序(以下所有命令均為 Linux shell 命令)。
使用之前創(chuàng )建的用戶(hù) 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[^"]*')
創(chuàng )建一個(gè)新作者(這將測試 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'
至此,您應該擁有了創(chuàng )建自己的 Java 應用程序所需的一切,可以根據需要對其進(jìn)行擴展和配置。
【注】本文譯自:Java Spring Boot Template With PostgreSQL, Keycloak Securit
