前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈spring security中的权限控制

浅谈spring security中的权限控制

作者头像
算法之名
发布2019-08-20 10:57:40
1.2K0
发布2019-08-20 10:57:40
举报
文章被收录于专栏:算法之名算法之名

当我们在OAuth登陆后,获取了登陆的令牌,使用该令牌,我们就有了访问一些受OAuth保护的接口的能力。具体可以看本人的这两篇博客OAuth2.0用户名,密码登录解析 OAuth2.0通过token获取受保护资源的解析

但现在我们要区分这些登陆人员的具体分工,哪些接口归哪些登陆人员可以访问,这就要用到了spring security中的权限控制。

首先我们需要有一个权限的对象

代码语言:javascript
复制
/**
 * 权限标识
 */
@Data
public class SysPermission implements Serializable {

   private static final long serialVersionUID = 280565233032255804L;

   private Long id; //权限id
   private String permission; //具体的权限
   private String name; //权限名称
   private Date createTime;
   private Date updateTime;

}

对应于数据库中的权限表

那么问题来了,我们要对权限进行管理需要什么样的权限呢,当然我们需要权限管理权限,这是在系统一开始建立的时候保存进数据库的

这四个权限并不是通过前端写入的。

现在我们需要通过前端接口增加其他的权限就需要使用到这四个权限之一。

在这里我们给出一些权限的增删改查的mybatis dao

代码语言:javascript
复制
@Mapper
public interface SysPermissionDao {

   @Options(useGeneratedKeys = true, keyProperty = "id")
   @Insert("insert into sys_permission(permission, name, createTime, updateTime) values(#{permission}, #{name}, #{createTime}, #{createTime})")
   int save(SysPermission sysPermission);

   @Update("update sys_permission t set t.name = #{name}, t.permission = #{permission}, t.updateTime = #{updateTime} where t.id = #{id}")
   int update(SysPermission sysPermission);

   @Delete("delete from sys_permission where id = #{id}")
   int delete(Long id);

   @Select("select * from sys_permission t where t.id = #{id}")
   SysPermission findById(Long id);

   @Select("select * from sys_permission t where t.permission = #{permission}")
   SysPermission findByPermission(String permission);

   int count(Map<String, Object> params);

   List<SysPermission> findData(Map<String, Object> params);

}

现在我们要在Controller中增加一个新的权限

代码语言:javascript
复制
/**
 * 管理后台添加权限
 * 
 * @param sysPermission
 * @return
 */
@LogAnnotation(module = LogModule.ADD_PERMISSION)
@PreAuthorize("hasAuthority('back:permission:save')")
@PostMapping("/permissions")
public SysPermission save(@RequestBody SysPermission sysPermission) {
   if (StringUtils.isBlank(sysPermission.getPermission())) {
      throw new IllegalArgumentException("权限标识不能为空");
   }
   if (StringUtils.isBlank(sysPermission.getName())) {
      throw new IllegalArgumentException("权限名不能为空");
   }

   sysPermissionService.save(sysPermission);

   return sysPermission;
}

我们可以看到这个标签@PreAuthorize("hasAuthority('back:permission:save')"),首先我们是通过access_token令牌访问的该接口,系统可以知道登陆的是哪一个用户,以此看看该用户是否有back:permission:save的访问权限

我们来看看用户角色

代码语言:javascript
复制
@Data
public class SysRole implements Serializable {

   private static final long serialVersionUID = -2054359538140713354L;

   private Long id; //角色id
   private String code; //角色编码
   private String name; //角色名称
   private Date createTime;
   private Date updateTime;
}

对应数据库中的表结构如下

并给定一个管理员角色

该角色对应于哪些权限,这里可以看到是所有权限

而我们的用户是哪个角色呢

我们可以看到这里有两个用户,他们都属于管理员角色

如果我们现在用其中的一个用户登陆,并获取该用户的信息如下

{

"code" : 200 ,

"data" : {

"access_token" : "aaf4cd90-497e-4c33-adde-b580ab0f0c65" ,

"user" : {

"accountNonExpired" : true ,

"accountNonLocked" : true ,

"authorities" : [

{

"authority" : "back:menu:set2role"

},

{

"authority" : "mail:update"

},

{

"authority" : "back:permission:delete"

},

{

"authority" : "role:permission:byroleid"

},

{

"authority" : "back:menu:save"

},

{

"authority" : "back:menu:query"

},

{

"authority" : "ip:black:query"

},

{

"authority" : "ip:black:save"

},

{

"authority" : "file:del"

},

{

"authority" : "ip:black:delete"

},

{

"authority" : "mail:query"

},

{

"authority" : "back:user:query"

},

{

"authority" : "back:role:permission:set"

},

{

"authority" : "sms:query"

},

{

"authority" : "back:role:query"

},

{

"authority" : "back:permission:query"

},

{

"authority" : "back:user:role:set"

},

{

"authority" : "back:role:save"

},

{

"authority" : "log:query"

},

{

"authority" : "file:query"

},

{

"authority" : "back:menu:update"

},

{

"authority" : "back:role:update"

},

{

"authority" : "back:role:delete"

},

{

"authority" : "back:user:password"

},

{

"authority" : "ROLE_SUPER_ADMIN"

},

{

"authority" : "back:menu:delete"

},

{

"authority" : "back:user:update"

},

{

"authority" : "menu:byroleid"

},

{

"authority" : "mail:save"

},

{

"authority" : "user:role:byuid"

},

{

"authority" : "back:permission:save"

},

{

"authority" : "back:permission:update"

}

],

"createTime" : "2018-01-17T16:56:59.000+0800" ,

"credentialsNonExpired" : true ,

"enabled" : true ,

"headImgUrl" : "" ,

"id" : 1 ,

"nickname" : "测试1" ,

"password" : "$2a$10$QpeXBJpWYetNwfWEHnkvLeK0jS0P9R6V8QqCj37zeNGroqYvdvW.C" ,

"permissions" : [

"back:menu:set2role" ,

"mail:update" ,

"back:permission:delete" ,

"role:permission:byroleid" ,

"back:menu:save" ,

"back:menu:query" ,

"ip:black:query" ,

"ip:black:save" ,

"file:del" ,

"ip:black:delete" ,

"mail:query" ,

"back:user:query" ,

"back:role:permission:set" ,

"sms:query" ,

"back:role:query" ,

"back:permission:query" ,

"back:user:role:set" ,

"back:role:save" ,

"log:query" ,

"file:query" ,

"back:menu:update" ,

"back:role:update" ,

"back:role:delete" ,

"back:user:password" ,

"back:menu:delete" ,

"back:user:update" ,

"menu:byroleid" ,

"mail:save" ,

"user:role:byuid" ,

"back:permission:save" ,

"back:permission:update"

],

"phone" : "" ,

"sex" : 1 ,

"sysRoles" : [

{

"code" : "SUPER_ADMIN" ,

"createTime" : "2018-01-19T20:32:16.000+0800" ,

"id" : 1 ,

"name" : "超级管理员" ,

"updateTime" : "2018-01-19T20:32:18.000+0800"

}

],

"type" : "APP" ,

"updateTime" : "2018-01-17T16:57:01.000+0800" ,

"username" : "admin"

}

},

"msg" : "操作成功"

}

通过返回的信息我们可以看到他的权限,跟 @PreAuthorize( "hasAuthority('back:permission:save')")比对,我们知道登陆用户拥有该权限。可以访问该接口。

上面都是前菜,下面进入正题。

代码语言:txt
复制
我们来看一下    @PreAuthorize 标签的源码,它位于org.springframework.security.access.prepost包下   
代码语言:javascript
复制
/**
 * 用于指定将计算为的方法访问控制表达式的批注
 * 决定是否允许方法调用。
 *
 * @author Luke Taylor
 * @since 3.0
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
   /**
    * @return 在执行这个受保护的方法前进行Spring EL表达式的解析
    */
   String value();
}

这里有一个Spring EL表达式都解析,我们来看一下什么是Spring EL表达式

代码语言:javascript
复制
public class SpringELTest {
    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        //解析字符串,该字符串具有一段代码的味道
        Expression expression = parser.parseExpression("'Hello World'.bytes.length");
        int length = (int)expression.getValue();
        System.out.println(length);
    }
}

这个"'Hello World'.bytes.length"就是一段Spring EL表达式,虽然是一段字符串,但它有一段代码的含义,可以被解析执行

运行结果

11

那么很明显"hasAuthority('back:permission:save')"就是一段Spring EL表达式,它是可以被执行的。

要想使标签@PreAuthorize生效,我们需要设置一下OAuth的资源服务设置

代码语言:javascript
复制
/**
 * 资源服务配置
 */
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

   @Override
   public void configure(HttpSecurity http) throws Exception {
      http.csrf().disable().exceptionHandling()
            .authenticationEntryPoint(
                  (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and().authorizeRequests().antMatchers(PermitAllUrl.permitAllUrl("/users-anon/**", "/sys/login","/wechat/**")).permitAll()
            .anyRequest().authenticated().and().httpBasic();
   }
   @Override
   public void configure(ResourceServerSecurityConfigurer resource) throws Exception {
      //这里把自定义异常加进去
      resource.authenticationEntryPoint(new AuthExceptionEntryPoint())
            .accessDeniedHandler(new CustomAccessDeniedHandler());
   }


   @Bean
   public BCryptPasswordEncoder bCryptPasswordEncoder() {
      return new BCryptPasswordEncoder();
   }

}

其中@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)就是打开@PreAuthorize进行拦截生效的标签,当然一定要设置prePostEnabled = true。

既然"hasAuthority('back:permission:save')"是一段Spring EL表达式,那么hasAuthority()就一定是一个可以执行的方法,该方法位于SecurityExpressionRoot类下,该类位于org.springframework.security.access.expression包中。

Spring Security可用表达式对象的基类就是SecurityExpressionRoot,它支持很多的方法

表达式

描述

hasRole(role)

当前用户是否拥有指定角色。

hasAnyRole(role1,role2)

多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。

hasAuthority(auth)

等同于hasRole

hasAnyAuthority(auth1,auth2)

等同于hasAnyRole

Principle

代表当前用户的principle对象

authentication

直接从SecurityContext获取的当前Authentication对象

permitAll

总是返回true,表示允许所有的

denyAll

总是返回false,表示拒绝所有的

isAnonymous()

当前用户是否是一个匿名用户

isRememberMe()

表示当前用户是否是通过Remember-Me自动登录的

isAuthenticated()

表示当前用户是否已经登录认证成功了。

isFullyAuthenticated()

如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。

我们具体看一下hasAuthority这个方法的实现,只有当这个方法返回的结果为true的时候,我们才能进一步访问我们的接口代码

这里面传入的authority为"back:permission:save"

代码语言:javascript
复制
public final boolean hasAuthority(String authority) {
   return hasAnyAuthority(authority);
}
代码语言:javascript
复制
public final boolean hasAnyAuthority(String... authorities) {
   return hasAnyAuthorityName(null, authorities);
}
代码语言:javascript
复制
private boolean hasAnyAuthorityName(String prefix, String... roles) {
   //获取所有的用户角色权限
   Set<String> roleSet = getAuthoritySet();
   //由于我们这里传入的roles只有"back:permission:save"
   //所以role即为"back:permission:save",prefix则为null
   for (String role : roles) {
      //defaultedRole依然为"back:permission:save"
      String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
      //在权限集合中是否包含"back:permission:save"的该权限
      //根据我们之前登录的返回信息,可以看到"authority": "back:permission:save"的存在
      //所以此处是可以通过权限验证的。
      if (roleSet.contains(defaultedRole)) {
         return true;
      }
   }

   return false;
}
代码语言:javascript
复制
private Set<String> getAuthoritySet() {
   //Set<String> roles是SecurityExpressionRoot的属性
   //我们可以看到它是从一系列用户认证里面获取到的权限集合
   if (roles == null) {
      roles = new HashSet<>();
      //authentication是SecurityExpressionRoot极为重要的一个属性,它本身是一个接口
      //管理着用户认证信息的各个方法
      Collection<? extends GrantedAuthority> userAuthorities = authentication
            .getAuthorities();

      if (roleHierarchy != null) {
         userAuthorities = roleHierarchy
               .getReachableGrantedAuthorities(userAuthorities);
      }
      
      roles = AuthorityUtils.authorityListToSet(userAuthorities);
   }

   return roles;
}
代码语言:javascript
复制
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
   if (role == null) {
      return role;
   }
   //由于defaultRolePrefix为null,所以此处返回的就是"back:permission:save"
   if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
      return role;
   }
   if (role.startsWith(defaultRolePrefix)) {
      return role;
   }
   return defaultRolePrefix + role;
}

我们可以看一下AuthorityUtils.authorityListToSet()方法

代码语言:javascript
复制
public static Set<String> authorityListToSet(
      Collection<? extends GrantedAuthority> userAuthorities) {
   Set<String> set = new HashSet<>(userAuthorities.size());
   //很明显这里是把认证用户的所有权限给转化为Set集合
   for (GrantedAuthority authority : userAuthorities) {
      set.add(authority.getAuthority());
   }

   return set;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档