授权
什么是权限管理
权限管理核心概念
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 {
@Bean
public 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;
}
@Bean
public 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 {
@Bean
public 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;
}
@Bean
public 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_increment
primary key,
pattern varchar(100) null
)
comment '菜单表';
create table menu_role
(
id int auto_increment
primary key,
rid int not null,
mid int not null
);
create table role
(
id int auto_increment
primary key,
name varchar(255) null,
name_cn varchar(255) null
);
create table user
(
id int auto_increment
primary 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_increment
primary 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 {
@Autowired
private UserMapper userMapper;
@Override
public 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 {
@Autowired
private CustomerSecurityMetadataSource customerSecurityMetadataSource;
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
// 1. 获取工厂对象
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
// 2. 设置自定义URL权限处理
http.apply(new UrlAuthorizationConfigurer<>(applicationContext)).withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <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 {
@Autowired
MenuService menuService;
// 用来做路径比对
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 自定义动态资源权限元数据信息
*
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 根据当前请求对象获取URI
String 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;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
4. MenuService
@Service
public class MenuService {
@Autowired
private MenuMapper menuMapper;
@Autowired
private 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