Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh
Trong các dự án thực tế, các bạn sẽ gặp những trường hợp mà ứng dụng cần thiết phải sử dụng 2 cách login khác nhau tuỳ theo role của user, ví dụ như có những ứng dụng sẽ cần user bình thường login sử dụng token hoặc QR code, còn admin thì login sử dụng username, password. Để hiện thực multiple login page sử dụng Spring Security như thế nào? Mình sẽ hướng dẫn các bạn trong bài viết này các bạn nhé!
Ứng dụng ví dụ
Đầu tiên, mình sẽ tạo mới một Spring Boot project với Spring Security Starter, Spring Web Starter, Thymeleaf Starter:
và WebJars với Bootstrap dependency để làm ví dụ như sau:
<dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>5.0.2</version> </dependency>
Kết quả:
Để thể hiện nhu cầu mà chúng ta đang muốn giải quyết, mình sẽ tạo một controller để expose ra 2 trang, một chỉ cho user có role là USER access và một chỉ cho user có role ADMIN access. Cụ thể như sau:
package com.huongdanjava.springsecurity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class ApplicationController { @GetMapping("/user/view") public String userView() { return "user"; } @GetMapping("/admin/view") public String adminView() { return "admin"; } }
Thymeleaf template cho các trang này như sau:
admin.html:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Spring Security Example</title> <link href="/webjars/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h2 class="form-signin-heading">Hello Admin</h2> </div> </body> </html>
user.html:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Spring Security Example</title> <link href="/webjars/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h2 class="form-signin-heading">Hello User</h2> </div> </body> </html>
Lúc này, nếu các bạn chạy ứng dụng lên và request tới 2 trang này, trang login mặc định của Spring Security sẽ luôn được hiển thị.
Mình sẽ sử dụng trang login mặc định của Spring Security cho user “admin” với username và password, còn user bình thường “user” thì mình sẽ sử dụng một custom login page cũng với username và password, tương tự như mình đã làm trong bài viết Custom login page sử dụng Bootstrap và Thymeleaf trong Spring Security.
Code trang login-user.html cho user bình thường đăng nhập như sau:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Spring Security Example</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h2 class="form-signin-heading">Welcome to Huong Dan Java, please login</h2> <div th:if="${param.error}" class="alert alert-danger"> Invalid username and password. </div> <div th:if="${param.logout}" class="alert alert-success"> You have been logged out. </div> <form class="form-signin" method="POST" th:action="@{/login}"> <p> <label for="username" class="sr-only">Username</label> <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus> </p> <p> <label for="password" class="sr-only">Password</label> <input type="password" id="password" name="password" class="form-control" placeholder="Password" required> </p> <button class="btn btn-lg btn-primary btn-block" type="submit">Login</button> </form> </div> </body> </html>
Expose trang login với token này trong class ApplicationController như sau::
@GetMapping("/user-login") public String userLoginView() { return "login-user"; }
Bây giờ mình sẽ cấu hình cho Spring Security cho request tới 2 trang này.
Cấu hình Spring Security
Đầu tiên, mình sẽ cấu hình thông tin về user sẽ đăng nhập vào ứng dụng ví dụ.
Như mình nói ở trên, chúng ta sẽ có 2 user là “user” và “admin” với role tương ứng là “USER” và “ADMIN”. Cả hai user này sẽ sử dụng chung một source cho phần authentication, điều này có nghĩa là chúng được lưu trữ ở một nơi giống nhau, trong bài viết này chúng ta sẽ sử dụng source là in memory.
Mình sẽ tạo bean cho đối tượng UserDetailsService chứa thông tin của 2 user này như sau:
package com.huongdanjava.springsecurity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @SpringBootApplication public class SpringSecurityMultipleLoginApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityMultipleLoginApplication.class, args); } @Bean public UserDetailsService userDetailsService() throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User .withUsername("user") .password(encoder().encode("user")) .roles("USER") .build()); manager.createUser(User .withUsername("admin") .password(encoder().encode("admin")) .roles("ADMIN") .build()); return manager; } @Bean public static PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } }
Tiếp theo, chúng ta sẽ cấu hình Spring Security.
Ở đây, là vì chúng ta cần handle request cho user có role “USER” thì hiển thị trang custom login còn user có role “ADMIN” sẽ hiển thị trang login mặc định của Spring Security nên mình sẽ định nghĩa multiple class extends abstract class WebSecurityConfigurerAdapter với order như sau:
package com.huongdanjava.springsecurity; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class SpringSecurityConfiguration { @Configuration @Order(1) public class UserSpringSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.antMatcher("/user/**") .authorizeRequests() .anyRequest().hasRole("USER") .and() .formLogin() .loginPage("/user-login") .failureUrl("/user-login?error") .permitAll(); // @formatter:on } } @Configuration public class AdminSpringSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests() .antMatchers("/user-login/**").permitAll() .anyRequest().hasRole("ADMIN") .and() .formLogin(Customizer.withDefaults()); // @formatter:on } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/webjars/**"); } } }
Ở đây, mình đang khai báo để handle cho request của user bình thường sử dụng class UserSpringSecurityConfiguration còn admin user thì những request còn lại. Mình chỉ khai báo @Order annotation cho class UserSpringSecurityConfiguration để nó được gọi đầu tiên để handle bất kỳ request nào, nếu không thoả điều kiện antMatcher() để nó handle request đó thì class AdminSpringSecurityConfiguration sẽ handle.
Như các bạn thấy, đối với những request bắt đầu với “/user”, nếu user không
có role “USER”, mình cấu hình cho ứng dụng ví dụ của chúng ta redirect tới trang “/user-login” để user có thể login, lúc này class AdminSpringSecurityConfiguration sẽ handle request “/user-login” này. Trong class AdminSpringSecurityConfiguration, mình đã cấu hình đối với request bắt đầu bằng “/user-login”, chúng ta sẽ permitAll(). Lúc này trang “/user-login” sẽ được hiển thị. Nếu đăng nhập thành công và user có role “USER” thì chúng ta có thể đăng nhập request bắt đầu với “/user” này.
Chạy ứng dụng và request tới http://localhost:8080/user/view, các bạn sẽ thấy trang custom login hiển thị như sau:
Đăng nhập với user “user” và password là “user”, các bạn sẽ thấy kết quả như sau:
Còn nếu các bạn request tới http://localhost:8080/admin/view, các bạn sẽ thấy trang login mặc định của Spring Security hiển thị. Đăng nhập với user “admin” và password “admin”, các bạn sẽ thấy kết quả như sau:
Trong class AdminSpringSecurityConfiguration, mình đã cấu hình cho những request còn lại, ngoại trừ request “/user/**”, user phải có role là “ADMIN”.
Các bạn hãy nhớ là, để cấu hình cho những specific riêng biệt thì chúng ta sẽ sử dụng phương thức antMatcher() của đối tượng HttpSecurity với value là request URL mà chúng ta cần. Cho những request còn lại thì các bạn đừng khai báo annotation @Order với class cấu hình, để nó luôn là order cao nhất.
Bài viết gốc được đăng tải tại huongdanjava.com
Có thể bạn quan tâm:
- Custom login page sử dụng Bootstrap và Thymeleaf trong Spring Security
- Sử dụng Spring Security trong Spring Boot
- Custom authentication filter đăng nhập không cần password trong Spring Security
Xem thêm Việc làm IT hấp dẫn trên TopDev