在前两节的基础上,对权限控制作进一步的分析与设计。
RBAC(Role-Base Access Control,基于角色的访问控制)
本篇内容基于个人理解,不当之处,欢迎批评指正。
前两篇内容:
1、OAuth2中用户访问的基本流程
- 用户经过认证/授权后,进入客户端(认证中心给客户端发放令牌),客户端携带令牌访问对应的资源。
- 客户端是用户和资源之外的第三方,要想访问资源必须得到用户的允许。
- 用户拥有资源,通过客户端去访问,把访问权限赋于给了客户端。
2、SCOPE、ROLE、AUTH 区别
- SCOPE:范围;指用户授权客户端可以访问的范围。客户端只能在这个范围内去访问。是针对客户端来说的。
- ROLE:角色;是用户的身份。是针对用户来说的。
- AUTH:权限;是角色所拥有的。角色与权限是多对多的关系;一个角色可以有多个权限,一个权限也可以同时被多个角色所拥有。权限也可以直接针对于用户,如果用户不指定角色,可以直接把权限赋于用户。
区别 | 含义 | 面向对象 |
---|---|---|
SCOPE | 范围 | 客户端 |
ROLE | 角色 | 用户 |
AUTH | 权限 | 角色 或 用户 |
3、server、resource、client 中访问主体的区别
从图中可以看出,在每个系统中的访问主体及权限是不同的(这里的权限是统称,包括SCOPE、ROLE、AUTH,不仅仅指AUTH)
-
当用户登录后,在认证中心内,访问主体就是 第三方用户,它的权限是他在认证中心中的权限,和我方系统无关
-
在客户端中,访问主体还是 第三方用户,权限包括:用户授于客户端的
SCOPE
,以及ROLE_USER
;ROLE_USER
表示这是一个经过认证的用户,不管第三方用户在第三方系统中是什么身份,只要进入到我方系统中,就是ROLE_USER
身份;对应于ROLE_ANONYMOUS
(未认证用户) -
客户端携带令牌访问资源,在资源服务器中访问主体就是 客户端,权限只有:
SCOPE
;因为客户端是在用户授权下去访问的,所以在认证中心生成令牌时,只包括了用户授于的 SCOPE,不可能把用户的身份ROLE也赋于客户端。
4、访问控制分析
通过上面的分析可以发现,资源端只有 SCOPE
,不可能用 ROLE
或 AUTH
去控制用户的访问。认证中心不负责访问资源,要想通过 ROLE
或 AUTH
去控制用户访问资源,只能在 客户端 去操作。资源API在客户端有对应的接口,要想控制资源API,就控制客户端的对应接口就可以了。只要用户能访问客户端的某个API接口,它就能访问与之对应的资源API。
- 资源API 面向 SCOPE 开放;
- 客户端API 面向 ROLE 或 AUTH 开放;
但是,所有第三方用户,进入我方系统后,都具有 ROLE_USER
身份,身份是一样的,如何在客户端中通过 ROLE
或 AUTH
去控制用户访问资源呢?
解决方案:添加本地用户,赋于不同的 ROLE
或 AUTH
;第三方用户与本地用户实现绑定;通过本地用户的 ROLE
或 AUTH
去控制用户访问资源。这是三方登录的一个通用做法。
那第三方用户进入我方系统后,如何改变他的身份?把本地的 ROLE
或 AUTH
赋给他呢?办法就是权限提升!
5、客户端权限提升
- 第三方用户进入我方系统后,从
SecurityContextHolder
中获取第三方用户的name
和authorities
- 根据第三方用户的
name
,查询绑定的本地用户,进而得到本地用户的authorities
- 把本地
authorities
加入到 第三方用户的authorities
中- 重新生成新的
Authentication
- 注入
SecurityContextHolder
中,替换原来的authorities
,完成权限提升
public class IndexController {
@Autowired
UserDetailsService userDetailsService;
@GetMapping("/")
public String user(Model model) {
// 从安全上下文中获取登录信息,返回给model
Map<String, Object> map = new HashMap<>(5);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
map.put("当前用户", username);
map.put("原来权限", auth.getAuthorities());
// 使用Set,不使用List;List可以存重复元素;登录后,在首页刷新,List会重复添加
//List<GrantedAuthority> authorities = new ArrayList<>(auth.getAuthorities());
Set<GrantedAuthority> authorities = new HashSet<>(auth.getAuthorities());
// 根据三方用户查绑定的本地用户
String localUser = getLocalUser(username);
UserDetails userDetails = userDetailsService.loadUserByUsername(localUser);
map.put("本地用户", localUser);
// 本地用户权限
//List<GrantedAuthority> authorities1 = new ArrayList<>(userDetails.getAuthorities());
Set<GrantedAuthority> authorities1 = new HashSet<>(userDetails.getAuthorities());
map.put("本地用户权限", authorities1);
// 把本地用户权限加入原来权限集中
authorities.addAll(authorities1);
map.put("新的权限", authorities);
// 生成新的认证信息
Authentication newAuth = new OAuth2AuthenticationToken((OAuth2User) auth.getPrincipal(),authorities,"myClient");
// 重置认证信息
SecurityContextHolder.getContext().setAuthentication(newAuth);
model.addAttribute("user", map);
return "index";
}
/**
* 模拟通过第三方用户,得到本地用户
* @param remoteUsername
* @return
*/
private String getLocalUser(String remoteUsername){
String u = "";
// 模拟通过三方用户查本地用户
if(StringUtils.isNotEmpty(remoteUsername)){
u = "local_admin";
}
return u;
}
}
@Configuration
public class SecurityConfiguration {
/**
* 虚拟一个本地用户
*
* @return UserDetailsService
*/
@Bean
UserDetailsService userDetailsService() {
return username -> User.withUsername("local_admin")
.password("123456")
.roles("TEST","ABC")
//.authorities("ROLE_ADMIN", "ROLE_USER")
.build();
}
}
- 访问测试
6、权限设计
- 客户端
客户端分类 | 被授于的 SCOPE |
---|---|
电脑端 | SCOPE_1 |
手机端 | SCOPE_2 |
内部资源服务 | SCOPE_0 |
- 资源端
资源分类 | 允许访问的 SCOPE | 说明 |
---|---|---|
r1/res1 | SCOPE_0、SCOPE_1、SCOPE_2 | 资源服务器1 中的 资源1,可以被三个客户端访问 |
r1/res2 | SCOPE_0、SCOPE_1 | 资源服务器1 中的 资源2,只可以被电脑端、内部资源访问 |
r2/res1 | SCOPE_0、SCOPE_2 | 资源服务器2 中的 资源1,只可以被手机端、内部资源访问 |
r2/res2 | SCOPE_1、SCOPE_2 | 资源服务器2 中的 资源2,只可以被电脑端、手机端访问 |
r3/res1 | SCOPE_2 | 资源服务器3 中的 资源1,只可以被手机端访问 |
r3/res2 | SCOPE_0 | 资源服务器3 中的 资源2,只可以被内部资源访问 |
- 用户与角色
用户 | 角色 |
---|---|
张三 | ROLE_1 |
李四 | ROLE_2 |
- 角色与权限
权限 | 角色 |
---|---|
AUTH_1 | ROLE_1 |
AUTH_2 | ROLE_2 |
AUTH_3 | ROLE_1、ROLE_2 |
AUTH_4 | ROLE_2 |
ROLE_1:包含 AUTH_1、AUTH_3
ROLE_2:包含 AUTH_2、AUTH_3、AUTH_4
-
客户端与资源的访问绑定关系是一一对应的,应该相应稳定。客户端能访问某个资源就提供一个接口。不能随时修改。
-
用户通过角色访问客户端中的服务API,这个关系比较灵活, 相对松散。客户端中只需指定某个接口可以被哪些AUTH访问即可。
-
角色ROLE与权限AUTH的关系相对稳定,但可以比客户端和资源的关系灵活,可以修改编辑。
-
在项目设计阶段,应该首先确定客户端的种类,再基本确认项目中所涉及的角色。根据资源API功能,决定需要哪些权限,应该把权限赋于哪种角色。