前言
上次《Spring Security实战(一)-- 基于数据库认证》讲到使用Spring Security如何实现对web层的安全认证,Spring Security还可以实现对方法级别的权限控制。对方法的权限保护主要有两个应用场景:
1、由于开发疏忽导致web层的安全认证有漏洞,从而绕过了web层的安全认证。对重要的方法再进行一次权限控制,可以避免此类安全问题。
2、假设A、B两个用户都是普通用户,都有修改和删除自己数据的权限,但不能修改和删除对方的数据,web层的安全认证只是正对链接规则 很难做到如此细粒度的权限控制,但方法级别的权限认证可以实现该功能。
本次总结,首先对Spring Security如何实现方法级别的权限控制进行讲解;然后以一个用户管理页面权限控制为例,讲解如何使用页面权限结合方法验证进行细粒度的权限控制。
Spring Security方法权权限认证
已经在web层引入了Spring Security的情况下,要在方法上使用权限控制很简单,在上一篇demo xml配置文件中,添加相应配置启动“注解保护方法”即可,Spring Security支持三种不同的安全注解:
1、Spring Security自带的@Secured
2、JSR-250的@RolesAllowed
3、4个表达式驱动注解:@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语句创建了三个用户A、B、C,其中A用户是超级管理员具备:ROLE_USER、ROLE_ADMIN角色;B、C用户都是普通管理员,只具备ROLE_USER角色。
程序启动后,会在内存里初始化3个店铺信息数据(ShopData),创建者分别为A、B、C。访问登陆页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用户登陆可以修改和删除所有数据。这部分测试内容,不再演示,读者可以自行测试。
转载请注明处在: