keycloak24整合spring security oauth2
上一篇文章我们通过rh-sso-7.6以及对应的以及红帽提供的适配器Spring Boot adapter完成对springboot的整合,但是鉴权是不方便的,需要在自己的客户端后台里一个一个把资源路径定义上关联好权限才能完成验证,用的是hm的验证逻辑而不是项目自己的,很不方便,而且支持的springboot版本太低了,keycloak24虽然也有对应的适配器Securing Applications and Services Guide (keycloak.org),但是还是不好维护的。
这里我们把版本升级到最新版24,然后接着用spring security oauth2的方式使用它。
具体步骤如下:
1,下载keycloak24,并改端口8180,配置数据库完成数据本地存储后,正常启动登录到管理控制台,配置可参考

db=mysql
db-username=root
db-password=root
db-url=jdbc:mysql://localhost:3306/keycloak
http-port=8180
启动:bin\kc.bat start-dev
2,在SpringBootKeycloak域下创建客服端login-app,具体过程就不说了,这里可以直接导入配置

3,创建springboot项目导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
4,项目配置
### server port server.port=8080 #ClientRegistration类保存了关于客户端的所有基本信息。Spring自动配置会查找带有schema # spring.security.oauth2.client.registration.[registrationId]的属性, # 并注册一个具有OAuth 2.0或OpenID Connect(OIDC)的客户端。 spring.security.oauth2.client.registration.keycloak.client-id=login-app spring.security.oauth2.client.registration.keycloak.client-secret=K9IfVKjj1H0ujciRWsO8eU9b9dMdHfPh spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.keycloak.scope=openid #Spring Boot应用程序需要与OAuth 2.0或OIDC提供程序进行交互,以处理不同授权类型的实际请求逻辑。 #因此,我们需要配置OIDC提供程序。可以根据属性值使用spring.security.oauth2.client.provider.[provider name]模式进行自动配置。 spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/realms/SpringBootKeycloak spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username #Keycloak服务器验证JWT令牌。 spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/realms/SpringBootKeycloak
5,通过创建一个SecurityFilterChain bean来配置HttpSecurity。此外,我们还需要使用http.oauth2Login()来启用OAuth2登录。
/**
* 我们通过创建一个SecurityFilterChain bean来配置HttpSecurity。
* 此外,我们还需要使用http.oauth2Login()来启用OAuth2登录。
* @param httpSecurity
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/*"))
.permitAll()
.anyRequest()
.authenticated());
httpSecurity.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults()));
httpSecurity.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return httpSecurity.build();
}
5.1 解析令牌
/**
* Keycloak返回一个包含所有相关信息的令牌。为了使Spring Security能够根据用户分配的角色做出决策,我们必须解析令牌并提取相关的细节。 然而,Spring
* Security通常会在每个角色名称前添加“ROLES_”前缀, 而Keycloak发送的是纯角色名称。为了解决这个问题,我们创建一个帮助方法,将从Keycloak检索到的每个角色添加“ROLE_”前缀。
*/
Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
Collectors.toList());
}
/**
* 解析令牌。首先,我们需要检查令牌是否为OidcUserAuthority或OAuth2UserAuthority的实例。
* 由于Keycloak令牌可以是任一类型,所以我们需要实现一个解析逻辑。下面的代码会检查令牌的类型,并决定解析机制。
*
* @return
*/
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
var authority = authorities.iterator().next();
boolean isOidc = authority instanceof OidcUserAuthority;
if (isOidc) {
var oidcUserAuthority = (OidcUserAuthority) authority;
var userInfo = oidcUserAuthority.getUserInfo();
// Tokens can be configured to return roles under
// Groups or REALM ACCESS hence have to check both
if (userInfo.hasClaim("realm_access")) {
var realmAccess = userInfo.getClaimAsMap("realm_access");
var roles = (Collection<String>) realmAccess.get("roles");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
} else if (userInfo.hasClaim("groups")) {
Collection<String> roles = userInfo.getClaim("groups");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("realm_access")) {
Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get("realm_access");
Collection<String> roles = (Collection<String>) realmAccess.get("roles");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
return mappedAuthorities;
};
}
完整类:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class SecurityConfig {
private final KeycloakLogoutHandler keycloakLogoutHandler;
SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
/**
* 我们通过创建一个SecurityFilterChain bean来配置HttpSecurity。
* 此外,我们还需要使用http.oauth2Login()来启用OAuth2登录。
* @param httpSecurity
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/*"))
.permitAll()
.anyRequest()
.authenticated());
httpSecurity.oauth2ResourceServer((oauth2) -> oauth2
.jwt(Customizer.withDefaults()));
httpSecurity.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return httpSecurity.build();
}
/**
* Keycloak返回一个包含所有相关信息的令牌。为了使Spring Security能够根据用户分配的角色做出决策,我们必须解析令牌并提取相关的细节。 然而,Spring
* Security通常会在每个角色名称前添加“ROLES_”前缀, 而Keycloak发送的是纯角色名称。为了解决这个问题,我们创建一个帮助方法,将从Keycloak检索到的每个角色添加“ROLE_”前缀。
*/
Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(
Collectors.toList());
}
/**
* 解析令牌。首先,我们需要检查令牌是否为OidcUserAuthority或OAuth2UserAuthority的实例。
* 由于Keycloak令牌可以是任一类型,所以我们需要实现一个解析逻辑。下面的代码会检查令牌的类型,并决定解析机制。
*
* @return
*/
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
var authority = authorities.iterator().next();
boolean isOidc = authority instanceof OidcUserAuthority;
if (isOidc) {
var oidcUserAuthority = (OidcUserAuthority) authority;
var userInfo = oidcUserAuthority.getUserInfo();
// Tokens can be configured to return roles under
// Groups or REALM ACCESS hence have to check both
if (userInfo.hasClaim("realm_access")) {
var realmAccess = userInfo.getClaimAsMap("realm_access");
var roles = (Collection<String>) realmAccess.get("roles");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
} else if (userInfo.hasClaim("groups")) {
Collection<String> roles = userInfo.getClaim("groups");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("realm_access")) {
Map<String, Object> realmAccess = (Map<String, Object>) userAttributes.get("realm_access");
Collection<String> roles = (Collection<String>) realmAccess.get("roles");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
return mappedAuthorities;
};
}
}
6:我们需要处理来自Keycloak的注销。为此,我们添加KeycloakLogoutHandler类:
@Component
public class KeycloakLogoutHandler implements LogoutHandler {
private static final Logger logger = LoggerFactory.getLogger(KeycloakLogoutHandler.class);
private final RestTemplate restTemplate;
public KeycloakLogoutHandler(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
logoutFromKeycloak((OidcUser) auth.getPrincipal());
}
private void logoutFromKeycloak(OidcUser user) {
String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout";
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.getIdToken().getTokenValue());
ResponseEntity<String> logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
if (logoutResponse.getStatusCode().is2xxSuccessful()) {
logger.info("Successfulley logged out from Keycloak");
} else {
logger.error("Could not propagate logout to Keycloak");
}
}
}
7:使用注解控制权限“:@PreAuthorize("hasRole('user-role11')")
@Controller
@RequestMapping("/web")
public class WebController {
// http://127.0.0.1:8080/web/index
@ResponseBody
@GetMapping(path = "/index")
@PreAuthorize("hasRole('user-role11')")
public String index() {
return "index";
}
// http://127.0.0.1:8080/web/logout
@GetMapping("/logout")
public String logout(HttpServletRequest request) throws Exception {
request.logout();
return "redirect:/";
}
// http://127.0.0.1:8080/web/customers
@GetMapping(path = "/customers")
@ResponseBody
public String customers(Principal principal, Model model) {
return "customers";
}
}
到此就完了,跑起来后访问首页进入登录页面:http://127.0.0.1:8080/web/index

登录后成功返回带有权限控制的数据。

把权限改成别的则是不能访问


到此到此呢我们的整合就算是成功了,但是其他微服务模块要通过Feign调用这个接口又怎么处理,以及注意什么我们后面在看吧,今天就到这,先出帖代码:stevensu1/demo-springboot-keycloak (github.com)
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)