在认证之后另一个重要的部分就是鉴权,所谓鉴权就是鉴定访问某一方法的用户是否有相应的权限。Spring Security有四种权限控制方法,在Aurora博客中使用的是灵活的动态鉴权,是将权限数据存放在数据库中,而不是以硬编码的形式写入到程序中。
Spring Security 支持在 URL 和方法权限控制时使用 SpEL 表达式,如果表达式返回值为 true 则表示需要对应的权限,否则表示不需要对应的权限。SecurityExpressionRoot 类中定义的最基本的 SpEL 有以下部分:
在Security的配置类中配置URL路径权限:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasAnyRole("admin", "user")
.anyRequest().authenticated()
.and()
...
}
通过在访问的方法上添加表达式注解来控制访问权限。在方法上添加注解控制权限,需要我们首先开启注解的使用,在 Spring Security 配置类上添加如下内容:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
...
}
这个配置开启了三个注解,分别是:
@Service
public class HelloService {
@PreAuthorize("principal.username.equals('javaboy')")
// 只有当前登录用户名为 javaboy 的用户才可以访问该方法。
public String hello() {
return "hello";
}
@PreAuthorize("hasRole('admin')")
// 表示访问该方法的用户必须具备 admin 角色。
public String admin() {
return "admin";
}
@Secured({"ROLE_user"})
// 该方法的用户必须具备 user 角色,但是注意 user 角色需要加上 ROLE_ 前缀。
public String user() {
return "user";
}
@PreAuthorize("#age>98")
// 访问该方法的 age 参数必须大于 98,否则请求不予通过。
public String getAge(Integer age) {
return String.valueOf(age);
}
}
Spring Security 中还有两个过滤函数 @PreFilter 和 @PostFilter,可以根据给出的条件,自动移除集合中的元素。
动态权限主要通过重写拦截器和决策器来实现,拦截请求,得到请求的url路径,从数据库中查找该url路径需要的角色,将角色放入列表之后传递给决策器。决策器根据用户拥有的角色和需要的角色相对比来决定是否放行。
要实现动态鉴权需要五张表,用户表user,角色表role,资源表resource,用户角色表user_role,角色资源表role_resource。当然在实际项目中大多是先编写好各种基本功能和界面,在前端中初始化分配用户角色和角色权限的,然后再整合上spring security模块。
动态权限的实现是依靠自己重写的FilterInvocationSecurityMetadataSource 获取资源权限和AccessDecisionManager决策器来实现的。当实现了这两个类之后需要自己配置到Security中:
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
// fsi FilterInvocation 能够得到访问请求的url和访问方式
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
// 自定义的权限查找器 securityMetadataSource 的作用是找到要访问的资源所需要的权限,如果数据库中没有设置这个url,则返回null,任何用户都可以访问
fsi.setSecurityMetadataSource(securityMetadataSource());
// 自定义的决策管理器,用于决策是否放行(允许访问资源),在实现中会authentication.getAuthorities()得到用户所有的权限,然后如果
// 其中包含所访问资源的其中一个权限就能够放行
fsi.setAccessDecisionManager(accessDecisionManager());
return fsi;
}
})
SecurityMetadataSource 接口负责提供受保护对象所需要的权限。需要继承并实现 FilterInvocationSecurityMetadataSource 接口,然后重写它里边的三个方法:
getAttributes:该方法的参数是受保护对象,在基于 URL 地址的权限控制中,受保护对象就是 FilterInvocation;该方法的返回值则是访问受保护对象所需要的权限。具体的代码和注释如下:
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 首先获取所有的资源所需要的角色列表
if (CollectionUtils.isEmpty(resourceRoleList)) {
this.loadResourceRoleList();
}
FilterInvocation fi = (FilterInvocation) object;
// 获取请求的方式
String method = fi.getRequest().getMethod();
// 获取请求的路径
String url = fi.getRequest().getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 遍历所有的资源
for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) {
// 获取和当前请求路径和请求方法相匹配资源
if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) && resourceRoleDTO.getRequestMethod().equals(method)) {
// 获取该资源所需要的角色
List<String> roleList = resourceRoleDTO.getRoleList();
// 如果角色为空,那么直接禁止访问
if (CollectionUtils.isEmpty(roleList)) {
return SecurityConfig.createList("disable");
}
// 否则返回选哟的角色列表
return SecurityConfig.createList(roleList.toArray(new String[]{}));
}
}
// 如果没找到对应的资源,返回null,默认返回null可以访问
return null;
}
继承实现AccessDecisionManager实现自己的决策方案类,需要实现decide决策函数,用以判断当前用户的角色是否包含访问资源需要的任意角色,否则抛出权限不足的异常。
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
// 获取当前用户所具有的角色列表
List<String> permissionList = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 遍历访问资源所需要的角色列表
for (ConfigAttribute item : collection) {
// 如果包含任意一个角色那么就准许访问
if (permissionList.contains(item.getAttribute())) {
return;
}
}
// 否则抛出异常
throw new AccessDeniedException("权限不足");
}