基于Spring Boot与Keycloak的细粒度权限同步方案问询
解决方案步骤
一、选择权限同步策略
方式1:通过Keycloak Admin API主动推送权限
适合应用端权限更新后实时同步到Keycloak的场景,当管理员在后台分配/修改用户权限时,直接调用Keycloak接口将权限写入Keycloak用户数据中。
- 先引入Keycloak Admin Client依赖:
<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> <version>${keycloak.version}</version> </dependency>
- 编写同步逻辑示例:
@Service public class KeycloakPermissionSyncService { @Value("${keycloak.auth-server-url}") private String authServerUrl; @Value("${keycloak.realm}") private String realm; @Value("${keycloak.resource}") private String clientId; @Value("${keycloak.credentials.secret}") private String clientSecret; public void syncUserPermissions(String userId, Set<String> permissions) { // 初始化Admin Client Keycloak keycloak = KeycloakBuilder.builder() .serverUrl(authServerUrl) .realm(realm) .clientId(clientId) .clientSecret(clientSecret) .grantType(OAuth2Constants.CLIENT_CREDENTIALS) .build(); // 将权限作为用户自定义属性存入Keycloak UserResource userResource = keycloak.realm(realm).users().get(userId); UserRepresentation user = new UserRepresentation(); Map<String, Object> attributes = new HashMap<>(); attributes.put("permissions", permissions); user.setAttributes(attributes); userResource.update(user); // 若需将权限映射为Keycloak Client角色(需提前在客户端创建对应角色) // ClientResource client = keycloak.realm(realm).clients().findByClientId(clientId).get(0); // for (String perm : permissions) { // RoleRepresentation role = client.roles().get(perm).toRepresentation(); // userResource.roles().clientLevel(client.getId()).add(Collections.singletonList(role)); // } } }
方式2:用Keycloak User Storage SPI拉取权限
适合权限数据源固定在Postgres,无需主动推送的场景——Keycloak在用户认证时,自动从应用数据库查询权限并注入用户信息。
- 核心实现:自定义
UserStorageProvider,继承AbstractUserAdapterFederatedStorage,重写getAttributes()或getRoleMappings()方法,从Postgres查询当前用户的权限并返回。
二、配置Keycloak将权限写入JWT Claims
登录Keycloak控制台,进入目标Realm → Clients → 选择你的应用客户端 → Mappers标签页,添加对应映射器:
针对自定义属性存储的权限
- Mapper Type:选择
User Attribute - Name:填
permissions-mapper - User Attribute:填
permissions(需和同步时的属性key一致) - Token Claim Name:填
permissions - Claim JSON Type:选
JSON(因为权限是集合) - 勾选Add to ID token和Add to access token
针对Client角色存储的权限
- Mapper Type:选择
Client Role - Name:填
client-roles-mapper - Client ID:选择你的应用客户端
- Token Claim Name:填
authorities - 勾选Add to ID token和Add to access token
三、配置Spring Security解析JWT权限
让Spring Security正确读取JWT中的权限并转换为GrantedAuthority:
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 对应Keycloak中设置的Claim名称 authoritiesConverter.setAuthoritiesClaimName("permissions"); // 若权限需要前缀(比如用hasRole校验时),可开启此行 // authoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return converter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))); return http.build(); } }
如果权限存在于resource_access.{client-id}.roles中,只需修改authoritiesConverter.setAuthoritiesClaimName("resource_access.your-client-id.roles")即可。
四、验证方法级权限
确保已启用方法级安全:在启动类或配置类上添加@EnableMethodSecurity(Spring Security 6+),或@EnableGlobalMethodSecurity(prePostEnabled = true)(旧版本)。之后即可继续使用@PreAuthorize("hasAuthority('LIST_CURRENCIES')")这类注解完成鉴权。
内容的提问来源于stack exchange,提问作者Ramanan
相关产品推荐
相关产品推荐

