我使用 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”:
现在我绝对不确定我所做的是否有任何目的,是配置部分:
@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 应该无需身份验证即可访问。但我有两个主要问题:
server.servlet.context-path
和 vaadin.urlMapping=/ui/*
,一旦我将这两个属性添加到 application.properties 中,就不再起作用了。不是使用 admin/admin 或 user/user 的默认登录,也不是 azure 广告登录。server.servlet.context-path
和 vaadin.urlMapping=/ui/*
时,我什至无法从 Azure AD 访问 oauth2 登录页面。如果有 Azure AD + Vaadin 或 Spring Boot 经验的人可以指导我或至少指出一些遗漏的点或错误,那就太好了。
我通过简单地使用此配置解决了我的问题:
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 登录。