通过前面的文章,我们已经实现了基于数据进行登录鉴权及基于注解的方式进行方法鉴权
注解方式的方法鉴权:
通过 @EnableGlobalMethodSecurity
注解来开启方法鉴权。
虽然非常灵活,但是毕竟是硬编码,不符合实际的生产需求,在项目中,每个角色的可访问权限必须是可调整的,一般情况下使用数据库进行持久化。
整合 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.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.0.5.RELEASEversion>
parent>
添加 springSecurity
及 mybatisPlus
的依赖,添加后的整体依赖如下
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
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;
}
分别为三个实体类添加 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表达式