如何使用 Spring 安全性保护 Vaadin 流应用程序
How to secure Vaadin flow application with Spring Security
我正在尝试将 vaadin 10 与 spring 安全性集成(使用 vaadin 提供的 spring 项目库),但我对它们的确切交互方式感到困惑。如果我直接在浏览器中输入受保护的 url(在本例中为“/about”),就会显示登录页面。如果我通过单击 UI 中的 link 转到相同的 URL,即使我未通过身份验证,该页面也会显示。所以我猜 Vaadin 没有通过 Spring 安全的过滤器链,但是我如何在 UI 中保护我的资源,以及如何在 vaadin 和 spring 之间共享经过身份验证的用户?我应该实施两次安全措施吗?可用的文档似乎没有涵盖这一点,互联网上的每个 link 都有 Vaadin 7-8 的示例,我从未使用过它,而且似乎与 10+ 的工作方式不同。
有谁知道这方面的任何资源,或者您能告诉我所有这些是如何协同工作的,以便我知道我在做什么吗?
这是我的安全配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] ALLOWED_GET_URLS = {
"/",
//"/about",
"/login/**",
"/frontend/**",
"/VAADIN/**",
"/favicon.ico"
};
private static final String[] ALLOWED_POST_URLS = {
"/"
};
//@formatter:off
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.mvcMatchers(HttpMethod.GET, ALLOWED_GET_URLS)
.permitAll()
.mvcMatchers(HttpMethod.POST, ALLOWED_POST_URLS)
.permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.permitAll();
}
//@formatter:on
}
使用 Vaadin Flow (12.0.2)、Spring Boot Starter (2.0.2.RELEASE) 和 Spring Boot Security,基本上,我发现授权基于 role/authority 使用以下方式;
Route/Context 基于 role/authority 管理
- Spring 安全性 (HttpSecurity)
- Vaadin API(BeforeEnterListener 和 Route/Navigation API)
事业部role/authority管理层
- 代码内部使用HttpServletRequest.isUserInRole方法
让我们从一个简单的例子开始 Spring 安全配置;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF is handled by Vaadin: https://vaadin.com/framework/security
.exceptionHandling().accessDeniedPage("/accessDenied")
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
.and().logout().logoutSuccessUrl("/")
.and()
.authorizeRequests()
// allow Vaadin URLs and the login URL without authentication
.regexMatchers("/frontend/.*", "/VAADIN/.*", "/login.*", "/accessDenied").permitAll()
.regexMatchers(HttpMethod.POST, "/\?v-r=.*").permitAll()
// deny any other URL until authenticated
.antMatchers("/**").fullyAuthenticated()
/*
Note that anonymous authentication is enabled by default, therefore;
SecurityContextHolder.getContext().getAuthentication().isAuthenticated() always will return true.
Look at LoginView.beforeEnter method.
more info: https://docs.spring.io/spring-security/site/docs/4.0.x/reference/html/anonymous.html
*/
;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password("a$obstjyWMAVfsNoKisfyCjO/DNfO9OoMOKNt5a6GRlVS7XNUzYuUbO").roles("ADMIN");// user and pass: admin
}
/**
* Expose the AuthenticationManager (to be used in LoginView)
* @return
* @throws Exception
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
如您所见,我还没有在我的任何路由视图(用@Route 注释)上指定任何基于角色的权限。我要做的是,如果我有一个路由视图,我将在构造它(路由视图)时注册一个 BeforeEnterListener,并在那里检查所需的 role/privilege。
以下是在导航到 admin-utils 视图之前检查用户是否具有 ADMIN 角色的示例;
@Route(value = "admin-utils")
public class AdminUtilsView extends VerticalLayout {
@Autowired
private HttpServletRequest req;
...
AdminUtilsView() {
...
UI.getCurrent().addBeforeEnterListener(new BeforeEnterListener() {
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
if (beforeEnterEvent.getNavigationTarget() != DeniedAccessView.class && // This is to avoid a
// loop if DeniedAccessView is the target
!req.isUserInRole("ADMIN")) {
beforeEnterEvent.rerouteTo(DeniedAccessView.class);
}
}
});
}
}
如果用户没有 ADMIN 角色,他将被路由到 DeniedAccessView,这在 Spring 安全配置中已被允许。
@Route(value = "accessDenied")
public class DeniedAccessView
extends VerticalLayout {
DeniedAccessView() {
FormLayout formLayout = new FormLayout();
formLayout.add(new Label("Access denied!"));
add(formLayout);
}
}
在上面的示例 (AdminUtilsView) 中,您还可以通过自动装配 HttpServletRequest 在 Vaadin 代码中看到 HttpServletRequest.isUserInRole() 的用例。
SUMMARY: 如果你的视图有路由,先使用 BeforeEnterListener 来授权请求,否则使用 Spring 安全
用于休息服务等的匹配器(例如 regexMatchers 或 antMatchers)。
注意: 将 Vaadin 路由和 Spring 安全匹配器规则一起用于同一规则可能有点扭曲,我不建议(它在 Vaadin 中导致一些内部循环;例如假设我们有一个视图路由 /view 和 Spring Security for /view with a required role 中的一个条目。如果用户缺少这样的角色并且(s)他是 routed/navigated 到这样的页面(使用 Vaadin 路由 API),Vaadin 尝试打开与路由关联的视图,而 Spring 安全性由于缺少角色而避免了这种情况。
此外,我认为,在将用户重新路由或导航到不同的 view/context 之前,使用 Vaadin 流导航 API 的一个好习惯是检查所需的 role/authority。
此外,要有一个在 Vaadin 中使用 AuthenticationManager 的例子,我们可以有一个基于 Vaadin 的 LoginView 类似于;
@Route(value = "login")
public class LoginView
extends FlexLayout implements BeforeEnterObserver {
private final Label label;
private final TextField userNameTextField;
private final PasswordField passwordField;
/**
* AuthenticationManager is already exposed in WebSecurityConfig
*/
@Autowired
private AuthenticationManager authManager;
@Autowired
private HttpServletRequest req;
LoginView() {
label = new Label("Please login...");
userNameTextField = new TextField();
userNameTextField.setPlaceholder("Username");
UiUtils.makeFirstInputTextAutoFocus(Collections.singletonList(userNameTextField));
passwordField = new PasswordField();
passwordField.setPlaceholder("Password");
passwordField.addKeyDownListener(Key.ENTER, (ComponentEventListener<KeyDownEvent>) keyDownEvent -> authenticateAndNavigate());
Button submitButton = new Button("Login");
submitButton.addClickListener((ComponentEventListener<ClickEvent<Button>>) buttonClickEvent -> {
authenticateAndNavigate();
});
FormLayout formLayout = new FormLayout();
formLayout.add(label, userNameTextField, passwordField, submitButton);
add(formLayout);
// center the form
setAlignItems(Alignment.CENTER);
this.getElement().getStyle().set("height", "100%");
this.getElement().getStyle().set("justify-content", "center");
}
private void authenticateAndNavigate() {
/*
Set an authenticated user in Spring Security and Spring MVC
spring-security
*/
UsernamePasswordAuthenticationToken authReq
= new UsernamePasswordAuthenticationToken(userNameTextField.getValue(), passwordField.getValue());
try {
// Set authentication
Authentication auth = authManager.authenticate(authReq);
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(auth);
/*
Navigate to the requested page:
This is to redirect a user back to the originally requested URL – after they log in as we are not using
Spring's AuthenticationSuccessHandler.
*/
HttpSession session = req.getSession(false);
DefaultSavedRequest savedRequest = (DefaultSavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
String requestedURI = savedRequest != null ? savedRequest.getRequestURI() : Application.APP_URL;
this.getUI().ifPresent(ui -> ui.navigate(StringUtils.removeStart(requestedURI, "/")));
} catch (BadCredentialsException e) {
label.setText("Invalid username or password. Please try again.");
}
}
/**
* This is to redirect user to the main URL context if (s)he has already logged in and tries to open /login
*
* @param beforeEnterEvent
*/
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//Anonymous Authentication is enabled in our Spring Security conf
if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
//https://vaadin.com/docs/flow/routing/tutorial-routing-lifecycle.html
beforeEnterEvent.rerouteTo("");
}
}
}
最后,这是可以从菜单或按钮调用的注销方法:
/**
* log out the current user using Spring security and Vaadin session management
*/
void requestLogout() {
//
SecurityContextHolder.clearContext();
req.getSession(false).invalidate();
// And this is similar to how logout is handled in Vaadin 8:
// https://vaadin.com/docs/v8/framework/articles/HandlingLogout.html
UI.getCurrent().getSession().close();
UI.getCurrent().getPage().reload();// to redirect user to the login page
}
您可以继续使用 Spring UserDetailsService 完成角色管理并通过查看以下示例创建 PasswordEncoder bean:
- https://github.com/igor-baiborodine/vaadin-demo-bakery-app/blob/master/src/main/java/com/kiroule/vaadin/bakeryapp/app/security/SecurityConfiguration.java
- https://github.com/igor-baiborodine/vaadin-demo-bakery-app/blob/master/src/main/java/com/kiroule/vaadin/bakeryapp/app/security/UserDetailsServiceImpl.java
- https://www.baeldung.com/role-and-privilege-for-spring-security-registration
我正在尝试将 vaadin 10 与 spring 安全性集成(使用 vaadin 提供的 spring 项目库),但我对它们的确切交互方式感到困惑。如果我直接在浏览器中输入受保护的 url(在本例中为“/about”),就会显示登录页面。如果我通过单击 UI 中的 link 转到相同的 URL,即使我未通过身份验证,该页面也会显示。所以我猜 Vaadin 没有通过 Spring 安全的过滤器链,但是我如何在 UI 中保护我的资源,以及如何在 vaadin 和 spring 之间共享经过身份验证的用户?我应该实施两次安全措施吗?可用的文档似乎没有涵盖这一点,互联网上的每个 link 都有 Vaadin 7-8 的示例,我从未使用过它,而且似乎与 10+ 的工作方式不同。
有谁知道这方面的任何资源,或者您能告诉我所有这些是如何协同工作的,以便我知道我在做什么吗?
这是我的安全配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] ALLOWED_GET_URLS = {
"/",
//"/about",
"/login/**",
"/frontend/**",
"/VAADIN/**",
"/favicon.ico"
};
private static final String[] ALLOWED_POST_URLS = {
"/"
};
//@formatter:off
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.mvcMatchers(HttpMethod.GET, ALLOWED_GET_URLS)
.permitAll()
.mvcMatchers(HttpMethod.POST, ALLOWED_POST_URLS)
.permitAll()
.anyRequest()
.fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.permitAll();
}
//@formatter:on
}
使用 Vaadin Flow (12.0.2)、Spring Boot Starter (2.0.2.RELEASE) 和 Spring Boot Security,基本上,我发现授权基于 role/authority 使用以下方式;
Route/Context 基于 role/authority 管理
- Spring 安全性 (HttpSecurity)
- Vaadin API(BeforeEnterListener 和 Route/Navigation API)
事业部role/authority管理层
- 代码内部使用HttpServletRequest.isUserInRole方法
让我们从一个简单的例子开始 Spring 安全配置;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF is handled by Vaadin: https://vaadin.com/framework/security
.exceptionHandling().accessDeniedPage("/accessDenied")
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
.and().logout().logoutSuccessUrl("/")
.and()
.authorizeRequests()
// allow Vaadin URLs and the login URL without authentication
.regexMatchers("/frontend/.*", "/VAADIN/.*", "/login.*", "/accessDenied").permitAll()
.regexMatchers(HttpMethod.POST, "/\?v-r=.*").permitAll()
// deny any other URL until authenticated
.antMatchers("/**").fullyAuthenticated()
/*
Note that anonymous authentication is enabled by default, therefore;
SecurityContextHolder.getContext().getAuthentication().isAuthenticated() always will return true.
Look at LoginView.beforeEnter method.
more info: https://docs.spring.io/spring-security/site/docs/4.0.x/reference/html/anonymous.html
*/
;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password("a$obstjyWMAVfsNoKisfyCjO/DNfO9OoMOKNt5a6GRlVS7XNUzYuUbO").roles("ADMIN");// user and pass: admin
}
/**
* Expose the AuthenticationManager (to be used in LoginView)
* @return
* @throws Exception
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
如您所见,我还没有在我的任何路由视图(用@Route 注释)上指定任何基于角色的权限。我要做的是,如果我有一个路由视图,我将在构造它(路由视图)时注册一个 BeforeEnterListener,并在那里检查所需的 role/privilege。
以下是在导航到 admin-utils 视图之前检查用户是否具有 ADMIN 角色的示例;
@Route(value = "admin-utils")
public class AdminUtilsView extends VerticalLayout {
@Autowired
private HttpServletRequest req;
...
AdminUtilsView() {
...
UI.getCurrent().addBeforeEnterListener(new BeforeEnterListener() {
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
if (beforeEnterEvent.getNavigationTarget() != DeniedAccessView.class && // This is to avoid a
// loop if DeniedAccessView is the target
!req.isUserInRole("ADMIN")) {
beforeEnterEvent.rerouteTo(DeniedAccessView.class);
}
}
});
}
}
如果用户没有 ADMIN 角色,他将被路由到 DeniedAccessView,这在 Spring 安全配置中已被允许。
@Route(value = "accessDenied")
public class DeniedAccessView
extends VerticalLayout {
DeniedAccessView() {
FormLayout formLayout = new FormLayout();
formLayout.add(new Label("Access denied!"));
add(formLayout);
}
}
在上面的示例 (AdminUtilsView) 中,您还可以通过自动装配 HttpServletRequest 在 Vaadin 代码中看到 HttpServletRequest.isUserInRole() 的用例。
SUMMARY: 如果你的视图有路由,先使用 BeforeEnterListener 来授权请求,否则使用 Spring 安全 用于休息服务等的匹配器(例如 regexMatchers 或 antMatchers)。
注意: 将 Vaadin 路由和 Spring 安全匹配器规则一起用于同一规则可能有点扭曲,我不建议(它在 Vaadin 中导致一些内部循环;例如假设我们有一个视图路由 /view 和 Spring Security for /view with a required role 中的一个条目。如果用户缺少这样的角色并且(s)他是 routed/navigated 到这样的页面(使用 Vaadin 路由 API),Vaadin 尝试打开与路由关联的视图,而 Spring 安全性由于缺少角色而避免了这种情况。
此外,我认为,在将用户重新路由或导航到不同的 view/context 之前,使用 Vaadin 流导航 API 的一个好习惯是检查所需的 role/authority。
此外,要有一个在 Vaadin 中使用 AuthenticationManager 的例子,我们可以有一个基于 Vaadin 的 LoginView 类似于;
@Route(value = "login")
public class LoginView
extends FlexLayout implements BeforeEnterObserver {
private final Label label;
private final TextField userNameTextField;
private final PasswordField passwordField;
/**
* AuthenticationManager is already exposed in WebSecurityConfig
*/
@Autowired
private AuthenticationManager authManager;
@Autowired
private HttpServletRequest req;
LoginView() {
label = new Label("Please login...");
userNameTextField = new TextField();
userNameTextField.setPlaceholder("Username");
UiUtils.makeFirstInputTextAutoFocus(Collections.singletonList(userNameTextField));
passwordField = new PasswordField();
passwordField.setPlaceholder("Password");
passwordField.addKeyDownListener(Key.ENTER, (ComponentEventListener<KeyDownEvent>) keyDownEvent -> authenticateAndNavigate());
Button submitButton = new Button("Login");
submitButton.addClickListener((ComponentEventListener<ClickEvent<Button>>) buttonClickEvent -> {
authenticateAndNavigate();
});
FormLayout formLayout = new FormLayout();
formLayout.add(label, userNameTextField, passwordField, submitButton);
add(formLayout);
// center the form
setAlignItems(Alignment.CENTER);
this.getElement().getStyle().set("height", "100%");
this.getElement().getStyle().set("justify-content", "center");
}
private void authenticateAndNavigate() {
/*
Set an authenticated user in Spring Security and Spring MVC
spring-security
*/
UsernamePasswordAuthenticationToken authReq
= new UsernamePasswordAuthenticationToken(userNameTextField.getValue(), passwordField.getValue());
try {
// Set authentication
Authentication auth = authManager.authenticate(authReq);
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(auth);
/*
Navigate to the requested page:
This is to redirect a user back to the originally requested URL – after they log in as we are not using
Spring's AuthenticationSuccessHandler.
*/
HttpSession session = req.getSession(false);
DefaultSavedRequest savedRequest = (DefaultSavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
String requestedURI = savedRequest != null ? savedRequest.getRequestURI() : Application.APP_URL;
this.getUI().ifPresent(ui -> ui.navigate(StringUtils.removeStart(requestedURI, "/")));
} catch (BadCredentialsException e) {
label.setText("Invalid username or password. Please try again.");
}
}
/**
* This is to redirect user to the main URL context if (s)he has already logged in and tries to open /login
*
* @param beforeEnterEvent
*/
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//Anonymous Authentication is enabled in our Spring Security conf
if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
//https://vaadin.com/docs/flow/routing/tutorial-routing-lifecycle.html
beforeEnterEvent.rerouteTo("");
}
}
}
最后,这是可以从菜单或按钮调用的注销方法:
/**
* log out the current user using Spring security and Vaadin session management
*/
void requestLogout() {
//
SecurityContextHolder.clearContext();
req.getSession(false).invalidate();
// And this is similar to how logout is handled in Vaadin 8:
// https://vaadin.com/docs/v8/framework/articles/HandlingLogout.html
UI.getCurrent().getSession().close();
UI.getCurrent().getPage().reload();// to redirect user to the login page
}
您可以继续使用 Spring UserDetailsService 完成角色管理并通过查看以下示例创建 PasswordEncoder bean:
- https://github.com/igor-baiborodine/vaadin-demo-bakery-app/blob/master/src/main/java/com/kiroule/vaadin/bakeryapp/app/security/SecurityConfiguration.java
- https://github.com/igor-baiborodine/vaadin-demo-bakery-app/blob/master/src/main/java/com/kiroule/vaadin/bakeryapp/app/security/UserDetailsServiceImpl.java
- https://www.baeldung.com/role-and-privilege-for-spring-security-registration