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 - 538Trong 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
- 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!
Các bài cũ hơn
- Hướng dẫn cài đặt môi trường lập trình Dart và Flutter trên Windows 10 từ A-Z (03:47 PM - 15/05/2024)
- Phát hiện khuôn mặt người trong ảnh sử dụng thư viện Haar Cascade với OpenCV-Python (09:53 AM - 27/03/2024)
- Hướng dẫn đăng nhập trên trang chủ và trang quản trị Spring MVC-MongoDB (05:58 PM - 21/03/2024)
- Hướng dẫn xây dựng Layout trang User và Admin trong Spring MVC (07:25 PM - 18/03/2024)
- Xử lý giỏ hàng trong Spring MVC Hibernate-SQL Server (02:22 PM - 11/03/2024)