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();
        }
    }
}
相关推荐
菜鸡且互啄691 小时前
Spring Boot Security自定义AuthenticationProvider
java·jvm·spring boot
青花锁1 小时前
Springboot实战:AI大模型+亮数据代理助力短视频时代
人工智能·spring boot·后端·短视频·亮数据
Mr.Aholic1 小时前
水果商城系统 SpringBoot+Vue
vue.js·spring boot·后端
一个小浪吴啊2 小时前
Java SpringBoot MongoPlus 使用MyBatisPlus的方式,优雅的操作MongoDB
java·spring boot·mongodb
java6666688885 小时前
如何在Spring Boot中实现实时通知
java·spring boot·后端
虫小宝5 小时前
Spring Boot与Jenkins的集成
spring boot·后端·jenkins
java6666688885 小时前
在Spring Boot中集成分布式日志收集方案
spring boot·分布式·jenkins
java6666688885 小时前
深入理解Spring Boot中的配置加载顺序
java·spring boot·后端
AllenIverrui6 小时前
MyBatisPlus的使用
spring boot·spring·java-ee·mybatis
冯宝宝^6 小时前
图书管理系统
服务器·数据库·vue.js·spring boot·后端