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();
        }
    }
}
相关推荐
腥臭腐朽的日子熠熠生辉2 小时前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
绝顶少年4 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端
西木风落4 小时前
springboot整合Thymeleaf web开发出现Whitelabel Error Page
spring boot·thymeleaf error·whitelabelerror
有来技术5 小时前
从0到1手撸企业级权限系统:基于 youlai-boot(开源) + Java17 + Spring Boot 3 完整实战
java·spring boot·后端
橘猫云计算机设计6 小时前
基于springboot微信小程序的旅游攻略系统(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·微信小程序·毕业设计·旅游
风象南6 小时前
SpringBoot中6种跨域请求解决方案
java·spring boot·后端
良枫6 小时前
Spring Security认证授权深度解析
spring boot·spring
码视野7 小时前
基于SpringBoot的河道水情大数据可视化分析平台设计与实现(源码+论文+部署讲解等)
spring boot·后端·物联网·信息可视化·论文·本科毕业论文·计算机专业毕业论文
用键盘当武器的秋刀鱼8 小时前
springBoot统一响应类型3.5.3版本
java·spring boot·spring
喻米粒062212 小时前
RabbitMQ消息相关
java·jvm·spring boot·spring·spring cloud·sentinel·java-rabbitmq