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,配置数据库完成数据本地存储后,正常启动登录到管理控制台,配置可参考

All configuration - Keycloak

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,具体过程就不说了,这里可以直接导入配置

demo-springboot-keycloak/src/main/resources/realm-export.json at master · stevensu1/demo-springboot-keycloak (github.com)

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)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值