You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Security OAuth2集成Azure AD:自定义UserDetails替代OidcUser

Got it, let's tackle this. You want Azure AD to handle the initial identity check (verifying the user is in a specific group/directory) but ditch Spring Security's default OidcUser in favor of your own custom UserDetails implementation. Here's a step-by-step solution that fits your flow:

1. Configure Spring Security OAuth2 Client to Use Your Custom Logic

First, adjust your security filter chain to disable the default OIDC user handling and hook in your custom service:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .userInfoEndpoint(userInfo -> userInfo
                .userService(customAzureAdOAuth2UserService())
            )
            .successHandler(customAuthSuccessHandler()) // Optional: Handle post-login actions
        );
    return http.build();
}
2. Build a Custom OAuth2UserService

This service will handle fetching Azure AD user data, validating group membership, and loading your custom UserDetails:

@Service
public class CustomAzureAdOAuth2UserService implements OAuth2UserService<OidcUserRequest, OAuth2User> {

    private final CustomUserDetailsService customUserDetailsService;
    private final String REQUIRED_GROUP_ID = "your-azure-ad-group-id"; // Replace with your group's ID

    public CustomAzureAdOAuth2UserService(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }

    @Override
    public OAuth2User loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        // Let Spring's default service fetch Azure AD's user data and tokens
        OidcUserService delegate = new OidcUserService();
        OidcUser oidcUser = delegate.loadUser(userRequest);

        // Validate user is in the required Azure AD group
        if (!isUserInRequiredGroup(oidcUser.getClaims())) {
            throw new OAuth2AuthenticationException(
                new OAuth2Error("invalid_group", "User not in authorized group", null)
            );
        }

        // Grab the user's email from Azure AD claims (adjust claim name if needed)
        String userEmail = oidcUser.getEmail();
        if (userEmail == null || userEmail.isBlank()) {
            throw new OAuth2AuthenticationException(
                new OAuth2Error("missing_email", "No email found in Azure AD response", null)
            );
        }

        // Load your custom UserDetails using the email
        UserDetails customUser = customUserDetailsService.loadUserByUsername(userEmail);

        // Wrap your custom user with Azure AD's claims/authorities
        return new CustomOAuth2UserWrapper(customUser, oidcUser.getClaims(), oidcUser.getAuthorities());
    }

    private boolean isUserInRequiredGroup(Map<String, Object> claims) {
        // Azure AD returns group IDs in the "groups" claim (ensure your app has permission to read groups)
        List<String> userGroups = (List<String>) claims.getOrDefault("groups", Collections.emptyList());
        return userGroups.contains(REQUIRED_GROUP_ID);
    }
}
3. Create a Wrapper for Your Custom UserDetails

This class bridges your UserDetails with Spring's OAuth2User interface, keeping both your custom user data and Azure AD's metadata:

public class CustomOAuth2UserWrapper implements OAuth2User {

    private final UserDetails customUser;
    private final Map<String, Object> azureAdClaims;
    private final Collection<? extends GrantedAuthority> combinedAuthorities;

    public CustomOAuth2UserWrapper(UserDetails customUser, Map<String, Object> azureAdClaims, Collection<? extends GrantedAuthority> azureAdAuthorities) {
        this.customUser = customUser;
        this.azureAdClaims = azureAdClaims;
        // Merge your custom user's authorities with Azure AD's (adjust if you don't need this)
        this.combinedAuthorities = Stream.concat(
            customUser.getAuthorities().stream(),
            azureAdAuthorities.stream()
        ).collect(Collectors.toSet());
    }

    @Override
    public Map<String, Object> getAttributes() {
        return azureAdClaims;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return combinedAuthorities;
    }

    @Override
    public String getName() {
        return customUser.getUsername();
    }

    // Helper to access your custom UserDetails anywhere in the app
    public UserDetails getCustomUser() {
        return customUser;
    }
}
4. Implement Your Custom UserDetails & UserDetailsService

This is your application-specific user logic (e.g., loading from a database):

// Your custom UserDetails implementation
public class AppUser implements UserDetails {

    private final String email;
    private final Collection<GrantedAuthority> authorities;

    public AppUser(String email, Collection<GrantedAuthority> authorities) {
        this.email = email;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return null; // No need for password since Azure AD handles authentication
    }

    @Override
    public String getUsername() {
        return email;
    }

    // Implement remaining UserDetails methods based on your app's needs
    @Override
    public boolean isAccountNonExpired() { return true; }
    @Override
    public boolean isAccountNonLocked() { return true; }
    @Override
    public boolean isCredentialsNonExpired() { return true; }
    @Override
    public boolean isEnabled() { return true; }
}

// Your UserDetailsService to load users from your data store
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository; // Replace with your user repo

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        // Fetch user from your database
        UserEntity dbUser = userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));

        // Convert to your custom UserDetails with roles/authorities
        List<GrantedAuthority> authorities = dbUser.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .collect(Collectors.toList());

        return new AppUser(dbUser.getEmail(), authorities);
    }
}
5. Optional: Customize Post-Login Behavior

If you need to handle actions like logging or redirects after successful authentication:

@Bean
public AuthenticationSuccessHandler customAuthSuccessHandler() {
    return (request, response, authentication) -> {
        CustomOAuth2UserWrapper principal = (CustomOAuth2UserWrapper) authentication.getPrincipal();
        AppUser customUser = (AppUser) principal.getCustomUser();

        // Add your post-login logic here (e.g., log login event, set session attributes)

        // Redirect to your app's home page
        response.sendRedirect("/dashboard");
    };
}

Key Notes

  • Azure AD Permissions: Make sure your Azure AD app registration has the GroupMember.Read.All permission (or equivalent) to access group data for validation.
  • Claim Adjustments: Azure AD claim names can vary based on your app config—double-check if the email is in email or upn claims, and adjust the code accordingly.
  • Token Validation: Spring Security automatically validates Azure AD's ID token signature and expiration, so you don't need to handle that manually.

内容的提问来源于stack exchange,提问作者Oleh Kurpiak

火山引擎 最新活动