Spring Security实战(二)-- 方法权限认证

 

前言

 

上次Spring Security实战()-- 基于数据库认证》讲到使用Spring Security如何实现对web层的安全认证,Spring Security还可以实现对方法级别的权限控制。对方法的权限保护主要有两个应用场景:

1、由于开发疏忽导致web层的安全认证有漏洞,从而绕过了web层的安全认证。对重要的方法再进行一次权限控制,可以避免此类安全问题。

2、假设AB两个用户都是普通用户,都有修改和删除自己数据的权限,但不能修改和删除对方的数据,web层的安全认证只是正对链接规则 很难做到如此细粒度的权限控制,但方法级别的权限认证可以实现该功能。

 

本次总结,首先对Spring Security如何实现方法级别的权限控制进行讲解;然后以一个用户管理页面权限控制为例,讲解如何使用页面权限结合方法验证进行细粒度的权限控制。

 

Spring Security方法权权限认证

 

已经在web层引入了Spring Security的情况下,要在方法上使用权限控制很简单,在上一篇demo xml配置文件中,添加相应配置启动“注解保护方法”即可,Spring Security支持三种不同的安全注解:

1Spring Security自带的@Secured

2JSR-250@RolesAllowed

34个表达式驱动注解:@PreAuthorize@PostAuthorize@PreFilter@PostFilter

 

@Secured@RolesAllowed

 

其中@Secured@RolesAllowed注解的作用相同,主要实现根据用户的角色权限判断是否有权限方法该方法,只是一个是SpringSecurity自己的规范,一个是java标准规范。

@Secured的使用

由于@Secured@RolesAllowed注解的作用相同,这里以启用@Secured注解进行讲解,要启用@Secured注解可以在xml配置文件中添加如下配置:

<!--<security:global-method-security jsr250-annotations="enabled"/>-->
<security:global-method-security secured-annotations="enabled"/>

然后在需要做权限控制的方法上添加@Secured注解即可:

    

@Secured({"ROLE_USER","ROLE_ADMIN"})
    public void userPermit() {
        System.out.println("允许普通角色和管理员角色访问");
    }
    @Secured("ROLE_ADMIN")
    public void adminPermit() {
        System.out.println("允许管理员角色访问");
    }

 

@Secured("ROLE_ADMIN")等价于表达式@Secured("hasRole('ROLE_USER')")Spring Security还提供了类似的其他内置表达式:



 

这张来自官网的表很关键,通过上述表达式可以覆盖我们遇到的大部分权限认证场景。其中hasPermission()表达式为扩展表达,用户可以自己实现PermissionEvaluator接口很方便的完成自定义扩展。

 

4个表达式驱动注解

 

@PreAuthorize@PostAuthorize@PreFilter@PostFilter 4个驱动表达式注解使用SpEl表达式,可以是实现更细粒度的表达式约束。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。要使它们的定义能够对我们的方法的调用产生影响我们需要设置global-method-security元素的pre-post-annotations=enabled”,默认为disabled

<!—开启@Secured注解,同时开启4个表达式驱动注解-->
<security:global-method-security secured-annotations="enabled" pre-post-annotations="enabled"/>
 

 

在需要做权限认证的方法上添加注解:

/**
     * 普通管理员 只能获取自己的店铺列表
     * @param username
     * @return
     */
    @Override
    @PreAuthorize("principal.username.equals(#username)")
    public List<ShopData> select(String username) {
        List<ShopData> ret = new ArrayList<>();
        for(ShopData temp:shops){
            if(temp.getUsername().equals(username)){
                ret.add(temp);
            }
        }
        return ret;
    }
 

 

上述注解,可以隔离不同普通用户之间的数据 彼此不可见。

 

再来看一个稍微复杂点的例子:

@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
    @PreFilter("hasRole('ROLE_ADMIN') || filterObject.shopdata.username == principal.username")
    public void deletexx(List<ShopData> list) {
        //省略代码
    }

 

@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")表示在方法调用前进行权限认证,具备'ROLE_USER','ROLE_ADMIN'角色的用户可以访问该方法。

 

@PreFilter("hasRole('ROLE_ADMIN') || filterObject.shopdata.username == principal.username")含义为:如果是超级管理员可以删除list中的所有数据,如果是普通管理员,需要对list进行过滤 只能删除自己的数据。

 

hasPermission表达式自定义权限

 

有时候通过上述常规表示式无法完成一些复杂的权限认证,比如:根据id删除数据,判断如果是超级管理员可以直接删除,如果是普通管理原型 需要根据id先查询数据,再判断数据创建者是否是当前用户。这时只能通过hasPermission表达式实现,要使用hasPermission表达式必须实现自己的PermissionEvaluator接口:

@Component("myPermissionEvaluator")
public class MyPermissionEvaluator implements PermissionEvaluator {
 
    private static final GrantedAuthority ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN");
 
    @Resource
    private ShopDataHelper shopDataHelper;
 
    @Override
    public boolean hasPermission(Authentication authentication, Object o, Object o1) {
        if(o instanceof Integer){
            //管理员拥有所有权限
            if(isAdmin(authentication)){
                return true;
            }
            Integer id = (Integer) o;
            ShopData shopData = shopDataHelper.selectById(id);
            String username = shopData.getUsername();
 
            //判断普通管理员是否有删除、修改权限
            if("delete".equals(o1) || "update".equals(o1)){
                if(authentication.getName().equals(username)){
                    return true;
                }else{
                    return false;
                }
            }
        }
 
        throw new UnsupportedOperationException("hasPermission 第一个参数必须是Integer型,第二个参数必须为delete或者update");
    }
 
    /**
     * 暂不实现
     */
    @Override
    public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
        throw new UnsupportedOperationException();
    }
 
    private boolean isAdmin(Authentication authentication){
        return authentication.getAuthorities().contains(ADMIN);
    }
}

 

 

该接口有两个主要方法:

boolean hasPermission(Authentication authentication, Object targetDomainObject,
                                                                 Object permission);
 
boolean hasPermission(Authentication authentication, Serializable targetId,
                                                                 String targetType, Object permission);

这里MyPermissionEvaluator只实现了第一个方法,在使用hasPermission表达式时,不用传Authentication参数,默认会自动带上:

   @PreAuthorize("hasPermission(#id,'update')")
    public void update(Integer id) {
        for(ShopData temp:shops){
            if(temp.getId()==id){
                if(temp.isState()){
                    temp.setState(false);
                }else{
                    temp.setState(true);
                }
                break;
            }
        }
  }

 

@PreAuthorize("hasPermission(#id,'update')")权限控制为:如果是超级管理员可以直接删除数据,如果是普通管理员首先通过id查询得到该id对应的店铺信息,然后判断该店铺的创建者是否是当前用户,如果是才允许删除操作。这部分逻辑被封装到MyPermissionEvaluator中。

 

有人会说,不使用hasPermission表达式直接在update方法中做这部分校验操作也可以,实际工作中有很多系统也是这样做的。但是采用hasPermission表达式可以把业务逻辑代码与权限控制代码完全隔离,而且复用性更好,其他有类似权限控制的地方直接使用这个hasPermission表达式即可。

 

Demo演示

 

这里笔者根据上述讲解写了一份demo演示代码,github地址为:https://github.com/gantianxing/spring-security1.git。在运行代码之前,首先执行下列sql语句:

-- ----------------------------
-- Table structure for `authorities`
-- ----------------------------
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
  `username` varchar(50) COLLATE utf8_bin NOT NULL,
  `authority` varchar(50) COLLATE utf8_bin NOT NULL,
  UNIQUE KEY `ix_auth_username` (`username`,`authority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
 
-- ----------------------------
-- Records of authorities
-- ----------------------------
INSERT INTO `authorities` VALUES ('A', 'ROLE_ADMIN');
INSERT INTO `authorities` VALUES ('A', 'ROLE_USER');
INSERT INTO `authorities` VALUES ('B', 'ROLE_USER');
INSERT INTO `authorities` VALUES ('C', 'ROLE_USER');
 
-- ----------------------------
-- Table structure for `users`
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `username` varchar(50) COLLATE utf8_bin NOT NULL,
  `password` varchar(50) COLLATE utf8_bin NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
 
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('A', '123456', '1');
INSERT INTO `users` VALUES ('B', '123456', '1');
INSERT INTO `users` VALUES ('C', '123456', '1');
 

 

上述sql语句创建了三个用户ABC,其中A用户是超级管理员具备:ROLE_USERROLE_ADMIN角色;BC用户都是普通管理员,只具备ROLE_USER角色。

 

程序启动后,会在内存里初始化3个店铺信息数据(ShopData),创建者分别为ABC。访问登陆页http://localhost/login,首先使用A账号登陆:



 

再访问店铺列表页,由于A账号是超级管理员,可以看到所有的三条数据:




如果切换成B用户登陆,再次访问店铺列表页,只能看到B用户创建的店铺信息:



 

 

访问http://localhost/shop/add,可以在当前用户下创建一个新店铺(再次访问店铺列表页):



 

 

访问http://localhost/shop/update?id=2,可以修改店铺上下线状态,再次访问店铺列表页:



 

 

如果访问http://localhost/shop/update?id=1,由于当前登录用户是B,但id=1的店铺是A用户创建的,所以没有权限,执行结果为:



 

 

同理还可以执行http://localhost/shop/delete?id=2,对B用户自己的数据进行删除,但缺无法删除别人的数据,如果是A用户登陆可以修改和删除所有数据。这部分测试内容,不再演示,读者可以自行测试。

 

转载请注明处在:

 

http://moon-walker.iteye.com/blog/2395306

猜你喜欢

转载自moon-walker.iteye.com/blog/2395306