SpringSecurity学习(七)授权

时间:2021-03-05 00:58:18

授权

什么是权限管理
权限管理核心概念
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 {
    @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的好处是支持正则表达式。
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 {
    @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>";
    }
}

四、授权的原理分析

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_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();
SpringSecurity学习(七)授权

附录:

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