Vaadin 用户登录和 Azure AD 登录不起作用

问题描述 投票:0回答:1

我使用 java 17、vaadin 23.3.5 和 spring boot 2.7.5 构建了一个 Web 应用程序。 我已经成功集成了自定义用户管理,该自定义用户管理是根据默认的 vaadin 用户管理进行自定义的。利益相关者的新要求是他们希望能够通过 Azure AD 登录,因为他们从那里管理所有公司用户。但他们仍然希望能够通过默认登录来登录用户。

由于我使用的是 Maven 3,所以我声明了所有必要的依赖项:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.azure.spring</groupId>
            <artifactId>spring-cloud-azure-dependencies</artifactId>
            <version>4.11.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>     
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>com.azure.spring</groupId>
        <artifactId>spring-cloud-azure-starter-active-directory</artifactId>
    </dependency>
    <dependency>
        <groupId>com.microsoft.azure</groupId>
        <artifactId>msal4j</artifactId>
        <version>1.13.5</version>
    </dependency>
</dependencies>

在application.properties中,我配置了连接参数:

    spring.cloud.azure.active-directory.enabled=true
    spring.cloud.azure.active-directory.profile.tenant-id=xxx
    spring.cloud.azure.active-directory.credential.client-id=yyy
    spring.cloud.azure.active-directory.credential.client-secret=zzz
    spring.cloud.azure.active-directory.redirect-uri-template=http://localhost:8080/login/oauth2/code/azure
    spring.security.oauth2.client.access-token-uri=https://login.microsoftonline.com/common/oauth2/v2.0/token
    spring.security.oauth2.client.user-authorization-   uri=https://login.microsoftonline.com/common/oauth2/v2.0/authorize
    logging.level.com.azure.spring.cloud=trace

为了检查用户是否经过身份验证,我从 vaadin 自定义了 AuthenticatedUser 类,以便它应该处理来自 Azure AD 的 DefaultOidcUser 对象:

@Component
public class AuthenticatedUser {

    @Autowired
    private HttpSession session;
    @Autowired
    protected OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;

    private final UserRepository userRepository;
    private final AuthenticationContext authenticationContext;

    public AuthenticatedUser(AuthenticationContext authenticationContext, UserRepository userRepository) {
        this.userRepository = userRepository;
        this.authenticationContext = authenticationContext;
    }

    private Set<Role> mapRoles(List<String> aadRoles) {
        Map<String, Role> dict = new HashMap<>();
        dict.put("app.admin", Role.ADMIN);
        dict.put("app.user", Role.USER);

        Set<Role> roles = new HashSet<>();
        for (String aadRole : aadRoles) {
            if (dict.containsKey(aadRole)) {
                roles.add(dict.get(aadRole));
            }
        }
        return roles;
    }

    @Transactional
    public Optional<User> get() {
        try {
            Optional<DefaultOidcUser> aadUser = authenticationContext.getAuthenticatedUser(DefaultOidcUser.class);

            if (aadUser.isPresent()) {
                // if a context is found from azure ad, create a dummy user to work with
                User user = new User();
                user.setUsername(aadUser.get().getPreferredUsername());
                user.setName(aadUser.get().getName());
                user.setId((long)((String) aadUser.get().getAttribute("oid")).hashCode());

                List<String> aadRoles = aadUser.get().getAttribute("roles");
                user.setRoles(mapRoles(aadRoles));

                return Optional.of(user);
            }

        } catch (ClassCastException e) {
            return authenticationContext.getAuthenticatedUser(UserDetails.class)
                    .map(userDetails -> userRepository.findByUsername(userDetails.getUsername()));
        }
        return Optional.empty();
    }

    public void logout() {
        Optional<DefaultOidcUser> aadUser = authenticationContext.getAuthenticatedUser(DefaultOidcUser.class);
        if (aadUser.isPresent()) {
            UI.getCurrent().getPage().setLocation("/logout");
        } else {
            authenticationContext.logout();
        }
    }

}

然后,我装饰了默认的 LoginView,使其具有一个附加按钮,以允许通过 Azrue AD 登录,重新路由到“/oauth2/authorization/azure”: Login View

现在我绝对不确定我所做的是否有任何目的,是配置部分:

@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {

    /**
     * A repository for OAuth 2.0 / OpenID Connect 1.0 ClientRegistration(s).
     */
    @Autowired
    protected ClientRegistrationRepository repo;


    /**
     * restTemplateBuilder bean used to create RestTemplate for Azure AD related http request.
     */
    @Autowired
    protected RestTemplateBuilder restTemplateBuilder;

    /**
     * OIDC user service.
     */
    @Autowired
    protected OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;

    /**
     * AAD authentication properties
     */
    @Autowired
    protected AadAuthenticationProperties properties;

    /**
     * JWK resolver implementation for client authentication.
     */
    @Autowired
    protected ObjectProvider<OAuth2ClientAuthenticationJwkResolver> jwkResolvers;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();
        http.cors().disable();



        Filter conditionalAccessFilter = conditionalAccessFilter();
        if (conditionalAccessFilter != null) {
            http.addFilterAfter(conditionalAccessFilter, OAuth2AuthorizationRequestRedirectFilter.class);
        }


        http
                .oauth2Login(oauth2Login ->
                        oauth2Login
                                .authorizationEndpoint()
                                .authorizationRequestResolver(requestResolver())
                                .and()
                                .tokenEndpoint()
                                .accessTokenResponseClient(accessTokenResponseClient())
                                .and()
                                .userInfoEndpoint()
                                .oidcUserService(oidcUserService)
                )
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessHandler(oidcLogoutSuccessHandler());

        http.authorizeRequests()
                .requestMatchers(new AntPathRequestMatcher("/api/**"))
                .permitAll();

        http.authorizeRequests(authorizeRequests ->
                authorizeRequests
                        .requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll()
                        .requestMatchers(new AntPathRequestMatcher("/line-awesome/**/*.svg")).permitAll()
                        //.antMatchers("/login").anonymous() // Allow /login for Vaadin login
                        .antMatchers("/oauth2/authorization/azure").authenticated()// Require authentication for Azure
                        //.anyRequest().authenticated() // All other requests require authentication
        );
        http.formLogin() // Use form-based login for /login
                .loginPage("/login") // Specify the login page URL
                .permitAll();
        http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler());

        super.configure(http);

        setLoginView(http, LoginView.class);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers("/images/*.png");
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new VaadinAccessDeniedHandler();
    }

    protected OAuth2AuthorizationRequestResolver requestResolver() {
        return new AadOAuth2AuthorizationRequestResolver(this.repo, properties);
    }

    protected OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient();
        result.setRestOperations(createOAuth2AccessTokenResponseClientRestTemplate(restTemplateBuilder));
        if (repo instanceof AadClientRegistrationRepository) {
            AadOAuth2AuthorizationCodeGrantRequestEntityConverter converter =
                    new AadOAuth2AuthorizationCodeGrantRequestEntityConverter(
                            ((AadClientRegistrationRepository) repo).getAzureClientAccessTokenScopes());
            OAuth2ClientAuthenticationJwkResolver jwkResolver = jwkResolvers.getIfUnique();
            if (jwkResolver != null) {
                converter.addParametersConverter(new AadJwtClientAuthenticationParametersConverter<>(jwkResolver::resolve));
            }
            result.setRequestEntityConverter(converter);
        }
        return result;
    }

    protected LogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
                new OidcClientInitiatedLogoutSuccessHandler(this.repo);
        String uri = this.properties.getPostLogoutRedirectUri();
        if (StringUtils.hasText(uri)) {
            oidcLogoutSuccessHandler.setPostLogoutRedirectUri(uri);
        }
        return oidcLogoutSuccessHandler;
    }

    protected Filter conditionalAccessFilter() {
        return null;
    }
}

我有两个视图(HelloWorld 和 About)。 HelloWorldView 有这些注释:

@PageTitle("Hello World")
@Route(value = "hello", layout = MainLayout.class)
@RouteAlias(value = "", layout = MainLayout.class)
@AnonymousAllowed

而 AboutView 仅使用

@PageTitle("About")
@Route(value = "about", layout = MainLayout.class)
@RolesAllowed({"APPROLE_app.user", "ROLE_USER"})

因此 HelloWorldView 应该无需身份验证即可访问。但我有两个主要问题:

  1. 当我使用一个来自 start.vaadin.com 的简单应用程序时,我至少得到了一些可以工作的东西(即使不是很漂亮)。但由于我想使用
    server.servlet.context-path
    vaadin.urlMapping=/ui/*
    ,一旦我将这两个属性添加到 application.properties 中,就不再起作用了。不是使用 admin/admin 或 user/user 的默认登录,也不是 azure 广告登录。
  2. 当我将要点 1 中的工作设置应用到迄今为止构建的复杂业务应用程序并删除
    server.servlet.context-path
    vaadin.urlMapping=/ui/*
    时,我什至无法从 Azure AD 访问 oauth2 登录页面。

如果有 Azure AD + Vaadin 或 Spring Boot 经验的人可以指导我或至少指出一些遗漏的点或错误,那就太好了。

spring-boot spring-security azure-active-directory vaadin vaadin-flow
1个回答
0
投票

我通过简单地使用此配置解决了我的问题:

http.authorizeRequests()
        .requestMatchers(new AntPathRequestMatcher("/oauth2/authorization/azure"))
        .permitAll();
http.oauth2Login(oauthLogin ->
            oauthLogin.userInfoEndpoint(uie -> uie.userService(oidcUserService()))
                    .tokenEndpoint()
                    .accessTokenResponseClient(accessTokenResponseClient())
);

并配置以下 bean:

@Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
    return new DefaultAuthorizationCodeTokenResponseClient();
}

@Bean
public DefaultOAuth2UserService oidcUserService() {
    return new DefaultOAuth2UserService() {
        @Override
        public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
            OAuth2User user = super.loadUser(userRequest);
            return user;
        }
    };
}

通过使用上面提到的 AuthenticatedUser 类,我可以简单地使用 Spring Security 的默认 Vaadin 登录,并使用 oauth2 通过 azure ad 登录。

© www.soinside.com 2019 - 2024. All rights reserved.