CÔNG NGHỆ THÔNG TIN >> BÀI VIẾT CHỌN LỌC

Bảo mật phân quyền người dùng trong ứng dụng Web với Spring Boot 3 Security

Đăng lúc: 03:02 PM - 03/06/2024 bởi Charles Chung - 119

Trong bài viết này tôi sẽ hướng dẫn các bạn sử dụng Security trong Spring Boot 3 để xác thực người dùng và phân quyền truy cập vào các tài nguyên trong website.

1. Vấn đề

Xây dựng website với Spring Boot 3 gồm 2 phần

  • Backend: Trang quản lý các dữ liệu trên web, trang này chỉ cho phép tài khoản có ROLE_ADMIN vào.
  • FrontEnd: Trang dành cho khách hàng xem, tìm kiếm, chọn hàng để mua, nếu đặt hàng thì phải đăng nhập (bất kỳ ROLE nào).
  • Cơ sở dữ liệu SQL Server 2016
  • Sử dung các thư viện Security, Hibernate JPA trong Spring Boot.

2. Giải pháp

- Truy cập trang https://start.spring.io/ tạo một dự án với các thông số như hình dưới

- Chọn gerneate để tải Project Spring Boot về và mở ra với Visual Code/ Eclipse/ Intellij IDE

- Các dependency trong file pom.xml (nếu generate thiếu thì bổ sung vào nhé)

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity6</artifactId>
			<!-- Temporary explicit version to fix Thymeleaf bug -->
			<version>3.1.1.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.microsoft.sqlserver</groupId>
			<artifactId>mssql-jdbc</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

- Cấu hình kết nối với SQL Server trong file Application.properties

spring.application.name=securityweb
spring.datasource.url= jdbc:sqlserver://localhost:1433;encrypt=true;trustServerCertificate=true;databaseName=springbootdb
spring.datasource.username= sa
spring.datasource.password= 123465
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.SQLServerDialect
spring.jpa.hibernate.ddl-auto= update
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

- Tạo Enum models/ERole.java

package com.hanam88.securityweb.models;

public enum ERole {
  ROLE_USER,
  ROLE_MODERATOR,
  ROLE_ADMIN
}

- Tạo Class models/Account.java

package com.hanam88.securityweb.models;

import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;

@Entity
@Table(name = "account", uniqueConstraints = {
		@UniqueConstraint(columnNames = "username"),
		@UniqueConstraint(columnNames = "email")
})
public class Account {
	@Id
	@Column(name = "accountid", length = 36)
	private String accountId;

	@NotBlank
	@Size(max = 64)
	@Column(name = "username", length = 64)
	private String username;

	@NotBlank
	@Column(name = "password", length = 256)
	private String password;

	@Email
	@Column(name = "email", length = 64)
	private String email;

	@Column(name = "phone", length = 64)
	private String phone;

	@Column(name = "fullname", columnDefinition = "nvarchar(100)")
	private String fullname;

	@Column(name = "address", columnDefinition = "nvarchar(256)")
	private String address;

	@Column(name = "picture", length = 512)
	private String picture;

	@Column(name = "enabled")
	private boolean enabled;

	@ManyToMany(fetch = FetchType.LAZY)
	@JoinTable(name = "account_role", joinColumns = @JoinColumn(name = "accountid"), inverseJoinColumns = @JoinColumn(name = "roleid"))
	private Set<Role> roles = new HashSet<>();

	public Account() {

	}

	public Account(String accountId, String username, String password,
			String email, String phone, String fullname, String address, String picture, boolean enabled) {
		this.accountId = accountId;
		this.username = username;
		this.password = password;
		this.email = email;
		this.phone = phone;
		this.fullname = fullname;
		this.address = address;
		this.picture = picture;
		this.enabled = enabled;
	}

	public String getAccountId() {
		return accountId;
	}

	public void setAccountId(String accountId) {
		this.accountId = accountId;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

	public String getFullname() {
		return fullname;
	}

	public void setFullname(String fullname) {
		this.fullname = fullname;
	}

	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String getPicture() {
		return picture;
	}

	public void setPicture(String picture) {
		this.picture = picture;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	public Set<Role> getRoles() {
		return roles;
	}

	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}

}

- Tạo Class models/Role.java

package com.hanam88.securityweb.models;

import jakarta.persistence.*;

@Entity
@Table(name = "roles")
public class Role {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="roleid")
	private Integer roleId;

	@Enumerated(EnumType.STRING)
	@Column(name="rolename",length = 20)
	private ERole roleName;

	public Role() {

	}

	public Role(Integer roleId, ERole roleName) {
		super();
		this.roleId = roleId;
		this.roleName = roleName;
	}

	public Integer getRoleId() {
		return roleId;
	}

	public void setRoleId(Integer roleId) {
		this.roleId = roleId;
	}

	public ERole getRoleName() {
		return roleName;
	}

	public void setRoleName(ERole roleName) {
		this.roleName = roleName;
	}

}

- Tạo Class models/AccountDetail.java

package com.hanam88.securityweb.models;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class AccountDetails implements UserDetails {
    private Collection<? extends GrantedAuthority> authorities;
    private String email;
    private String fullName;
    private String password;
    private String username;
    private String picture;
    private String phone;
    private String address;
    private boolean enabled;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;

   
    public AccountDetails(Collection<? extends GrantedAuthority> authorities, String email, String fullName,
            String password, String username, String picture, String phone, String address, boolean enabled,
            boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired) {
        this.authorities = authorities;
        this.email = email;
        this.fullName = fullName;
        this.password = password;
        this.username = username;
        this.picture = picture;
        this.phone = phone;
        this.address = address;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPicture() {
        return picture;
    }

    public void setPicture(String picture) {
        this.picture = picture;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }


    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
    
}

- Tạo interface repository/RoleRepository.java 

package com.hanam88.securityweb.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.hanam88.securityweb.models.ERole;
import com.hanam88.securityweb.models.Role;
@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
	Optional<Role> findByRoleName(ERole roleName);
}

- Tạo interface repository/AccountRepository.java 

package com.hanam88.securityweb.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.hanam88.securityweb.models.Account;

@Repository
public interface AccountRepository extends JpaRepository<Account, String> {
	Optional<Account> findByUsername(String username);

	Boolean existsByUsername(String username);

	Boolean existsByEmail(String email);
}

- Tạo Classs services/AccountDetailService.java 

package com.hanam88.securityweb.services;

import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.hanam88.securityweb.models.AccountDetails;
import com.hanam88.securityweb.models.ERole;
import com.hanam88.securityweb.models.Role;
import com.hanam88.securityweb.models.Account;
import com.hanam88.securityweb.repository.AccountRepository;

@Service
public class AccountDetailService implements UserDetailsService {
    @Autowired
    AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Account> user = accountRepository.findByUsername(username);
        if (!user.isPresent())
            return null;
        Account acc=user.get();
        //xử lý lấy roles của người dùng đưa vào GrantedAuthority
        Collection<GrantedAuthority> grantedAuthoritySet = new HashSet<>();
        Set<Role> roles = acc.getRoles();
        for (Role userRole : roles) {
        	ERole rolename=userRole.getRoleName();
        	grantedAuthoritySet.add(new SimpleGrantedAuthority(rolename.name()));
		}
        //trả về đối tượng AccountDetails
        return new AccountDetails(grantedAuthoritySet, acc.getEmail(), acc.getFullname(), acc.getPassword(), acc.getUsername(), acc.getPicture(),acc.getPhone(),acc.getAddress(),  acc.isEnabled(),true,true,true);
    }
}

- Tạo Class config/WebConfigSecurity.java

package com.hanam88.securityweb.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.web.SecurityFilterChain;

import com.hanam88.securityweb.services.AccountDetailService;

@Configuration
@EnableWebSecurity
public class WebConfigSecurity {
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.csrf((c) -> c.disable())
				.authorizeHttpRequests(authorize -> authorize
						.requestMatchers("/",
								"/403",
								"/success",
								"/css/**",
								"/js/**",
								"/backend/**",
								"/images/**")
						.permitAll()
						.requestMatchers("/admin/**").hasRole("ADMIN"))
				.formLogin((formLogin) -> formLogin
						.usernameParameter("username")
						.passwordParameter("password")
						.loginPage("/login")
						.defaultSuccessUrl("/success", true)
						.failureUrl("/login?error").permitAll())
				.logout((logout) -> logout.logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll())
				.exceptionHandling((ex) -> ex.accessDeniedPage("/403"));

		return http.build();
	}

	@Bean
	public DaoAuthenticationProvider authenticationProvider() {
		DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
		authProvider.setUserDetailsService(userDetailsService());
		authProvider.setPasswordEncoder(passwordEncoder());
		return authProvider;
	}

	@Bean
	public UserDetailsService userDetailsService() {
		return new AccountDetailService();
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

}

- Tạo Class controllers/admin/AdminController.java

package com.hanam88.securityweb.controllers.admin;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AdminController {

    @GetMapping(value = { "/admin", "/admin/index" })
    public String index(Model model) {
        
            return "admin/index";
    }
    @GetMapping("/403")
    public String authorize(Model model) {
        return "admin/403";
    }
    @GetMapping("login")
    public String login() {
        return "admin/login";
    }

}

- Tạo Class controllers/client/HomeController.java

package com.hanam88.securityweb.controllers.client;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.hanam88.securityweb.models.AccountDetails;
import com.hanam88.securityweb.models.ERole;

@Controller
public class HomeController {

    @GetMapping(value = "/")
    public String index(Model model) {
        return "client/home";
    }
    @GetMapping(value = "/success")
    public String success(){
        // điều hướng login
        try {
            AccountDetails account = (AccountDetails) SecurityContextHolder.getContext().getAuthentication()
                   .getPrincipal();
           if (account.getAuthorities().toString().contains(ERole.ROLE_ADMIN.name()))
               return "redirect:/admin";
       } catch (Exception e) {
           
       }
       return "redirect:/";
    }
}

- Cấu trúc source code của project 

- Cấu trúc tài nguyên của project (tải source code về xem nhé)

- Chạy project rồi mở trình duyệt truy cập http://localhost:8080 để sinh database

- Nhập dữ liệu mẫu cho 3 bảng

--Thêm dữ liệu vào bảng roles
INSERT INTO dbo.roles(rolename) 
values
('ROLE_ADMIN'),
('ROLE_USER'),
('ROLE_MODERATOR')
--Thêm dữ liệu vào bảng Account
	INSERT INTO dbo.Account
	(
	    AccountId,
	    UserName,
	    Password,
	    FullName,
	    Picture,
	    Email,
	    Address,
	    Phone,
	    Enabled
	)
	VALUES
	(   '844259FE-64C2-4EBF-9FD8-94F3B26FA63F',   
	    'chungld',  
	    '$2a$12$HTTAeDbIxXuqqQpZK.oLX.WJ48fiVAP7kQ5YQ1fwg0FCIhIjl46jG',   
	    N'Lại Đức Chung', 
	    'chungld.jpg',  
	    'chungld@bachkhoa-aptech.edu.vn',  
	    N'238 Hoàng Quốc Việt Cầu Giấy Hà Nội',  
	    '0339513657',   
	    1 
	    ),
		('BE8922F0-713A-471B-A91F-4DD543F828D6',   
	    'haibt',  
	    '$2a$12$HTTAeDbIxXuqqQpZK.oLX.WJ48fiVAP7kQ5YQ1fwg0FCIhIjl46jG',  
	    N'Bùi Thanh Hải', 
	    'haibt.jpg',   
	    'haibt@bachkhoa-aptech.edu.vn',  
	    N'Hà Đông-Hà Nội', 
	    '0339513658',  
	    1  
	    )
--Thêm dữ liệu vào bảng account_role
INSERT INTO dbo.account_role(accountid, roleid)
values('844259FE-64C2-4EBF-9FD8-94F3B26FA63F',1),
('BE8922F0-713A-471B-A91F-4DD543F828D6',2)

- Chạy project rồi mở trình duyệt và truy cập http://localhost:8080

- Màn hình đăng nhập

- Khi đăng nhập sai

- Khi đăng nhập với ROLE_ADMIN thì điều hướng vào trang Admin và hiển thị Avatar, Họ và tên

- Khi đăng nhập với ROLE_USER thì điều hướng về trang chủ và hiển thị Avatar, Họ và tên

alt text

- Màn hình thông báo quyền

Tải source code

Video hướng dẫn dùng

thay lời cảm ơn!

QUẢNG CÁO - TIẾP THỊ