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 pages 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:
1 2 3 4 5 6 7 8 9 |
<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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<!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 này trong class ApplicationController như sau::
1 2 3 4 |
@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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
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ì ứng dụng của chúng ta sẽ tự động redirect tới trang bắt đầu với “/user” mà chúng ta đã request tới.
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.
Dinh Tran
Mình cảm thấy hơi khó hiểu, user đang login vào hệ thống thì user đang là anonymous , ko hề có role hay gì cả, làm sao lại check dk hasrole = admin để điều hướng đến trang login của admin??
Nếu đây là trang login thì bản chất bất cứ ai có url đều có thể truy cập được, cái cần thiết là nếu dùng user thường mà login vào trang của admin thì sẽ bị redirect hoặc chặn thôi
Khanh Nguyen
Bạn nên hiểu là có 2 request khác nhau:
* /user/view
* /admin/view
User thì có thể là anonymous user hoặc là user, sau khi đăng nhập, có role USER hoặc ADMIN.
Anonymous user thì chắc chắn không request được tới 2 request ở trên rồi. Lúc này, app mình sẽ redirect tới trang login để user login tùy theo request mà họ đang muốn, sau đó thì mới kiểm tra logged in user có quyền access tới request mà họ muốn không? Dựa vào role của logged user.
Không hiểu gì thì cứ nói mình nhé!