SpringSecurity之授权

1.权限管理

1.1.认证

身份认证,就是判断一个用户是否为合法用户的处理过程。Spring Security中支持多种不同方式的认证,但是无论开发者使用哪种方式认证,都不会影响授权功能使用。因为Spring Security很好做到了认证和授权解耦。

1.2.授权

授权,即访问控制,控制谁能访问哪些资源。简单的理解授权就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行相应操作。

2.授权核心概念

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

SpringSecurity之授权_第1张图片

那么问题来了,针对于这个返回值GrantedAuthority应该如何理解?是角色还是权限?

基于角色进行权限管理

基于资源进行权限管理 -->权限字符串

R(Role Resources) B(base) A(access) C(controll)

我们针对于授权可以是基于角色权限管理基于资源权限管理,从设计层面上来说,角色和权限是两个完全不同的东西:权限是一些具体操作,角色则是某些权限集合。如:READ_BOOK和ROLE_ADMIN是完全不同的。因此至于返回值是什么取决于你的业务设计情况:

  • 基于角色权限设计就是:用户<>角色<>资源 三者关系 返回就是用户的角色

  • 基于资源权限设计就是: 用户<>权限<>资源 三者关系 返回就是用户的权限

  • 基于角色和资源权限设计就是: 用户<>角色<>权限<==>资源 返回统称为用户的权限

为什么统称为权限,因为从代码层面角色和权限没有太大不同都是权限,特别是在Spring Security中。角色和权限处理方式基本上都是一样的。唯一区别SpringSecurity在很多时候会自动给角色添加一个**ROLE_**前缀,而权限则不会自动添加。

3.权限管理策略

可以访问系统中哪些资源(http url method)

Spring Security中提供的权限管理策略主要有两种类型:

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

3.1.基于URL的权限管理

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //创建内存数据源
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/admin").hasRole("ADMIN")  //具有 admin角色
                .mvcMatchers("/user").hasRole("USER")   //具有user角色
                .mvcMatchers("/getInfo").hasAuthority("READ_INFO")  //具有read_info权限
                .antMatchers(HttpMethod.GET,"admin").hasRole("ADMIN")
//                .regexMatchers().hasRole()   //好处: 支持正则表达式
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable();
    }
}

SpringSecurity之授权_第2张图片

权限表达式
SpringSecurity之授权_第3张图片

SpringSecurity之授权_第4张图片

3.2.基于方法的权限管理

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

@EnableGlobalMethodSecurity

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

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{

}
  • perPostEnabled:开启Spring Security提供的四个权限注解,@PostAuthorize、@PostFilter、@PreAuthorize以及@PreFilter。
  • securedEnabled:开启Spring Security提供的@Secured注解支持,该注解不支持权限表达式
  • jsr250Enabled:开启JSR-250提供的注解,主要是@DenyAll、@PermitAll、@RolesAll同样这些注解也不支持权限表达式
# 以上注解含义如下:
- @PostAuthorize: 在目标方法执行之后进行权限校验。
- @PostFilter: 在目标方法执行之后对方法的返回结果进行过滤。
- @PreAuthorize: 在目标方法执行之前进行权限校验。
- @PreFilter: 在目标方法执行之前对方法参数进行过滤。
- @secured: 访问目标方法必须具备相应的角色。
- @DenyAll: 拒绝所有访问。
- @PermitAll: 允许所有访问。
- @RolesAllowed: 访问目标方法必须具备相应的角色。

这些机遇方法的权限管理相关的注解,一般来说只要设置prePostEnabled=true就够用了。

SpringSecurity之授权_第5张图片

SpringSecurity之授权_第6张图片

@RestController
@RequestMapping("/hello")
public class AuthorizeMethodController {

    @PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
    @GetMapping
    public String hello(){
        return "hello";
    }
    @PreAuthorize("authentication.name ==#name")    //参数与认证的用户名一致才可以访问
    @GetMapping("/name")
    public String hello(String name){
        return "hello:" + name;
    }

    @PreFilter(value ="filterObject.id%2!=0",filterTarget = "users")   //filterTarget表示要过滤的参数,必须是数组、集合类型    filterObject是数组中的对象,如User。
    @PostMapping("/users")
    public void addUsers(@RequestBody List<User> users){
        System.out.println("users = "+ users);
    }

    @PostAuthorize("returnObject.id ==1")    //表示方法返回前处理
    @GetMapping("/userId")
    public User getUserById(Integer id){
        return new User(id,"zkt");
    }
    @PostFilter("filterObject.id%2==0")   //用来对方法返回值进行过滤
    @GetMapping("/lists")
    public List<User> getAll(){
        List<User> users = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            users.add(new User(i,"zkt"+i));
        }
        return users;
    }
    @Secured({"ROLE_USER"}) //只能判断角色
    @GetMapping("/secured")
    public User getUserByUsername(){
        return new User(99,"secured");
    }

    @Secured({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个即可
    @GetMapping("/username")
    public User getUserByUsername2(String username){
        return new User(99,username);
    }

    @PermitAll
    @GetMapping("/permitAll")
    public String permitAll(){
        return "PermitAll";
    }

    @DenyAll
    @GetMapping("/denyAll")
    public String denyAll(){
        return "DenyAll";
    }

    @RolesAllowed({"ROLE_ADMIN","ROLE_USER"})  //具有其中一个角色即可
    @GetMapping("/rolesAllowed")
    public String rolesAllowed(){
        return "RolesAllowed";
    }

}

4.原理分析

SpringSecurity之授权_第7张图片

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

5.动态授权实战

在前面的案例中,我们配置的URL拦截规则和请求URL所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个URL所需要的权限,就需要修改代码。

动态管理权限规则就是我们将URL拦截规则和访问URI所需要的权限都保存在数据库中,这样,在不修改源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。

5.1.库表设计

用户 <----> 中间表 <----> 角色 <----> 中间表 <---->菜单

set Names utf8mb4;
set foreign_key_checks = 0;
drop table if exists `menu`;
create table `menu`(
		`id` int(11) not null auto_increment,
		`pattern` varchar(128) default null,
		primary key(`id`)
	)engine=Innodb auto_increment=4 default charset=utf8;

begin;
insert into `menu` values(1,'/admin/**');
insert into `menu` values(2,'/user/**');
insert into `menu` values(3,'/guest/**');
commit;

drop table if exists `menu_role`;
create table `menu_role`(
	`id` int(11) not null auto_increment,
	`mid` int(11) default null,
	`rid` int(11) default null,
	primary key(`id`),
	key `mid`(`mid`),
	key `rid`(`rid`),
	constraint `menu_role_ibfk_1` foreign key(`mid`) REFERENCES `menu`(`id`),
	constraint `menu_role_ibfk_2` foreign key(`rid`) REFERENCES `role`(`id`)
)engine=Innodb auto_increment=5 default charset=utf8;

begin;
insert into `menu_role` values(1,1,1);
insert into `menu_role` values(2,2,2);
insert into `menu_role` values(3,3,3);
insert into `menu_role` values(4,3,2);

drop table if exists `role`;

create table `role`(
	`id` int(11) not null auto_increment,
	`name` varchar(32) default null,
	`nameZh` varchar(32) default null,
	primary key(`id`)
)engine=innodb auto_increment=4 default charset=utf8;

begin;
insert into `role` values(1,'ROLE_ADMIN','系统管理员');
insert into `role` values(2,'ROLE_USER','普通用户');
insert into `role` values(3,'ROLE_GUEST','游客');
commit;

drop table if exists `user`;
create table `user`(
	`id` int(11) not null auto_increment,
	`username` varchar(32) default null,
	`password` varchar(255) default null,
	`enabled` tinyint(1) default null,
	`locked` tinyint(1) default null,
	primary key(`id`)
	)engine=InnoDB auto_increment=4 default charset=utf8;
	
	begin;
	insert into `user` values(1,'admin','{noop}123',1,0);
	insert into `user` values(2,'user','{noop}123',1,0);
	insert into `user` values(3,'zkt','{noop}123',1,0);
	commit;
	
	drop table if exists `user_role`;
	create table `user_role`(
		`id` int(11) not null auto_increment,
		`uid` int(11) default null,
		`rid` int(11) default null,
		primary key(`id`),
		key `uid`(`uid`),
		key `rid`(`rid`),
		CONSTRAINT `user_role_ibfk_1` foreign key(`uid`) REFERENCES `user`(id),
		constraint `user_role_ibfk_2` foreign key(`rid`) REFERENCES `role`(`id`)	
	)engine=innodb auto_increment=5 default charset=utf8;
	
	begin;
	insert into `user_role` VALUES(1,1,1);
	insert into `user_role` VALUES(2,1,2);
	insert into `user_role` VALUES(3,2,2);
  insert into `user_role` VALUES(4,3,3);
	commit;
	set foreign_key_checks =1;

5.2.springboot依赖

 		
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.8version>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.38version>
        dependency>

        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.2.0version>
        dependency>

5.3.springboot配置设置

# 应用名称
spring.application.name=spring-security-17dynamic-authorize
# 应用服务 WEB 访问端口
server.port=8080


spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security_authorize?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:com/zkt/mapper/*.xml
mybatis.type-aliases-package=com.zkt.entity

5.4.实体类

public class Role {
    private Integer id;
    private String name;
    private String nameZh;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNameZh() {
        return nameZh;
    }

    public void setNameZh(String nameZh) {
        this.nameZh = nameZh;
    }
}

public class User implements UserDetails {

    private Integer id;
    private String username;
    private String password;

    private boolean enabled;
    private boolean locked;
    private List<Role> roles;

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(r ->new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public boolean isLocked() {
        return locked;
    }

    public void setLocked(boolean locked) {
        this.locked = locked;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}
public class Menu {
    private Integer id;
    private String pattern;
    private List<Role> roles;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

5.5.mapper文件


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zkt.dao.UserMapper">
    
    
    <select id="loadUserByUsername" resultType="com.zkt.entity.User">
        select *
        from user
        where username = #{username}
    select>


    <select id="getUserRoleByUid" resultType="com.zkt.entity.Role">
        select r.* from role r, user_role ur
        where r.id = ur.rid
        and ur.rid = #{uid}
    select>
mapper>

DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zkt.dao.MenuMapper">

    <resultMap id="MenuResultMap" type="com.zkt.entity.Menu">
        <id property="id" column="id"/>
        <result property="pattern" column="pattern">result>
        <collection property="roles" ofType="com.zkt.entity.Role">
            <id column="rid" property="id"/>
            <result column="rname" property="name"/>
            <result column="rnameZh" property="nameZh"/>
        collection>
    resultMap>

    <select id="getAllMenu" resultMap="MenuResultMap">
        select m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh
        from menu m
            left join menu_role mr on m.`id` = mr.`mid`
            left join role r on r.`id` =mr.`rid`
    select>
mapper>

5.6.Dao层

@Mapper
public interface MenuMapper {
    List<Menu> getAllMenu();
}

@Mapper
public interface UserMapper {

    //根据用户id获取角色信息
    List<Role> getUserRoleByUid(Integer uid);
    //根据用户名获取用户信息
    User loadUserByUsername(String username);
}

5.7.Service层

@Service
public class UserService implements UserDetailsService {

    private UserMapper userMapper;

    @Autowired
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.根据用户名查询信息
        User user = userMapper.loadUserByUsername(username);
        if( user == null){
            throw new UsernameNotFoundException("用户不存在");
        }

        user.setRoles(userMapper.getUserRoleByUid(user.getId()));
        return user;
    }
}
@Service
public class MenuService {

    private final MenuMapper menuMapper;

    @Autowired
    public MenuService(MenuMapper menuMapper) {
        this.menuMapper = menuMapper;
    }

    public List<Menu> getAllMenu(){
        return menuMapper.getAllMenu();
    }
}

5.8.Controller层

@RestController
public class HelloController {

    @GetMapping("/admin/hello")
    public String admin(){
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user(){
        return "hello user";
    }

    @GetMapping("/guest/hello")
    public String guest(){
        return "hello guest";
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

5.9.自定义CustomSecurityMetaSource(相关)

@Component
public class CustomSecurityMetaSource implements FilterInvocationSecurityMetadataSource {

    private MenuService menuService;

    @Autowired
    public CustomSecurityMetaSource(MenuService menuService) {
        this.menuService = menuService;
    }

    AntPathMatcher antPathMatcher = new AntPathMatcher();


    /*
    * 自定义动态资源权限元数据信息
    * */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //1.当前请求对象
        String requestURI = ((FilterInvocation)object).getRequest().getRequestURI();
        //2.查询所有菜单
        List<Menu> allMenu = menuService.getAllMenu();

        for(Menu menu : allMenu){
            if(antPathMatcher.match(menu.getPattern(),requestURI)){
                String[] roles = menu.getRoles().stream().map(r ->r.getName()).toArray(String[]::new);
                return SecurityConfig.createList(roles);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

SpringSecurity之授权_第8张图片

SpringSecurity之授权_第9张图片

5.10.自定义配置类中配置(相关)

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled=true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomSecurityMetaSource customSecurityMetadataSource;
    private final UserDetailsService userDetailsService;

    @Autowired
    public MySecurityConfig(CustomSecurityMetaSource customSecurityMetadataSource,UserDetailsService userDetailsService) {
        this.customSecurityMetadataSource = customSecurityMetadataSource;
        this.userDetailsService = userDetailsService;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void 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(customSecurityMetadataSource);
                        //是否拒绝公共资源访问
                        object.setRejectPublicInvocations(false);
                        return object;
                    }
                });
        http.formLogin().and().csrf().disable();
    }
}

SpringSecurity之授权_第10张图片

5.11.测试

数据库表中对应的权限如下
SpringSecurity之授权_第11张图片

使用不同的用户进行登录进行访问相对应的资源。

你可能感兴趣的:(SpringSecurity,java,spring)