Multiple login page với Spring Security

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é!

  Bảo mật ứng dụng Java web bởi Spring Security
  Cách sử dụng properties trong tập tin cấu hình của Spring

Ứ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:

Multiple login page với Spring Security

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ả:

Multiple login page với Spring Security

Để 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:

Multiple login page với Spring Security

Đăng nhập với user “user” và password là “user”, các bạn sẽ thấy kết quả như sau:

Multiple login page với Spring Security

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:

Multiple login page với Spring Security

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:

Xem thêm Việc làm IT hấp dẫn trên TopDev