keycloak-鉴权springboot3后端服务

一、问题描述

springboot一般都用于后端服务,所以keycloak只需要验证JWT是否满足鉴权即可,此方式在keycloak中客户端配置bear-only方式。然而springboot3不再直接集成keycloak,yml中直接配置keycloak不起作用。那么,springboot3如何集成keycloak鉴权jwt呢?

二、解决方案

2.1 环境描述

springboot:3.0.1

keycloak: 18.0.1

2.2 解决方案

1、pom.xml 引入jar包

集成5个包,包括keycloak两个,security一个,oauth2两个(jwt方式集成keycloak)

        <!-- 集成keycloak所需要的包 - 开始 -->
        <!-- 管理员客户端API   注册和创建client、realm需要用到的 -->
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-admin-client</artifactId>
            <version>18.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>18.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</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-oauth2-resource-server</artifactId>
        </dependency>
        <!-- 集成keycloak所需要的包 - 结束 -->

2、application.yml配置

将keycloak鉴权集成到jwt中,springboot已集成了jwt

spring:
  #keycloak 鉴权相关配置
  security:
    oauth2:
      resourceserver:
        jwt:
          # user-login 是我的一个域(realms)
          issuer-uri: https://127.0.0.1:8443/realms/user_login
          jwk-set-uri: https://127.0.0.1:8443//realms/user_login/protocol/openid-connect/certs

至此,已经可以keycloak鉴权了,如果来自前端的请求没有带正确Authorization会被拦截返回401,但是,如何放行部分链接呢?请看第三步

3、注入 securityFilterChain Bean,直接粘贴代码吧

package com.hlxx.finance.config;

import com.hlxx.finance.controller.FinanceController;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;

import java.io.IOException;

/**
 * Keycloak对应的Security文件
 * 解决方案来自于 GPT4.0 + 智谱清言
 */
@Configuration
@EnableWebSecurity
public class SecurityConfigKeyCloak {
    private static Logger logger = LoggerFactory.getLogger(SecurityConfigKeyCloak.class);
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new LoggingFilter(), SecurityContextPersistenceFilter.class) // 添加拦截器在Spring Security过滤器链之前
                // 其他安全配置...
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers("/public/**") // 为特定URL禁用CSRF保护
                )
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                    .requestMatchers("/public/**").permitAll() // 放行公开的链接
                    .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                    .jwt(jwt -> jwt
//                            .decoder(jwtDecoder())
                            .jwtAuthenticationConverter(jwtAuthenticationConverter())
                    )
                )
                // 使用新的配置器替代 exceptionHandling()
                .exceptionHandling(exceptionHandling -> exceptionHandling
                    .authenticationEntryPoint(new CustomBearerTokenAuthenticationEntryPoint())
                    .accessDeniedHandler(new CustomAccessDeniedHandler())
                );



        return http.build();
    }

//    @Bean
//    public JwtDecoder jwtDecoder() {
//        // 配置 JWT 解码器
//        return JwtDecoders.fromIssuerLocation("http://127.0.0.1:8080/realms/user_login");
//    }

    // 自定义访问被拒绝处理器
    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        System.out.println("被拒绝处理器");
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthoritiesConverter());
        return converter;
    }
    // 自定义访问被拒绝处理器
    private static class CustomBearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write("{\"code\": 401, \"error\": \"Unauthorized\", \"message\": \"JWT token is invalid or expired\"}");
        }
    }
    // 自定义访问被拒绝处理器
    private static class CustomAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write("{\"code\": 401, \"error\": \"Forbidden\", \"message\": \"Access denied due to invalid credentials\"}");
        }
    }
}

代码中的LoggerFactory是另一个类,用于记录日志调试,代码如下

package com.hlxx.finance.config;

import com.alibaba.fastjson2.JSONObject;
import com.nimbusds.jose.shaded.gson.JsonObject;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class LoggingFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            logger.info("Request URL: {}", request.getRequestURL());
            // 其他需要记录的信息
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            // 异常处理
            logger.error("Exception occurred: {}", e.getMessage(), e);
            throw e;
        }
    }
}

至此,问题解决

三、仍然存在的问题

1、放行的链接,如果前端不带Authorization会直接放行,但是带了错误的Authorization还是会拦截。

2、错误的Authorization会返回response为空。

以上两个问题尝试未果,然后通过调整前端去解决了,有遇到的请留言解决方案,感谢。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值