SpringBoot2.0實戰 | 第三十章:整合SpringSecurity之基於SpEL表達式實現動態方法鑑權

通過前面的文章,我們已經實現了基於數據進行登錄鑑權及基於註解的方式進行方法鑑權

註解方式的方法鑑權:
通過 @EnableGlobalMethodSecurity 註解來開啓方法鑑權。

  • securedEnabled:開啓 @Secured 註解
    • 單個角色:@Secured(“ROLE_USER”)
    • 多個角色任意一個:@Secured({“ROLE_USER”,“ROLE_ADMIN”})
  • prePostEnabled:開啓 @PreAuthorize 及 @PostAuthorize 註解,分別適用於進入方法前後進行鑑權,支持表達式
    • 允許所有訪問:@PreAuthorize(“true”)
    • 拒絕所有訪問:@PreAuthorize(“false”)
    • 單個角色:@PreAuthorize(“hasRole(‘ROLE_USER’)”)
    • 多個角色與條件:@PreAuthorize(“hasRole(‘ROLE_USER’) AND hasRole(‘ROLE_ADMIN’)”)
    • 多個角色或條件:@PreAuthorize(“hasRole(‘ROLE_USER’) OR hasRole(‘ROLE_ADMIN’)”)
  • jsr250Enabled:開啓 JSR-250 相關注解
    • 允許所有訪問:@PermitAll
    • 拒絕所有訪問:@DenyAll
    • 多個角色任意一個:@RolesAllowed({“ROLE_USER”, “ROLE_ADMIN”})

雖然非常靈活,但是畢竟是硬編碼,不符合實際的生產需求,在項目中,每個角色的可訪問權限必須是可調整的,一般情況下使用數據庫進行持久化。

目標

整合 SpringSecurity 及 MybatisPlus 實現使用讀取數據庫數據進行方法鑑權

思路

使用配置類的 HttpSecurity 提供的 access 方法,通過擴展SpEL表達式,實現自定義鑑權

.access("@authService.canAccess(request, authentication)")

其中 authService 是 Spring 容器中的 Bean,canAccess 是其中的一個方法。

@Service
public class AuthService {
    public boolean canAccess(HttpServletRequest request, Authentication authentication) {
        //在這裏編寫校驗代碼…
        return true;
    }
}

準備工作

創建用戶表 user、角色表 role、用戶角色關係表 user_role,資源表 resource,資源角色關係表 role_resource

CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(32) NOT NULL COMMENT '角色名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COMMENT='角色';

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL COMMENT '用戶名',
  `password` varchar(128) NOT NULL COMMENT '密碼',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COMMENT='用戶';

CREATE TABLE `user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`user_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='用戶角色關係表';

CREATE TABLE `resource` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `url` varchar(128) NOT NULL COMMENT '請求路徑',
  PRIMARY KEY (`id`),
  UNIQUE KEY (`url`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COMMENT='資源';

CREATE TABLE `role_resource` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `resource_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`resource_id`,`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='資源角色關係表';

操作步驟

添加依賴

引入 Spring Boot Starter 父工程

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
</parent>

添加 springSecuritymybatisPlus 的依賴,添加後的整體依賴如下

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.2.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

配置

配置一下數據源

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&useSSL=false
    username: app
    password: 123456

編碼

用戶登錄相關代碼請參考 第二十五章:整合SpringSecurity之使用數據庫實現登錄鑑權,這裏不再粘貼。

實體類

角色實體類 Role,實現權限接口 GrantedAuthority

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("role")
public class Role implements GrantedAuthority {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String rolename;

    @Override
    public String getAuthority() {
        return this.rolename;
    }
}

資源實體

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("resource")
public class Resource {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String url;

}

資源角色關係實體

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("role_resource")
public class RoleResource {

    @TableId(type = IdType.AUTO)
    private Long id;
    private Long resourceId;
    private Long roleId;

}
Repository 層

分別爲三個實體類添加 Mapper

@Mapper
public interface RoleRepository extends BaseMapper<Role> {
}
@Mapper
public interface ResourceRepository extends BaseMapper<Resource> {
}
@Mapper
public interface RoleResourceRepository extends BaseMapper<RoleResource> {
}
實現自定義方法鑑權
@Service
@AllArgsConstructor
public class AuthService {

    private ResourceRepository resourceRepository;
    private RoleResourceRepository roleResourceRepository;
    private RoleRepository roleRepository;

    public boolean canAccess(HttpServletRequest request, Authentication authentication) {
        String uri = request.getRequestURI();
        List<Role> requestRoles = getRolesForResource(uri);
        if (requestRoles != null && !requestRoles.isEmpty()) {
            for (Role requestRole : requestRoles) {
                for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                    if (requestRole.getAuthority().equals(grantedAuthority.getAuthority())) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private List<Role> getRolesForResource(String uri) {
        if (StringUtils.isEmpty(uri)) {
            return Collections.emptyList();
        }
        Resource resource = resourceRepository.selectOne(
                new QueryWrapper<Resource>().lambda().eq(Resource::getUrl, uri));
        if (resource == null) {
            return Collections.emptyList();
        }
        List<RoleResource> roleResources = roleResourceRepository.selectList(
                new QueryWrapper<RoleResource>().lambda().eq(RoleResource::getResourceId, resource.getId()));
        if (roleResources == null || roleResources.isEmpty()) {
            return Collections.emptyList();
        }
        List<Long> roleIds = roleResources.stream().map(RoleResource::getRoleId).collect(Collectors.toList());
        return roleRepository.selectList(
                new QueryWrapper<Role>().lambda().in(Role::getId, roleIds));
    }

}
註冊配置

不用再聲明 @EnableGlobalMethodSecurity 註解,註冊自定義鑑權方法 authService.canAccess

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().access("@authService.canAccess(request, authentication)")
//            .anyRequest().authenticated()
            .and()
            .formLogin().and().httpBasic();
    }
}
去掉原來的方法鑑權相關注解
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

//    @Secured("ROLE_USER")
    @GetMapping("/secure")
    public String secure() {
        return "Hello Security";
    }

//    @PreAuthorize("true")
    @GetMapping("/authorized")
    public String authorized() {
        return "Hello World";
    }

//    @PreAuthorize("false")
    @GetMapping("/denied")
    public String denied() {
        return "Goodbye World";
    }

}
啓動類
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

驗證結果

初始化數據

執行測試用例進行初始化數據

@Slf4j
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = Application.class)
@AllArgsConstructor
public class SecurityTest {

    private UserRepository userRepository;
    private UserRoleRepository userRoleRepository;
    private RoleRepository roleRepository;
    private ResourceRepository resourceRepository;
    private RoleResourceRepository roleResourceRepository;

    @Test
    public void initData() {
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "admin", new BCryptPasswordEncoder().encode("123456"), null));
        userList.add(new User(2L, "user", new BCryptPasswordEncoder().encode("123456"), null));

        List<Role> roleList = new ArrayList<>();
        roleList.add(new Role(1L, "ROLE_ADMIN"));
        roleList.add(new Role(2L, "ROLE_USER"));

        List<UserRole> urList = new ArrayList<>();
        urList.add(new UserRole(1L, 1L, 1L));
        urList.add(new UserRole(2L, 1L, 2L));
        urList.add(new UserRole(3L, 2L, 2L));

        List<Resource> resourceList = new ArrayList<>();
        resourceList.add(new Resource(1L, "/hello"));
        resourceList.add(new Resource(2L, "/secure"));

        List<RoleResource> rrList = new ArrayList<>();
        rrList.add(new RoleResource(1L, 1L, 1L));
        rrList.add(new RoleResource(1L, 2L, 1L));
        rrList.add(new RoleResource(1L, 1L, 2L));

        userList.forEach(userRepository::insert);
        roleList.forEach(roleRepository::insert);
        urList.forEach(userRoleRepository::insert);
        resourceList.forEach(resourceRepository::insert);
        rrList.forEach(roleResourceRepository::insert);
    }

}
校驗

使用 admin 登錄可以訪問 /hello/secure,使用 user 登錄則只能訪問 /hello

源碼地址

本章源碼 : https://gitee.com/gongm_24/spring-boot-tutorial.git

參考

249.Spring Boot+Spring Security:基於URL動態權限:擴展access()的SpEL表達式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章