我们开发一个系统,必然面临权限控制的问题,即不同的用户具有不同的访问、操作、数据权限。形成理论的权限控制模型有:自主访问控制(DAC: Discretionary Access Control)、强制访问控制(MAC: Mandatory Access Control)、基于属性的权限验证(ABAC: Attribute-Based Access Control)等。最常被开发者使用也是相对易用、通用的就是RBAC权限模型(Role-Based Access Control),本文就将向大家介绍该权限模型。
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。模型中有几个关键的术语:
RBAC权限模型核心授权逻辑如下:
想到权限控制,人们最先想到的一定是用户与权限直接关联的模式,简单地说就是:某个用户具有某些权限。如图:
这种模型能够清晰的表达用户与权限之间的关系,足够简单。但同时也存在问题:
在实际的团体业务中,都可以将用户分类。比如对于薪水管理系统,通常按照级别分类:经理、高级工程师、中级工程师、初级工程师。也就是按照一定的角色分类,通常具有同一角色的用户具有相同的权限。这样改变之后,就可以将针对用户赋权转换为针对角色赋权。因为角色少、权限多,所以基于角色管理权限,减少用户在授权与权限回收过程中的过多操作。
我们可以用下图中的数据库设计模型,描述这样的关系。
但是在实际的应用系统中,一个用户一个角色远远满足不了需求。如果我们希望一个用户既担任销售角色、又暂时担任副总角色。该怎么做呢?为了增加系统设计的适用性,我们通常设计:
我们可以用下图中的数据库设计模型,描述这样的关系。
为了适应这种需求,我们可以把页面资源(菜单)和操作资源(按钮)分表存放,如上图。也可以把二者放到一个表里面存放,用一个字段进行标志区分。
数据权限比较好理解,就是某个用户能够访问和操作哪些数据。
所以为了面对复杂的需求,数据权限的控制通常是由程序员书写个性化的SQL来限制数据范围的,而不是交给权限模型或者Spring Security或shiro来控制。当然也可以从权限模型或者权限框架的角度去解决这个问题,但适用性有限。
我们所有的用户、角色、权限信息都是在配置文件里面写死的,然而在实际的业务系统中,这些信息通常是存放在RBAC权限模型的数据库表中的。
下面来把这些信息从数据库里面进行加载。
下面我们来回顾一下其中的核心概念:
UserDetails接口表达你是谁?你有什么角色权限。UserDetailsService接口表达的是如何动态加载UserDetails数据。
下面我们来看一下UserDetails接口都有哪些方法。
public interface UserDetails extends Serializable {
//获取用户的权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//账号是否没过期
boolean isAccountNonExpired();
//账号是否没被锁定
boolean isAccountNonLocked();
//密码是否没过期
boolean isCredentialsNonExpired();
//账户是否可用
boolean isEnabled();
}
现在我们明白了,只要我们把这些信息提供给Spring Security,Spring Security就知道怎么做登录验证了,根本不需要我们自己写Controller实现登录验证逻辑。那我们怎么把这些信息提供给Spring Security,用的就是下面的接口方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public class MyUserDetails implements UserDetails {
String password; //密码
String username; //用户名
boolean accountNonExpired; //是否没过期
boolean accountNonLocked; //是否没被锁定
boolean credentialsNonExpired; //密码是否没过期
boolean enabled; //账号是否可用
Collection<? extends GrantedAuthority> authorities; //用户的权限集合
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true; //暂时未用到,直接返回true,表示账户未过期
}
@Override
public boolean isAccountNonLocked() {
return true; //暂时未用到,直接返回true,表示账户未被锁定
}
@Override
public boolean isCredentialsNonExpired() {
return true; //暂时未用到,直接返回true,表示账户密码未过期
}
@Override
public boolean isEnabled() {
return enabled;
}
}
我们就是写了一个适应于UserDetails的java POJO类,所谓的 UserDetails接口实现就是一些get方法。
目前数据库表里面没有定义accountNonExpired、accountNonLocked、credentialsNonExpired这三个字段,目前暂时也用不到,因此这三个属性对应的get方法,返回值默认为true
@Component
public class MyUserDetailsService implements UserDetailsService {
@Resource
private MyUserDetailsServiceMapper myUserDetailsServiceMapper;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//获得用户信息
MyUserDetails myUserDetails =
myUserDetailsServiceMapper.findByUserName(username);
if(myUserDetails == null){
throw new UsernameNotFoundException("用户名不存在");
}
//获得用户角色列表
List<String> roleCodes =
myUserDetailsServiceMapper.findRoleByUserName(username);
//通过角色列表获取权限列表
List<String> authorities =
myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
//为角色标识加上ROLE_前缀(Spring Security规范)
roleCodes = roleCodes.stream()
.map(rc -> "ROLE_" + rc )
.collect(Collectors.toList());
//角色是一种特殊的权限,所以合并
authorities.addAll(roleCodes);
//转成用逗号分隔的字符串,为用户设置权限标识
myUserDetails.setAuthorities(
AuthorityUtils.commaSeparatedStringToAuthorityList(
String.join(",",authorities)
)
);
return myUserDetails;
}
}
重写WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Resource
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
使用BCryptPasswordEncoder,表示存储中(数据库)取出的密码必须是经过BCrypt加密算法加密的。
这里需要注意的是,因为我们使用了BCryptPasswordEncoder加密解密,所以数据库表里面存的密码应该是加密之后的密码(造数据的过程),可以使用如下代码加密(如密码是:123456)。将打印结果保存保存到密码字段。
@Resource
PasswordEncoder passwordEncoder;
@Test
public void contextLoads() {
System.out.println(passwordEncoder.encode("123456"));
}
至此,我们将系统里面的所有的用户、角色、权限信息都通过UserDetailsService和UserDetails告知了Spring Security。但是多数朋友可能仍然不知道该怎样实现登录的功能,其实剩下的事情很简单了:
然后把这些信息通过配置方式告知Spring Security ,以上的配置信息名称都可以灵活修改。
附录:Mybatis持久层数据接口
public interface MyUserDetailsServiceMapper {
//根据userID查询用户信息
@Select("SELECT username,password,enabled\n" +
"FROM sys_user u\n" +
"WHERE u.username = #{userId}")
MyUserDetails findByUserName(@Param("userId") String userId);
//根据userID查询用户角色
@Select("SELECT role_code\n" +
"FROM sys_role r\n" +
"LEFT JOIN sys_user_role ur ON r.id = ur.role_id\n" +
"LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
"WHERE u.username = #{userId}")
List<String> findRoleByUserName(@Param("userId") String userId);
//根据用户角色查询用户权限
@Select({
"<script>",
"SELECT url " ,
"FROM sys_menu m " ,
"LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id " ,
"LEFT JOIN sys_role r ON r.id = rm.role_id ",
"WHERE r.role_code IN ",
"<foreach collection='roleCodes' item='roleCode' open='(' separator=',' close=')'>",
"#{roleCode}",
"</foreach>",
"</script>"
})
List<String> findAuthorityByRoleCodes(@Param("roleCodes") List<String> roleCodes);
}
我们已经实现了从RBAC数据库中加载用户的角色、权限信息。在我们的Spring Security配置类SecurityConfig中仍然有一部分内容是通过代码静态配置的,那就是:资源鉴权规则。
简单说“资源鉴权规则”就是:你有哪些权限?这些权限能够访问哪些资源?即:权限与资源的匹配关系。
上图是资源鉴权规则完成之后的效果:
@Component("rbacService")
public class MyRBACService {
/** * 判断某用户是否具有该request资源的访问权限 */
public boolean hasPermission(HttpServletRequest request, Authentication authentication){
//获取认证主体
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetails){
//获取当前登录用户的UserDetails
UserDetails userDetails = ((UserDetails)principal);
//将当前请求的访问资源路径,如:"/syslog",包装成资源权限标识
SimpleGrantedAuthority simpleGrantedAuthority
= new SimpleGrantedAuthority(request.getRequestURI());
//判断用户已授权访问的资源中,是否包含“本次请求的资源”
return userDetails.getAuthorities().contains(simpleGrantedAuthority);
}
return false;
}
}
上述代码逻辑很简单:
如果使用admin用户登录,其加载数据内容如下图(根据之前章节调整RBAC模型数据库表里面的数据)。所以通过admin登录只能访问“用户管理”和“日志管理”功能。
如果使用admin用户登录,其加载数据内容如下图(根据之前章节调整RBAC模型数据库表里面的数据)。所以通过admin登录只能访问“具体业务一”和“具体业务二”功能。
从spring security 3.0开始已经可以使用spring Expression表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。Spring Security可用表达式对象的基类是SecurityExpressionRoot。
表达式函数 | 描述 |
---|---|
hasRole([role]) | 用户拥有指定的角色时返回true (Spring security默认会带有ROLE_前缀),去除前缀参考Remove the ROLE_ |
hasAnyRole([role1,role2]) | 用户拥有任意一个指定的角色时返回true |
hasAuthority([authority]) | 拥有某资源的访问权限时返回true |
hasAnyAuthority([auth1,auth2]) | 拥有某些资源其中部分资源的访问权限时返回true |
permitAll | 永远返回true |
denyAll | 永远返回false |
anonymous | 当前用户是anonymous时返回true |
rememberMe | 当前用户是rememberMe用户返回true |
authentication | 当前登录用户的authentication对象 |
fullAuthenticated | 当前用户既不是anonymous也不是rememberMe用户时返回true |
hasIpAddress(‘192.168.1.0/24’) | 请求发送的IP匹配时返回true |
部分朋友可能会对Authority和Role有些混淆。Authority作为资源访问权限可大可小,可以是某按钮的访问权限(如资源ID:biz1),也可以是某类用户角色的访问权限(如资源ID:ADMIN)。当Authority作为角色资源权限时,hasAuthority(‘ROLE_ADMIN’)与hasRole(‘ADMIN’)是一样的效果
我们可以通过继承WebSecurityConfigurerAdapter,实现相关的配置方法,进行全局的安全配置 。下面就为大家介绍一些如何在全局配置中使用SPEL表达式。
config.antMatchers("/person/*").access("hasRole('admin') or hasAuthority('ROLE_user')")
.anyRequest().authenticated();
这里我们定义了应用/person/*URL的范围,只有拥有ADMIN或者USER权限的用户才能访问这些person资源。
这种方式,比较适合有复杂权限验证逻辑的情况,当Spring Security提供的默认表达式方法无法满足我们的需求的时候。实际上在上面的动态加载资源鉴权规则里面,我么已经使用了这种方法。首先我们定义一个权限验证的RbacService。
@Component("rbacService")
@Slf4j
public class RbacService {
//返回true表示验证通过
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//验证逻辑代码
return true;
}
public boolean checkUserId(Authentication authentication, int id) {
//验证逻辑代码
return true;
}
}
对于"/person/{id}"对应的资源的访问,调用rbacService的bean的方法checkUserId进行权限验证,传递参数为authentication对象和person的id。该id为PathVariable,以#开头表示。
config.antMatchers("/person/{id}").access("@rbacService.checkUserId(authentication,#id)")
.anyRequest().access("@rbacService.hasPermission(request,authentication)");
如果我们想实现方法级别的安全配置,Spring Security提供了四种注解,分别是@PreAuthorize
, @PreFilter ,``@PostAuthorize
和 @PostFilter
在Spring安全配置代码中,加上EnableGlobalMethodSecurity
注解,开启方法级别安全配置功能。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@EnableGlobalMethodSecurity是一个组合注解,里面包含了@Configuration,所以不需要再加一个@Configuration注解了
@PreAuthorize
注解适合进入方法前的权限验证。只有拥有ADMIN
角色才能访问findAll方法。
@PreAuthorize("hasRole('admin')")
public List<PersonDemo> findAll(){
return null;
}
如果当前登录用户没有PreAuthorize需要的权限,将抛出org.springframework.security.access.AccessDeniedException异常!
@PostAuthorize 在方法执行后再进行权限验证,适合根据返回值结果进行权限验证。Spring EL 提供返回对象能够在表达式语言中获取返回的对象returnObject。下文代码只有返回值的name等于authentication对象的name(当前登录用户名)才能正确返回,否则抛出异常。
@PostAuthorize("returnObject.name == authentication.name")
public PersonDemo findOne(){
String authName =
SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println(authName);
return new PersonDemo("admin");
}
PreFilter 针对参数进行过滤,下文代码表示针对ids参数进行过滤,只有id为偶数的元素才被作为参数传入函数。
//当有多个对象是使用filterTarget进行标注
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
}
PostFilter 针对返回结果进行过滤,特别适用于集合类返回值,过滤集合中不符合表达式的对象。
@PostFilter("filterObject.name == authentication.name")
public List<PersonDemo> findAllPD(){
List<PersonDemo> list = new ArrayList<>();
list.add(new PersonDemo("kobe"));
list.add(new PersonDemo("admin"));
return list;
}
如果使用admin登录系统,上面的函数返回值list中kobe将被过滤掉,只剩下admin。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_53157173/article/details/121406522
内容来源于网络,如有侵权,请联系作者删除!