SpringSecurity学习(七)授权
授权
什么是权限管理
权限管理核心概念
SpringSecurity权限管理策略
基于URL地址的权限管理
基于方法的权限管理
一、权限管理
二、授权核心概念
在认证的过程成功之后会将当前用户登录信息保存到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)
- 基于过滤器(URL)的权限管理(FilterSecurityInterceptor)
基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来后,根据http请求地址进行权限校验。 - 基于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的好处是支持正则表达式。
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>";}
}
四、授权的原理分析
ConfigAttribute
在springsecurity中,用户请求一个资源(通常是一个接口或者Java方法)需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法,该方法赶回一个String字符串(角色名称)。一般的角色名称都带有一个ROLE_
前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具有的角色和请求某个资源所需要的ConfigAttribute之间的关系。AccessDecisionVoter
和AccessDecisionManager
都有众多实现类。在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();
。
附录:
- 本文涉及代码部分https://gitee.com/huathy/study-all/tree/master/spring_security_study