Spring Boot「43」为不同的 URL 配置安全规则

290 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 28 天,点击查看活动详情

在前面几篇文章中,我介绍了如何使用 Spring Security 框架进行安全认证。 今天,我将介绍如何为不同的 URL 配置不同的认证规则。

01-创建 API 接口

继续沿用 Spring Boot「41」一文搞懂 Spring Security 是如何工作的? 中的 HelloWorld 程序。 在原来程序的基础上,增加两个接口 User 列表接口:

@RestController
public class UserController {

    @Autowired
    private DemoUserRepository userRepository;

    @GetMapping("/users")
    public List<DemoUser> getUsers() {
        List<DemoUser> all = getUserRepository().findAll();
        return all;
    }

    public DemoUserRepository getUserRepository() {
        return userRepository;
    }
}

Permission 接口:

@RestController
public class PermissionController {

    @Autowired
    private DemoPermissionRepository permissionRepository;

    @GetMapping("/permissions")
    public List<DemoPermission> getPermissions() {
        List<DemoPermission> all = getPermissionRepository().findAll();
        return all;
    }

    public DemoPermissionRepository getPermissionRepository() {
        return permissionRepository;
    }
}

02-基于 URL 设置安全相关配置

使用 Spring Security 时,默认情况下,访问所有 URL 都会被拦截,若未认证,则不允许用户访问。 接下来,针对上节增加的两个 API 接口,我们打算制定如下的访问控制规则:

  • 对于 /users 开头的 URL,允许所有用户访问。
  • 针对 /permissions 开头的 URL,只允许具备 ADMIN 权限的用户访问。

这种访问控制如何在 Spring Security 框架中实现呢? 可以通过向 ApplicationContext 中注入一个 SecurityFilterChain 实现。 我们来看下代码:

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .antMatchers("/users/**")   // /users 开头的 URL 允许所有人访问
                .permitAll()
                .and()
                .authorizeHttpRequests()
                .antMatchers("/permissions/**")   // /permissions 开头的 URL 只允许 ADMIN 访问
                .hasRole("ADMIN")
                .anyRequest()
                .authenticated()
                .and()
                .httpBasic();
        return http.build();
    }
}

下面我来简单解释一下上面配置的含义。

  • authorizeHttpRequests() 意为将框架配置为,在控制用户访问时,使用如下的规则。
  • antMatchers("/users/**").permitAll() 意为对满足这种 URL 规则的访问,使用 permitAll 规则,即允许任何人访问。
  • and() 指与下面的规则同时生效
  • antMatchers("/permissions/**").hasRole("ADMIN") 意为对满足这种 URL 规则的访问,访问者必须具有 ADMIN 角色。若没有认证通过,则返回 “401 Unauthorized”; 对于认证通过,但是不具备 ADMIN 角色的请求,则返回 “403 Forbidden”。
  • anyRequest().authenticated() 是默认配置,针对的是前面规则约束不到的请求,例如 "/roles" 请求并不匹配前面两种规则,则适用默认配置。这里要求其他请求必须是认证过的。 需要注意的是,默认配置只能有一个,且需要在规则的最后面。

03-基于内存创建用户

有了上述的配置,我们来创建两个用户。 这里我们使用 InMemoryUserDetailsManager,即基于内存的用户信息存储,不用依赖数据库,测试起来更方便。

@Bean(name = "userDetailsService")
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
    User.UserBuilder users = User.withDefaultPasswordEncoder();
    UserDetails user = users
            .username("user")
            .password("password")
            .roles("USER")
            .build();
    UserDetails admin = users
            .username("admin")
            .password("password")
            .roles("USER", "ADMIN")
            .build();
    return new InMemoryUserDetailsManager(user, admin);
}

我创建了两个用户:其中一个名为 user,它具有 USER 角色;另一个名为 admin,它具有 ADMIN 角色。

04-运行测试用例

上述工作完成之后,让我们启动 HelloWorld 应用。 测试 http://localhost:8080/users 是否是不需要验证就能访问:

curl -i http://localhost:8080/users
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 02 Mar 2023 14:14:17 GMT

[{"userId":1,"username":"admin","password":"{noop}admin","locked":0,"ctime":20230211143234,"enabled":true,"accountNonExpired":true,"credentialsNonExpired":true,"accountNonLocked":true,"authorities":null},{"userId":2,"username":"lihua","password":"{noop}lihua123","locked":0,"ctime":20230211143234,"enabled":true,"accountNonExpired":true,"credentialsNonExpired":true,"accountNonLocked":true,"authorities":null},{"userId":3,"username":"hanmeimei","password":"{noop}hanmeimei123","locked":0,"ctime":20230211143234,"enabled":true,"accountNonExpired":true,"credentialsNonExpired":true,"accountNonLocked":true,"authorities":null}]

能够拿到用户列表,说明访问成功了。

下面,我们来测试下 http://localhost:8080/permission 能否访问:

curl -i http://localhost:8080/permissions
HTTP/1.1 401
Set-Cookie: JSESSIONID=A5000511919EE9745B91E5CA0B704AE6; Path=/; HttpOnly
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Thu, 02 Mar 2023 14:14:25 GMT

结果是 401,说明需要认证。接下来,我们添加用户名、密码来访问:

curl -u admin:password -i http://localhost:8080/permissions
HTTP/1.1 200
Set-Cookie: JSESSIONID=E9A29F2BAE0D1708649439D21E04BEDB; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 02 Mar 2023 14:18:41 GMT

[{"permissionId":1,"name":"新增用户","permissionValue":"user:add","status":1,"authority":"user:add"},{"permissionId":2,"name":"删除用户","permissionValue":"user:delete","status":1,"authority":"user:delete"},{"permissionId":3,"name":"查看用 户","permissionValue":"user:get","status":1,"authority":"user:get"}]

结果是 200,说明我们访问成功了。 最后,我们测试下,没有 ADMIN 权限的用户 user,访问 http://localhost:8080/permissions 是什么反应?

curl -u user:password -i http://localhost:8080/permissions
HTTP/1.1 403
Set-Cookie: JSESSIONID=F3FF230E5A1741E7146BD5A1DED66B63; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 02 Mar 2023 14:19:51 GMT

{"timestamp":"2023-03-02T14:19:51.685+00:00","status":403,"error":"Forbidden","path":"/permissions"}

返回结果是 403,拒绝访问,跟我们的预期一致。

05-总结

今天,我介绍了如何在 Spring Security 中配置不同的 URL 需要不同的认证级别。 并且,基于之前的 HelloWorld 程序,实现了基于 URL 的访问控制。最后,对实现的程序进行了验证,符合起初的预期。

希望今天的内容能对你有所帮助。