> 文章列表 > SpringSecurity学习(七)授权

SpringSecurity学习(七)授权

SpringSecurity学习(七)授权

授权

什么是权限管理
权限管理核心概念
SpringSecurity权限管理策略
基于URL地址的权限管理
基于方法的权限管理

一、权限管理

SpringSecurity学习(七)授权

二、授权核心概念

在认证的过程成功之后会将当前用户登录信息保存到Authentication对象中,Authentication对象中有一个getAuthorities()方法,用来返回当前登录用户具备的权限信息。该方法返回值是Collections<extends GrantedAuthority>,当需要进行权限判断时,就根据集合返回权限信息调用对应方法进行判断。

public interface Authentication extends Principal, Serializable {Collection<? extends GrantedAuthority> getAuthorities();// 省略
}

那么针对返回值应该如何理解?是权限还是角色
RBAC(Role/Resource Base Access Controll)
针对收取按可以是基于角色权限管理基于资源权限管理,从设计层面来说:角色和权限是俩个不同的东西。权限是一些具体的操作,角色是一些权限的集合。eg:READ_BOOK和ROLE_ADMIN是完全不同的。因此至于返回值是什么应当取决于业务的设计。

  • 基于角色权限设计:用户<=>角色<=>资源三者关系,返回就是用户的角色
  • 基于资源权限设计:用户<=>权限<=>资源三者关系,返回就是用户的权限
  • 基于角色和资源权限设计:用户<=>角色<=>权限<=>资源的关系,返回统称为用户的权限
    这里统称为权限,是因为代码层面来说权限和角色没有太大的不同都是权限。其在SpringSecurity中处理方式也基本相同。唯一的区别是会自动给角色多加一个ROLE_前缀。

三、两种权限管理策略

SpringSecurity主要提供俩种权限管理策略:
可以访问系统中的那些资源(URL、Method)

  1. 基于过滤器(URL)的权限管理(FilterSecurityInterceptor)
    基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来后,根据http请求地址进行权限校验。
  2. 基于AOP(Method)的权限管理(MethodSecurityInterceptor)
    基于AOP权限管理主要用来处理方法级别的权限问题。当需要调用某一方法时,通过aop将操作拦截,然后判断用户是否具备相关权限。

1、基于URL权限管理

1.1 案例

编写HiController
@RestController
public class HiController {@RequestMapping("/")public String home() {return "<h1>HI SPRING SECURITY</hi>";}@RequestMapping("/admin")public String admin() {return "<h1>HI SPRING ADMIN</hi>";}@RequestMapping("/user")public String user() {return "<h1>HI USER</hi>";}@RequestMapping("/getInfo")public Authentication getInfo() {return SecurityContextHolder.getContext().getAuthentication();}
}
编写SecurityConfig
@EnableWebSecurity
public class SecurityConfig {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());manager.createUser(User.withUsername("whx").password("{noop}123").roles("USER").build());manager.createUser(User.withUsername("dy").password("{noop}123").authorities("READ_INFO").build());return manager;}@Beanpublic SecurityFilterChain configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests(req -> {req.mvcMatchers("/admin").hasRole("ADMIN");req.mvcMatchers("/user").hasRole("USER");req.mvcMatchers("/getInfo").hasAuthority("READ_INFO");req.anyRequest().authenticated();});http.formLogin();http.csrf().disable();return http.build();}
}

1.2 权限表达式

public interface SecurityExpressionOperations {// 获取用户权限信息Authentication getAuthentication();// 当前用户是否具备指定权限boolean hasAuthority(String authority);// 当前用户是否具备指定权限中的任意一个boolean hasAnyAuthority(String... authorities);// 当前用户是否具备指定角色boolean hasRole(String role);// 当前用户是否具备指定角色任意一个boolean hasAnyRole(String... roles);// 放行所有请求boolean permitAll();// 拒绝所有请求boolean denyAll();// 当前用户是否匿名用户boolean isAnonymous();// 当前用户是否已经认证成功boolean isAuthenticated();// 当前用户是否通过RememberMe记住我自动登录boolean isRememberMe();// 当前用户是否既不是宁ing用户也不是通过rememberMe自动登录boolean isFullyAuthenticated();// 当前用户是否具备指定目标的指定权限信息boolean hasPermission(Object target, Object permission);// 当前用户是否具备指定目标的指定权限信息boolean hasPermission(Object targetId, String targetType, Object permission);
}

1.3 URL匹配规则:antMatchers、mvcMatchers、regexMatchers

antMatchers和mvcMatchers的区别,在于mvcMatchers更加强大通用,而regexMatchers的好处是支持正则表达式。
SpringSecurity学习(七)授权

2. 基于方法的权限管理

基于方法的权限管理通过AOP来实现,SpringSecurity中通过MethodSecurityInterceptor来提供相关实现。不同在于FilterSecurityInterceptor只是在请求之前进行前置处理,MethodSecurityInterceptor除了前置处理之外还可以进行后置处理。前置处理就是在请求之前判断是否具有响应权限,而后置处理则是对方法执行结果进行二次过滤。前置处理和后置处理对应了不同的实现类。

@EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity注解用来开启权限,用法如下:

@EnableWebSecurity
// 开启全局方法权限配置,仅可能的显示配置三个属性为true
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class SecurityConfig2 {
  • prePostEnabled :开启SpringSecurity提供的四个权限注解@PostAuthorize@PostFilter@PreAuthorize@PreFilter
  • securedEnabled:开启SpringSecurity提供的@Secured注解支持,该注解不支持权限表达式
  • jsr250Enabled:开启JSR-250提供的注解,主要是@DenyAll@PermitAll@RolesAll,同样的这些注解也不支持权限表达式。
注解 含义
@PostAuthorize 在目标方法执行之后进行权限校验
@PostFilter 在目标方法执行之后对返回结果进行过滤
@PreAuthorize 在目标方法执行之前进行权限校验
@PreFilter 在目标方法执行之前对方法参数进行过滤
@Secured 访问目标方法必须具备对应的角色
@DenyAll 拒绝所有访问
@PermitAll 允许所有访问
@RolesAll 访问目标方法必须具备对应的角色

这些基于方法的权限管理相关的注解,由于后四个不常用,一般来说只需要设置prePostEnabled =true即可
权限表达式:例子hasRole("admin")

案例:

编写SecurityConfig
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class SecurityConfig2 {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());manager.createUser(User.withUsername("whx").password("{noop}123").roles("USER").build());manager.createUser(User.withUsername("dy").password("{noop}123").authorities("READ_INFO").build());return manager;}@Beanpublic SecurityFilterChain configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests(req -> {req.anyRequest().authenticated();});http.formLogin();http.csrf().disable();return http.build();}
}
编写T2Controller
@RestController
@RequestMapping("t2")
public class T2Controller {@PreAuthorize("hasRole('ADMIN')  and authentication.name == 'root'")@RequestMapping("/")public String home() {return "<h1>HI SPRING SECURITY</hi>";}// http://localhost:8888/t2/name?name=root@PreAuthorize("authentication.name == #name")@RequestMapping("/name")public String admin(String name) {return "<h1>HI SPRING " + name + "</hi>";}//    [ { "id":"1","username":"huathy" },
//    { "id":"2","username":"dy" } ]@PreFilter(value = "filterObject.id%2 != 0", filterTarget = "users")  //filterTarget参数必须是集合类型@RequestMapping("/add")public List<User> add(@RequestBody List<User> users) {List<User> result = new ArrayList<>();for (User user : users) {result.add(User.build(user.getId(), user.getUsername()));}return result;}// http://localhost:8888/t2/userId?id=1@PostAuthorize("returnObject.id == 1")@RequestMapping("/userId")public User userId(Integer id) {User user = User.build(id, "HUATHY");return user;}@PostFilter("filterObject.id%2 == 0")@RequestMapping("/lists")public List<User> getAllUser() {List<User> users = new ArrayList<>();for (int i = 0; i < 10; i++) {users.add(User.build(i, "嘻嘻" + i));}return users;}@PreAuthorize("hasAuthority('READ_INFO')")@RequestMapping("/getInfo")public Object getInfo() {return SecurityContextHolder.getContext().getAuthentication().getPrincipal();}/* === 以下是不常用的 只做演示 === */// 具备其中一个即可@Secured({"ROLE_ADMIN", "ROLE_USER"})@RequestMapping("/secured")public String secured() {return "<h1>HUATHY</h1>";}@PermitAll@RequestMapping("permitAll")public String permitAll() {return "<h1>permitAll</h1>";}@DenyAll@RequestMapping("DenyAll")public String DenyAll() {return "<h1>DenyAll</h1>";}@RolesAllowed({"ROLE_ADMIN", "ROLE_USER"})// 具备其中一个即可@RequestMapping("rolesAllowed")public String rolesAllowed() {return "<h1>rolesAllowed</h1>";}
}

四、授权的原理分析

SpringSecurity学习(七)授权

  • ConfigAttribute在springsecurity中,用户请求一个资源(通常是一个接口或者Java方法)需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法,该方法赶回一个String字符串(角色名称)。一般的角色名称都带有一个ROLE_前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具有的角色和请求某个资源所需要的ConfigAttribute之间的关系。
  • AccessDecisionVoterAccessDecisionManager都有众多实现类。在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户方法,因而AccessDecisionVoter和AccessDecisionManager俩者关系类似于AuthenticationProvicder和ProviderManager的关系。

授权实战—权限模型说明1

在前面的案例中,我们配置的URL拦截规则和URL所需要的权限都是通过代码配置的,这样过于死板。如果需要动态的管理权限规则,我们可以将URL拦截规则和访问URL所需的权限都保存到数据库中,这样在不修改代码的情况下只需要吸怪数据库即可对权限进行调整。
用户 < --用户角色表-- > 角色 < --角色菜单表-- > 菜单

库表设计

create table menu
(id      int auto_incrementprimary key,pattern varchar(100) null
)comment '菜单表';create table menu_role
(id  int auto_incrementprimary key,rid int not null,mid int not null
);create table role
(id      int auto_incrementprimary key,name    varchar(255) null,name_cn varchar(255) null
);create table user
(id                    int auto_incrementprimary key,username              varchar(255) null,password              varchar(255) null,accountNonExpired     int(1)       null,accountNunLocked      int(1)       null,credentialsNonExpired int(1)       null,enable                int(1)       null
);create table user_role
(id  int auto_incrementprimary key,uid int null,rid int null
);

数据

insert into role values (101,'superadmin','超级管理员');
insert into role values (102,'admin','管理员');
insert into role values (103,'user','普通用户');insert into user values (1001,'huathy','{noop}123',0,0,0,0);
insert into user values (1002,'whx','{noop}123',0,0,0,0);
insert into user values (1003,'dy','{noop}123',0,0,0,0);insert into user_role values (0,1001,101);
insert into user_role values (0,1002,102);
insert into user_role values (0,1003,103);insert into menu values (1,'/admin/**');
insert into menu values (2,'/user/**');
insert into menu values (3,'/guest/**');insert into menu_role values (0,101,1);
insert into menu_role values (0,102,2);
insert into menu_role values (0,103,3);

实现

本文只展示了核心代码,详细的参考附录1。

1. MyUserDetailsService

实现自定义UserDetailsService,从数据库获取用户信息。

@Component
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.getUserByUname(username);if (ObjectUtils.isEmpty(user)) {throw new UsernameNotFoundException("用户名不正确");}List<Role> roles = userMapper.getRolesByUid(user.getId());user.setRoles(roles);return user;}
}

2. 编写SecurityCfg配置,来自定义URL权限处理。

@EnableWebSecurity
public class SecurityCfg {@Autowiredprivate CustomerSecurityMetadataSource customerSecurityMetadataSource;@Beanpublic SecurityFilterChain configure(HttpSecurity http) throws Exception {// 1. 获取工厂对象ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);// 2. 设置自定义URL权限处理http.apply(new UrlAuthorizationConfigurer<>(applicationContext)).withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O object) {object.setSecurityMetadataSource(customerSecurityMetadataSource);// 是否拒绝公共资源的访问object.setRejectPublicInvocations(false);return object;}});http.authorizeHttpRequests().anyRequest().authenticated();http.formLogin();http.csrf().disable();return http.build();}
}

3. 编写自定义权限元数据CustomerSecurityMetadataSource

需要注意的是此类中的SecurityConfig,是springSecurity官方的SecurityConfig。

@Component
public class CustomerSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {@AutowiredMenuService menuService;// 用来做路径比对AntPathMatcher antPathMatcher = new AntPathMatcher();/*** 自定义动态资源权限元数据信息** @param object* @return* @throws IllegalArgumentException*/@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {// 根据当前请求对象获取URIString requestURI = ((FilterInvocation) object).getRequest().getRequestURI();// 查询菜单对象List<Menu> allMenu = menuService.getList(null);for (Menu menu : allMenu) {if (antPathMatcher.match(menu.getPattern(), requestURI)) {String[] roles = new String[menu.getRoles().size()];for (int i = 0; i < menu.getRoles().size(); i++) {roles[i] = "ROLE_" + menu.getRoles().get(i).getName();}return SecurityConfig.createList(roles);}}return null;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}@Overridepublic boolean supports(Class<?> clazz) {return FilterInvocation.class.isAssignableFrom(clazz);}
}

4. MenuService

@Service
public class MenuService {@Autowiredprivate MenuMapper menuMapper;@Autowiredprivate MenuRoleMapper menuRoleMapper;public List<Menu> getList(Object o) {List<Menu> menus = menuMapper.selectList(null);for (Menu menu : menus) {List<Role> roles = menuRoleMapper.getAllMenuRoles(menu.getId());if (!CollectionUtils.isEmpty(roles)) {menu.setRoles(roles);}}return menus;}
}

踩坑

这里有点坑的地方就是SpringSecurity会给角色的权限自动加上ROLE_,即便我加了前缀,他还是会自动加一次。这导致了这里equls的时候匹配失败。所以这里我们取消数据库中的前缀,这样查询出来的用户的角色是不带前缀的(eg:ADMIN)而我们在查询菜单的角色的构建CustomerSecurityMetadataSource元数据的时候给手动加上前缀ROLE_,就像这样子:roles[i] = "ROLE_" + menu.getRoles().get(i).getName();
SpringSecurity学习(七)授权

附录:

  1. 本文涉及代码部分https://gitee.com/huathy/study-all/tree/master/spring_security_study