本篇使用Spring Security Basic Authentication驗證,使用者有多種角色(role)權限,搭配JSR-250 @RoleAllowed
做REST API的存取限制。
範例環境:
- Java 11
- Maven
- Spring Boot 2.2.6.RELEASE
- Spring Web
- Spring Security
- Spring Data JPA
- H2 Database
- Lombok
使用者只有單一角色請參考「Spring Security 簡單角色權限範例」。
因為Spring constructor依賴注入搭配Lombok的@AllArgsConstructor
,所以類別成員變數不用@Autowired
即可取得Bean的實例。
Spring Security配置類DemoWebSecurityConfig
。使用Basic Authentication驗證,使用自訂UserDetailsService
取得使用者資訊。啟用@RoleAllowed
。
DemoWebSecurityConfig
package com.abc.demo3.config.security;
import lombok.AllArgsConstructor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@AllArgsConstructor
@EnableGlobalMethodSecurity(jsr250Enabled = true)
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
// .permitAll()
.authenticated()
.and().formLogin().disable()
.httpBasic()
.and().csrf().disable();
}
}
UserService
實作UserDetailsService
。覆寫loadUserByUsername()
方法來委託UserDao
從資料庫取得使用者資訊及權限。
UserService
package com.abc.demo3.service;
import com.abc.demo3.dao.RoleDao;
import com.abc.demo3.dao.UserDao;
import com.abc.demo3.dao.UserRoleDao;
import com.abc.demo3.entity.Role;
import com.abc.demo3.entity.User;
import lombok.AllArgsConstructor;
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 java.util.List;
@AllArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserDao userDao;
private final UserRoleDao userRoleDao;
private final RoleDao roleDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.getByUsername(username).orElse(new User());
UserDetails userDetails = org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password("{noop}" + user.getPassword())
.roles(getRoles(user.getId()))
.build();
return userDetails;
}
private String[] getRoles(Long userId) {
return userRoleDao.getByUserId(userId).stream()
.map(e -> roleDao.getById(e.getRoleId())
.map(Role::getRoleName).orElse(null))
.toArray(String[]::new);
}
public List<String> getAllUserNames() {
return userDao.getAllUserNames();
}
public List<String> getAllRoleNames() {
return roleDao.getAllRoleNames();
}
}
UserDao
負責從資料庫取得使用者資料。在實例建構後建立預設的使用者。
UserDao
package com.abc.demo3.dao;
import com.abc.demo3.entity.User;
import com.abc.demo3.repo.UserRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@AllArgsConstructor
@Service
public class UserDao {
private final UserRepo userRepo;
@PostConstruct
void init() {
List<User> userList = List.of(
User.builder()
.username("david") // PRODUCT_MANAGER, HR_MANAGER
.password("123").build(),
User.builder()
.username("andy") // PRODUCT_MANAGER
.password("123").build(),
User.builder()
.username("amber") // PRODUCT_STAFF
.password("123").build(),
User.builder()
.username("bob") // HR_MANAGER
.password("123").build(),
User.builder()
.username("bill") // HR_STAFF
.password("123").build(),
User.builder()
.username("clare") // PRODUCT_STAFF, HR_STAFF
.password("123").build());
userRepo.saveAll(userList);
}
public List<String> getAllUserNames() {
return userRepo.findAll().stream()
.map(User::getUsername)
.collect(Collectors.toList());
}
public Optional<User> getByUsername(String username) {
return userRepo.findByUsername(username);
}
}
Spring Data JPA存取User
的UserRepo
介面。
UserRepo
package com.abc.demo3.repo;
import com.abc.demo3.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepo extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
與資料表USER
映設的entity實體類User
。不過這邊使用H2 in-memory database來模擬資料庫。
User
package com.abc.demo3.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
}
RoleDao
負責從資料庫取得角色資料。在實例建構後建立預設的角色。
RoleDao
package com.abc.demo3.dao;
import com.abc.demo3.entity.Role;
import com.abc.demo3.repo.RoleRepo;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@AllArgsConstructor
@Service
public class RoleDao {
private final RoleRepo roleRepo;
@PostConstruct
void init() {
List<Role> roleList = List.of(
Role.builder().roleName("PRODUCT_MANAGER").build(),
Role.builder().roleName("HR_MANAGER").build(),
Role.builder().roleName("PRODUCT_STAFF").build(),
Role.builder().roleName("HR_STAFF").build());
roleRepo.saveAll(roleList);
}
public Optional<Role> getById(Long id) {
return roleRepo.findById(id);
}
public List<String> getAllRoleNames() {
return roleRepo.findAll().stream()
.map(Role::getRoleName)
.collect(Collectors.toList());
}
}
存取Role
的RoleRepo
介面。
RoleRepo
package com.abc.demo3.repo;
import com.abc.demo3.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleRepo extends JpaRepository<Role, Long> {
}
與資料表ROLE
映設的entity實體類Role
。
Role
package com.abc.demo3.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Role {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String roleName;
}
UserRoleDao
負責從資料庫取得角色及權限資料。在實例建構後建立使用者與角色的關連。
UserRoleDao
package com.abc.demo3.dao;
import com.abc.demo3.entity.Role;
import com.abc.demo3.entity.User;
import com.abc.demo3.entity.UserRole;
import com.abc.demo3.repo.RoleRepo;
import com.abc.demo3.repo.UserRepo;
import com.abc.demo3.repo.UserRoleRepo;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
@AllArgsConstructor
@Service
@DependsOn({"userDao", "roleDao"})
public class UserRoleDao {
private final UserRoleRepo userRoleRepo;
private final UserRepo userRepo;
private final RoleRepo roleRepo;
@PostConstruct
void init() {
List<User> userList = userRepo.findAll();
List<Role> roleList = roleRepo.findAll();
List<UserRole> userRoleList = new ArrayList<>();
for (User user : userList) {
for (Role role : roleList) {
String username = user.getUsername();
String roleName = role.getRoleName();
if (username.equals("david")
&& (roleName.equals("PRODUCT_MANAGER") || roleName.equals("HR_MANAGER"))) {
setUserRoleList(userRoleList, user.getId(), role.getId());
} else if (username.equals("andy")
&& roleName.equals("PRODUCT_MANAGER")) {
setUserRoleList(userRoleList, user.getId(), role.getId());
} else if (username.equals("amber")
&& roleName.equals("PRODUCT_STAFF")) {
setUserRoleList(userRoleList, user.getId(), role.getId());
} else if (username.equals("bob")
&& roleName.equals("HR_MANAGER")) {
setUserRoleList(userRoleList, user.getId(), role.getId());
} else if (username.equals("bill")
&& roleName.equals("HR_STAFF")) {
setUserRoleList(userRoleList, user.getId(), role.getId());
} else if (username.equals("clare")
&& (roleName.equals("PRODUCT_STAFF") || roleName.equals("HR_STAFF"))) {
setUserRoleList(userRoleList, user.getId(), role.getId());
}
}
}
userRoleRepo.saveAll(userRoleList);
}
private void setUserRoleList(List<UserRole> userRoleList, Long userId, Long roleId) {
UserRole userRole = UserRole.builder().userId(userId).roleId(roleId).build();
userRoleList.add(userRole);
}
public List<UserRole> getByUserId(Long userId) {
return userRoleRepo.findByUserId(userId);
}
}
存取UserRole
的UserRoleRepo
介面。
UserRoleRepo
package com.abc.demo3.repo;
import com.abc.demo3.entity.UserRole;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface UserRoleRepo extends JpaRepository<UserRole, Long> {
List<UserRole> findByUserId(Long userId);
}
與資料表USER_ROLE
映設的entity實體類UserRole
。
UserRole
package com.abc.demo3.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class UserRole {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
private Long userId;
private Long roleId;
}
一位使用者會有多種角色,一種角色可分配給多位使用者,所以使用者與角色的關係是多對多(Many to Many),USER
與ROLE
透過中介的USER_ROLE
來關連。
USER_ROLE
+------------------------+
USER | ID | USER_ID | ROLE_ID |
+--------------------------+ |------------------------|
| ID | USERNAME | PASSWORD | | 1 | 1 | 1 | ROLE
|--------------------------| |------------------------| +----------------------+
| 1 | david | ******** | | 2 | 1 | 2 | | ID | ROLE_NAME |
|--------------------------| |------------------------| |----------------------|
| 2 | andy | ******** | | 3 | 2 | 1 | | 1 | PRODUCT_MANAGER |
|--------------------------|1 *|------------------------|* 1|----------------------|
| 3 | amber | ******** |---------| 4 | 3 | 3 |---------| 2 | HR_MANAGER |
|--------------------------| |------------------------| |----------------------|
| 4 | bob | ******** | | 5 | 4 | 2 | | 3 | PRODUCT_STAFF |
|--------------------------| |------------------------| |----------------------|
| 5 | bill | ******** | | 6 | 5 | 4 | | 4 | HR_STAFF |
|--------------------------| |------------------------| +----------------------+
| 6 | clare | ******** | | 7 | 6 | 3 |
+--------------------------+ |------------------------|
| 8 | 6 | 4 |
+------------------------+
ActionController
為測試使用者權限的REST API Controller。
ActionController
package com.abc.demo3.controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.security.RolesAllowed;
@RestController
@RequestMapping("/action")
public class ActionController {
@RolesAllowed({"HR_MANAGER", "HR_STAFF", "PRODUCT_MANAGER"})
@GetMapping("/user/get")
public String getUser() {
String message = "get user";
System.out.println(message);
return message;
}
@RolesAllowed({"HR_MANAGER"})
@PostMapping("/user/add")
public String addUser() {
String message = "add user";
System.out.println(message);
return message;
}
@RolesAllowed({"HR_MANAGER", "HR_STAFF"})
@PatchMapping("/user/update")
public String updateUser() {
String message = "update user";
System.out.println(message);
return message;
}
@RolesAllowed({"HR_MANAGER"})
@DeleteMapping("/user/delete")
public String deleteUser() {
String message = "delete user";
System.out.println(message);
return message;
}
@RolesAllowed({"PRODUCT_MANAGER", "PRODUCT_STAFF"})
@GetMapping("/product/get")
public String getProduct() {
String message = "get product";
System.out.println(message);
return message;
}
@RolesAllowed({"PRODUCT_MANAGER"})
@PostMapping("/product/add")
public String addProduct() {
String message = "add product";
System.out.println(message);
return message;
}
@RolesAllowed({"PRODUCT_MANAGER", "PRODUCT_STAFF"})
@PostMapping("/product/update")
public String updateProduct() {
String message = "update product";
System.out.println(message);
return message;
}
@RolesAllowed({"PRODUCT_MANAGER"})
@PostMapping("/product/delete")
public String deleteProduct() {
String message = "delete product";
System.out.println(message);
return message;
}
}
完整範例請見github。
沒有留言:
張貼留言