網頁

2020/5/30

Spring Security 多角色權限範例

本篇使用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存取UserUserRepo介面。

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());
    }

}

存取RoleRoleRepo介面。

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);
    }

}

存取UserRoleUserRoleRepo介面。

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),USERROLE透過中介的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


沒有留言:

張貼留言