A place to hold mainly reading notes, and some technical stuff occasionally. 这里主要是一些读书笔记、感悟;还有部分技术相关的内容。
目录[-]
本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验 SpringSecurity
的各项Feature。
RBAC是什么?Role Based Access Control,关于RBAC的介绍,网上资源很多,这里仅简单描述下。
新建一个 SpringBoot
项目,起名 springboot-security-rbac
,核心依赖为 Web
与 SpringSecurity
:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
创建资源接口: /user/add
, /user/query
,以及默认的home路径 /
,用以展示登录用户信息,若未登录,则展示匿名用户信息。
@RestController
@Slf4j
public class RoleAccessController {
@GetMapping(value = "/user/add")
public String accessResource1() {
return " Access Resource 1: Add User";
}
@GetMapping(value = "/user/query")
public String accessResource2() {
return " Access Resource 2: Query User";
}
@GetMapping(value = "/")
public String index() {
log.info(SecurityContextHolder.getContext().getAuthentication().toString());
return "Welcome " + SecurityContextHolder.getContext().getAuthentication();
}
}
@Configuration
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// There is no PasswordEncoder mapped for the id "null"
PasswordEncoder encoder = passwordEncoder();
String yourPassword = "123";
System.out.println("Encoded password: " + encoder.encode(yourPassword));
// Config account info and permissions
auth.inMemoryAuthentication()
.withUser("dev").password(encoder.encode(yourPassword)).roles("dev", "test")
.and()
.withUser("test").password(encoder.encode(yourPassword)).authorities("ROLE_test");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/add").hasRole("dev")
.antMatchers("/user/query").hasAuthority("ROLE_test")
.antMatchers("/user/**").authenticated()
.anyRequest().permitAll() // Let other request pass
.and()
.formLogin();
}
}
此时访问 /user/add
或 /user/query
都会跳转至登录页面;
dev
用户同时具备 ROLE_dev
与 ROLE_test
两个角色,此时访问 /user/add
或 /user/query
都会成功;
test
用户仅具有 ROLE_test
角色,此时访问 /user/add
失败,访问 /user/query
成功;
Note:
roles()
与 authorities()
,两者区别如下:roles()
时,参数不可加 ROLE_
前缀,否则报错。因为 roles()
自动会加这个前缀,参考源码 User类
:authorities()
时,则没有限制,配合 hasAuthority
使用时,权限命名一致即可。public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<>(roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"),
() -> role + " cannot start with ROLE_ (it is automatically added)");
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return authorities(authorities);
}
roles()
与 authorities()
不可在一个用户上同时使用,否则会发生覆盖,仅有最后一个会生效,参考源码 User类
:在启动类或配置类上添加注解: @EnableWebSecurity
与 @EnableGlobalMethodSecurity(securedEnabled=true)
,表示启用 SpringSecurity
默认的方法授权。
@SpringBootApplication
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled=true)
public class SpringbootSecurityRbacApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSecurityRbacApplication.class, args);
}
}
为了方便演示,这里新建一个Controller: SecuredAccessController
,资源请求以及用户授权都与上一个实验相同,这里可以删除 SecurityConfig
类中关于 http
请求权限的配置。
@RestController
@RequestMapping("/user0")
@Slf4j
public class SecuredAccessController {
@GetMapping(value = "/add")
@Secured({"ROLE_dev", "ROLE_test"}) // only support OR relation
public String accessResource1() {
return " Access Resource 1: Add User";
}
@GetMapping(value = "/query")
@Secured({"ROLE_test"})
public String accessResource2() {
return " Access Resource 2: Query User";
}
@GetMapping(value = "/")
public String index() {
log.info(SecurityContextHolder.getContext().getAuthentication().toString());
return "Welcome " + SecurityContextHolder.getContext().getAuthentication();
}
}
结果如下:
/user0/add
与 /user0/query
;/user0/add
与 /user0/query
;Note:
@Secured({"ROLE_dev", "ROLE_test"})
这里采用了角色组合,是“或”的关系。SpringSecurity
默认的方法授权的局限性,实际中更多地会使用 PreAuthorize
,可以实现“或”、“与”的关系,支持 Spring EL表达式
,看下个实验。配置类的注解增加属性配置 prePostEnabled=true
,变成: @EnableGlobalMethodSecurity(securedEnabled=true, prePostEnabled=true)
。
为了方便演示,这里新建一个Controller: PrePostAccessController
,资源请求以及用户授权都与上一个实验相同,这里可以删除 SecurityConfig
类中关于 http
请求权限的配置。
@RestController
@RequestMapping("/user1")
@Slf4j
public class PrePostAccessController {
@GetMapping(value = "/add")
// @PreAuthorize("hasRole('ROLE_dev')")
@PreAuthorize("hasRole('dev')")
// @PreAuthorize("hasAnyRole('ROLE_dev', 'ROLE_test')")
// @PreAuthorize("hasRole('ROLE_dev') and hasRole('ROLE_test')")
public String accessResource1() {
return " Access Resource 1: Add User";
}
@GetMapping(value = "/query")
@PreAuthorize("hasAuthority('ROLE_test')")
public String accessResource2() {
return " Access Resource 2: Query User";
}
@GetMapping(value = "/")
// @PreAuthorize("authenticated")
public String index() {
log.info(SecurityContextHolder.getContext().getAuthentication().toString());
return "Welcome " + SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping(value = "/res")
@PostAuthorize("returnObject==true")
public boolean response() {
int i = new Random().nextInt();
log.info("Response, {}", i);
return i > 0;
}
}
分别演示 PreAuthorize
本身及多个权限组合与 PostAuthorize
,可打开注释进行测试,具体结果:略。
其实,除了 securedEnabled
与 prePostEnabled
, @EnableGlobalMethodSecurity
还有第三个选项: jsr250Enabled
具体使用方法,此处不作演示了。
Note:虽然 SpringSecurity
的 @EnableGlobalMethodSecurity
注解提供了三种选项来使用方法级别的授权,但是实际使用时不建议混合使用,参考官方文档关于这一点的说明:
If you have any questions or any bugs are found, please feel free to contact me.
Your comments and suggestions are welcome!