侧边栏壁纸
  • 累计撰写 58 篇文章
  • 累计创建 67 个标签
  • 累计收到 1 条评论

认识 Spring Security

lihaocheng
2021-05-24 / 0 评论 / 0 点赞 / 870 阅读 / 4,797 字
温馨提示:
晚上记得开启夜间模式哦

这几天在研究开源 IoT 框架————Thingsboard ,Tb 使用了 Spring Security 来进行用户权限控制。之前一直想深入学习一下 Spring Security,借这个机会正好研究一下。

一、基本原理

Spring Security 是借助过滤器来协助完成的。

二、Getting Start

1.引入 Spring Security 包

第一步,当然是用 Maven 引入对应的包,我这里是一个 Spring Boot 项目,直接使用 spring-boot-starter-security 即可

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置完成后,我们新建一个 Controller 进行测试,代码如下

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

完成后,我们重新启动项目,访问 localhost:8080/hello

页面提示需要登录

截屏20210602 上午11.55.33.png

使用用户名user和从控制台找到第一次启动时生成的密码登录,就可以看到我们在 TestController 中定义的内容了。

访问每一个接口都需要先登录,这并非我们想要的,那么能不能选择一部分接口要登录,而另一部分可以直接访问呢?答案就是————WebSecurityConfigurerAdapter。

2.创建配置类 WebSecurityConfigurerAdapter

我们创建一个 MySecurityConfiguration 使其继承自 WebSecurityConfigurerAdapter。
并重写其configure(HttpSecurity http)方法、configure(AuthenticationManagerBuilder auth)方法。

@Configuration
@EnableWebSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return Objects.equals(charSequence.toString(),s);
            }
        };
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("admin").roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/api/**").hasRole("ADMIN")
                .and() .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and().csrf().disable();
    }
}

其中 configure(AuthenticationManagerBuilder auth) 定义了存储在内存的角色或权限,这里我们新建了一个 ADMIN 角色;

而 configure(HttpSecurity http) 定义了访问哪些接口需要什么样权限或角色,这里是/api下面的接口需要 ADMIN 角色才能访问

至于其他的部分,我们留到后面再讨论。

此时我们再创建一个名为 AdminController 的类,该类提供了一个 /api/admin 的接口

@RestController
@RequestMapping("/api")
public class AdminController {
    @GetMapping("/admin")
    public String getAdminById(AdminId adminId){
        return "admin";
    }
}

此时我们重新启动项目,和我们预想的一样————访问/api/admin接口提示我们需要登录,而之前的/hello接口不需要登录就能访问了。

三、核心配置———WebMvcConfigurerAdapter

四、注解

Controller 其中使用到了 Spring Security 的这几个注解

1.@PreAuthorize

在操作前进行权限的校验,在 Thingboard 中 Controller 大部分都是通过 @PreAuthorize 来实现鉴权的。
例如:AdminController 中的 getAdminSettings()

    @PreAuthorize("hasAuthority('SYS_ADMIN')")
    @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET)
    @ResponseBody
    public AdminSettings getAdminSettings(@PathVariable("key") String key) throws ThingsboardException {
	//省略了其他代码
        .....
    }

通过hasAuthority()参数来指定调用该方法所需的权限级别。
例如:调用 getAdminSettings() 方法时就会先验证用户是否为系统管理员。
此外,@PreAuthorize 也支持 Spring 表达式语言,提供了基于表达式的访问控制。

//同时拥有normal和admin权限才可以使用被修饰的方法
@PreAuthorize("hasAuthority('TENANT_NORMAL') AND hasAuthority('TENANT_ADMIN')") 

我们可以通过 AND 连接两个hasAuthority来表示需要同时拥有 normal 和 admin 权限才可以使用.

(1)hasAuthority()

我们可以从的 security.access.expression 包下的SecurityExpressionRoot

public final boolean hasAuthority(String authority) {
        return this.hasAnyAuthority(authority);
}

public final boolean hasAnyAuthority(String... authorities) {
        return this.hasAnyAuthorityName((String)null, authorities);
    
}

public final boolean hasRole(String role) {
        return this.hasAnyRole(role);
}

public final boolean hasAnyRole(String... roles) {
        return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
}

//上面的方法最终都会将数据交给 hasAnyAuthorityName 进行处理
private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = this.getAuthoritySet();
        String[] var4 = roles;
        int var5 = roles.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
}

从 hasAuthority 的源码中我们可以看到,无论我们使用hasAuthority()还是hasAnyAuthority(),最终都会交给hasAnyAuthorityName()来进行处理。

hasAnyAuthorityName()使用getAuthoritySet()方法获取到权限的集合,再判断是否和传入的参数契合,契合则返回 true,否则返回 flase。

0

评论区