前后端分离项目对接OIDC(OAuth2)外部认证,认证服务器可以使用Keycloak。
后端已有用户管理和权限管理,需要外部认证服务器的用户名和业务系统的用户名一致才可以登录。
后台基于Spring Boot 2.7 + Spring Security
流程:
- 前台浏览器跳转到 后台地址 + /login/oauth2/authorization/my-oidc-client
- 后台返回302重定向,重定向到登录外部认证服务器 http://my-oidc-provider.com
- 在外部认证网页登录成功后,自动重定向到前台地址 http://localhost/login ,前台取得URL中的参数,使用这些参数发送ajax post请求到 后台地址 + /login/oauth2/my-oidc-client
- 后台会自动与 http://my-oidc-provider.com 交互完成登录,并使用用户名取得本地用户信息后返回json
- 如果第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();
}
}
}