Spring Security对接OIDC(OAuth2)外部认证

前后端分离项目对接OIDC(OAuth2)外部认证,认证服务器可以使用Keycloak。

后端已有用户管理和权限管理,需要外部认证服务器的用户名和业务系统的用户名一致才可以登录。

后台基于Spring Boot 2.7 + Spring Security

流程:

  1. 前台浏览器跳转到 后台地址 + /login/oauth2/authorization/my-oidc-client
  2. 后台返回302重定向,重定向到登录外部认证服务器 http://my-oidc-provider.com
  3. 在外部认证网页登录成功后,自动重定向到前台地址 http://localhost/login ,前台取得URL中的参数,使用这些参数发送ajax post请求到 后台地址 + /login/oauth2/my-oidc-client
  4. 后台会自动与 http://my-oidc-provider.com 交互完成登录,并使用用户名取得本地用户信息后返回json
  5. 如果第4步失败,则返回自定义的错误信息json

(1)引入依赖:

复制代码
org.springframework.boot:spring-boot-starter-oauth2-client

(2)配置文件:

java 复制代码
spring.security.oauth2.client.registration.my-oidc-client.provider=my-oidc-provider
spring.security.oauth2.client.registration.my-oidc-client.client-id=my-client-id
spring.security.oauth2.client.registration.my-oidc-client.client-secret=my-client-secret
spring.security.oauth2.client.registration.my-oidc-client.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.my-oidc-client.scope=openid,profile
spring.security.oauth2.client.registration.my-oidc-client.redirect-uri=http://localhost/login
spring.security.oauth2.client.provider.my-oidc-provider.issuer-uri=http://my-oidc-provider.com

(3)Spring Security配置类:

java 复制代码
package com.example.demo.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
@EnableWebSecurity
public class MyOAuth2SecurityConfig {
    
    private final UserDetailsService userDetailsService;
    private final ClientRegistrationRepository clientRegistrationRepository;
    private final ObjectMapper objectMapper;

    public MyOAuth2SecurityConfig(UserDetailsService userDetailsService, ClientRegistrationRepository clientRegistrationRepository, ObjectMapper objectMapper) {
        this.userDetailsService = userDetailsService;
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.objectMapper = objectMapper;
    }

    @Bean
    public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
        http
        .authorizeHttpRequests(httpRequests -> httpRequests.anyRequest().authenticated())
        .oauth2Login(oauth2Login -> oauth2Login
            .authorizationEndpoint(authorization -> authorization
                .baseUri("/login/oauth2/authorization"))
            .loginProcessingUrl("/login/oauth2/*")
            .successHandler(new MyAuthenticationSuccessHandler())
            .failureHandler(new MyAuthenticationFailureHandler())
            .addObjectPostProcessor(new ObjectPostProcessor<OAuth2LoginAuthenticationFilter>() {
                @Override
                public <O extends OAuth2LoginAuthenticationFilter> O postProcess(O filter) {
                    filter.setAuthenticationResultConverter(new Converter<OAuth2LoginAuthenticationToken,OAuth2AuthenticationToken>() {
                        @Override
                        public OAuth2AuthenticationToken convert(OAuth2LoginAuthenticationToken source) {
                            OidcUser user = (OidcUser) source.getPrincipal();
                            String userName = user.getAttribute("preferred_username");
                            // 根据用户名获取适用于本系统的用户对象,用户对象同时实现UserDetails和OidcUser接口
                            UserDetails myUser = userDetailsService.loadUserByUsername(userName);
                            // 用户对象保存IdToken用于退出登录
                            // myUser.setIdToken(user.getIdToken());
                            return new OAuth2AuthenticationToken((OidcUser) myUser, myUser.getAuthorities(), source.getClientRegistration().getRegistrationId());
                        }
                    });
                    return filter;
                }
            }))
        .formLogin(formLogin -> formLogin.disable())
        .logout(logout -> logout
            .logoutUrl("/logout")
            .invalidateHttpSession(true)
            .deleteCookies("SESSION")
            .logoutSuccessHandler(this.logoutSuccessHandler()))
        .csrf(csrf -> csrf.disable());
        
        return http.build();
    }

    private LogoutSuccessHandler logoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler handler = new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
        handler.setPostLogoutRedirectUri(this.clientRegistrationRepository.findByRegistrationId("my-oidc-client").getRedirectUri());
        return handler;
    }

    class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws JsonProcessingException, IOException {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/json;charset=UTF-8");
            // 这里自定义返回的json对象内容
            response.getWriter().write(objectMapper.writeValueAsString(authentication.getPrincipal()));
            response.getWriter().flush();
            response.getWriter().close();
        }
    }

    class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            // 这里自定义返回的json对象内容
            response.getWriter().write(objectMapper.writeValueAsString(""));
            response.getWriter().flush();
            response.getWriter().close();
        }
    }
}
相关推荐
2301_7930868733 分钟前
SpringCloud 02 服务治理 Nacos
java·spring boot·spring cloud
MacroZheng2 小时前
还在用WebSocket实现即时通讯?试试MQTT吧,真香!
java·spring boot·后端
midsummer_woo2 小时前
基于springboot的IT技术交流和分享平台的设计与实现(源码+论文)
java·spring boot·后端
别惹CC4 小时前
Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
人工智能·spring boot·spring
柯南二号5 小时前
【Java后端】Spring Boot 集成 MyBatis-Plus 全攻略
java·spring boot·mybatis
javachen__6 小时前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
IT毕设实战小研12 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
一只爱撸猫的程序猿13 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
甄超锋13 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
武昌库里写JAVA16 小时前
JAVA面试汇总(四)JVM(一)
java·vue.js·spring boot·sql·学习