• 【SpringSecurity系列3】基于Spring Webflux集成SpringSecurity实现前后端分离无状态Rest API的权限控制


    源码传送门:
    https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/02-springsecurity-stateless-webflux
    

    一、前言

    Spring WebFlux 是一个异步非阻塞式的 Web 框架,它能够充分利用多核 CPU 的硬件资源去处理大量的并发请求。SpringSecurity 专门为 Webflux 定制了一套用于权限控制的 API,因此在 Webflux 应用中集成
    SpringSecurity,和前面讲的 Web 应用集成 SpringSecurity 还是有一定区别。老规矩,我们先看实现步骤,后续再来分析原理。

    二、实现步骤

    1、引入依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.1</version>
        </dependency>
    </dependencies>
    

    2、改写 Get 方法的 /login 请求

    和 Spring Web 集成 SpringSecurity 一样,Spring Webflux 集成 SpringSecurity 也需要改写默认的 Get 方法 /login 请求,但是有个小地方要注意下,就是在 Webflux 中要返回 Mono。源码如下:

    @GetMapping(value = "/login")
    public Mono<Result> login() {
        return Mono.just(Result.data(-1, "PLEASE LOGIN", "NO LOGIN"));
    }
    

    Result 类是定义的一个通用响应对象,具体代码可查看附上的源码链接。

    3、创建认证信息存储器 AuthenticationRepository

    在实际生产环境中,我们应该把认证信息存储在缓存或者数据库中,此处只是演示,就放在内存中了。具体代码如下:

    @Repository
    public class AuthenticationRepository {
    
        private static ConcurrentHashMap<String, Authentication> authentications = new ConcurrentHashMap<>();
    
        public void add(String key, Authentication authentication) {
            authentications.put(key, authentication);
        }
    
        public Authentication get(String key) {
            return authentications.get(key);
        }
    
        public void delete(String key) {
            if (authentications.containsKey(key)) {
                authentications.remove(key);
            }
        }
    }
    

    4、创建认证成功处理器 TokenServerAuthenticationSuccessHandler 和认证失败处理器 TokenServerAuthenticationFailureHandler

    对于 Webflux 应用 SpringSecurity 为我们提供了不同的认证成功接口 ServerAuthenticationSuccessHandler 和 认证失败处理接口 ServerAuthenticationFailureHandler,我们只需要实现这两个接口,然后实现我们需要的业务逻辑即可,具体代码如下:

    @Component
    public class TokenServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
    
        @Autowired
        private AuthenticationRepository authenticationRepository;
    
        @Override
        public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
            String token = IdUtil.simpleUUID();
            authenticationRepository.add(token, authentication);
    
            Result<String> result = Result.data(token, "LOGIN SUCCESS");
            return ServerHttpResponseUtils.print(webFilterExchange.getExchange().getResponse(), result);
        }
    }
    
    @Component
    public class TokenServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
    
        @Override
        public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
            Result<String> result = Result.data(-1, exception.getMessage(), "LOGIN FAILED");
            return ServerHttpResponseUtils.print(webFilterExchange.getExchange().getResponse(), result);
        }
    }
    

    ServerHttpResponseUtils 是封装的一个通过 ServerHttpResponse 向前端响应 JSON 数据格式的工具类,具体代码可查看附上的源码链接。

    5、创建退出成功处理器 TokenServerLogoutSuccessHandler

    @Component
    public class TokenServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {
    
        @Autowired
        private AuthenticationRepository authenticationRepository;
    
        @Override
        public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
            String token = exchange.getExchange().getRequest().getHeaders().getFirst("token");
            if (StrUtil.isNotEmpty(token)) {
                authenticationRepository.delete(token);
            }
    
            Result<String> result = Result.data(200, "LOGOUT SUCCESS", "OK");
            return ServerHttpResponseUtils.print(exchange.getExchange().getResponse(), result);
        }
    }
    

    6、创建无访问权限处理器 TokenServerAccessDeniedHandler

    public class TokenServerAccessDeniedHandler implements ServerAccessDeniedHandler {
    
        @Override
        public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
            Result<String> result = Result.data(403, denied.getMessage(), "ACCESS DENIED");
            return ServerHttpResponseUtils.print(exchange.getResponse(), result);
        }
    }
    

    7、创建 SpringSecurity 上下文仓库 TokenServerSecurityContextRepository

    和 Web 应用集成 SpringSecurity 不同的是,SpringSecurity 暂时没有为 Webflux 提供无状态的 SpringSecurity 上下文存取策略。目前 ServerSecurityContextRepository 接口暂时只有
    NoOpServerSecurityContextRepository(不存储 SecurityContext) 和 WebSessionServerSecurityContextRepository (基于 WebSession)两种实现策略。因此,我们要想实现无状态的
    SpringSecurity 上下文存取,需要我们自己去实现 ServerSecurityContextRepository 接口。源码如下:

    @Component
    public class TokenServerSecurityContextRepository implements ServerSecurityContextRepository {
    
        @Autowired
        private AuthenticationRepository authenticationRepository;
    
        @Override
        public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
            return Mono.empty();
        }
    
        @Override
        public Mono<SecurityContext> load(ServerWebExchange exchange) {
            String token = exchange.getRequest().getHeaders().getFirst("token");
            if (StrUtil.isNotEmpty(token)) {
                Authentication authentication = authenticationRepository.get(token);
                if (ObjectUtil.isNotEmpty(authentication)) {
                    SecurityContextImpl securityContext = new SecurityContextImpl();
                    securityContext.setAuthentication(authentication);
                    return Mono.just(securityContext);
                }
            }
            return Mono.empty();
        }
    }
    

    8、配置 WebFluxSecurityConfig,这是重点!!!

    创建 WebFluxSecurityConfig 类,并配置 SecurityWebFilterChain Bean对象,对于 Webflux 应用 SpringSecurity 是通过 ServerHttpSecurity 配置各项属性,具体配置如下:

    // 【注意】Webflux 中使用的注解是不一样的哦
    @EnableWebFluxSecurity
    @EnableReactiveMethodSecurity
    
    @Bean
    public MapReactiveUserDetailsService userDetailsService() {
        // 权限配置
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("index"));
        authorities.add(new SimpleGrantedAuthority("hasAuthority"));
        authorities.add(new SimpleGrantedAuthority("ROLE_hasRole"));
    
        // 认证信息
        UserDetails userDetails = User.builder().username("admin")
                .passwordEncoder(passwordEncoder()::encode)
                .password("123456")
                .authorities(authorities)
                .build();
        return new MapReactiveUserDetailsService(userDetails);
    }
    
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        // 禁用防止 csrf
        http.csrf(s -> s.disable())
            // 自定义 ServerSecurityContextRepository
            .securityContextRepository(tokenServerSecurityContextRepository)
            .formLogin(s -> s
                // 指定登录请求url
                .loginPage("/login")
                // 配置认证成功处理器
                .authenticationSuccessHandler(tokenServerAuthenticationSuccessHandler)
                // 配置认证失败处理器
                .authenticationFailureHandler(tokenServerAuthenticationFailureHandler)
            )
            // 配置退出成功处理器
            .logout(s -> s.logoutSuccessHandler(tokenServerLogoutSuccessHandler))
            // 放行 /login 请求,其他请求必须经过认证
            .authorizeExchange(s -> s.pathMatchers("/login").permitAll().anyExchange().authenticated())
            // 配置无访问权限处理器
            .exceptionHandling().accessDeniedHandler(new TokenServerAccessDeniedHandler());
        return http.build();
    }
    

    9、创建一些测试用的 API 接口

    @RestController
    public class IndexController {
    
        @RequestMapping(value = "/index")
        @PreAuthorize("hasAuthority('index')")
        public Mono<String> index() {
            return Mono.just("index");
        }
    
        @RequestMapping(value = "/hasAuthority")
        @PreAuthorize("hasAuthority('hasAuthority')")
        public Mono<String> hasAuthority() {
            return Mono.just("hasAuthority");
        }
    
        @RequestMapping(value = "/hasRole")
        @PreAuthorize("hasRole('hasRole')")
        public Mono<String> hasRole() {
            return Mono.just("hasRole");
        }
    
        @RequestMapping(value = "/home")
        @PreAuthorize("hasRole('home')")
        public Mono<String> home() {
            return Mono.just("home");
        }
    
    }
    

    三、测试

    1、未登录访问受保护 API

    // 请求地址 GET请求
    http://localhost:8080/index
    
    // curl
    curl --location --request GET 'http://localhost:8080/index'
    
    // 响应结果
    {
        "code": -1,
        "msg": "NO LOGIN",
        "time": 1654524412270,
        "data": "PLEASE LOGIN"
    }
    

    2、登录 API

    // 请求地址 POST请求 【注意:参数格式要指定为 x-www-form-urlencoded,源码中是通过 getFormData 获取 username 和 password】
    http://localhost:8080/login
    
    // curl
    curl --location --request POST 'http://localhost:8080/login' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'username=admin' \
    --data-urlencode 'password=123456'
    
    // 响应结果
    {
        "code": 200,
        "msg": "LOGIN SUCCESS",
        "time": 1654524449600,
        "data": "80b60ffc7e2b419f9a8e7d8dec355e02"
    }
    

    3、携带 token 访问受保护 API

    // 请求地址 GET请求 请求头中添加认证 token
    http://localhost:8080/index
    
    // curl
    curl --location --request GET 'http://localhost:8080/index' --header 'token: 80b60ffc7e2b419f9a8e7d8dec355e02'
    
    // 响应结果
    index
    

    4、携带 token 访问未授权 API

    // 请求地址 GET请求 请求头中添加认证 token
    http://localhost:8080/home
    
    // curl
    curl --location --request GET 'http://localhost:8080/home' --header 'token: 612c29a2dd824191b6afe07a38285e81'
    
    // 响应结果
    {
        "code": 403,
        "msg": "ACCESS DENIED",
        "time": 1654524759366,
        "data": "Denied"
    }
    

    5、退出 API

    // 请求地址 POST请求 请求头中添加认证 token【注意:是 POST 请求,源码中退出匹配的是 POST 方法 /logout 请求】
    http://localhost:8080/logout
    
    // curl
    curl --location --request POST 'http://localhost:8080/logout' --header 'token: 612c29a2dd824191b6afe07a38285e81'
    
    // 响应结果
    {
        "code": 200,
        "msg": "OK",
        "time": 1654524806801,
        "data": "LOGOUT SUCCESS"
    }
    

    四、总结

    有了基于 Spring Web 集成 SpringSecurity 经验,根据类比的思想,实现 Spring Webflux 集成 SpringSecurity 不算困难。经过简单的改造之后,基本能满足前后端分离无状态 API 权限控制的需求。但是,在应用于生产环境前,有两点需要进一步改造:

    1、将身份认证和权限获取,改为从数据库中获取。

    2、将通过认证的身份信息存储在缓存或数据库中。

    在下一篇,我们将进一步分析 Spring Webflux 集成 SpringSecurity 的实现原理,大家多多关注哦~

    【打个广告】推荐下个人的基于 SpringCloud 开源项目,供大家学习参考,欢迎大家留言进群交流

    Gitee:https://gitee.com/ningzxspace/exam-ning-springcloud-v1

    Github:https://github.com/ningzuoxin/exam-ning-springcloud-v1

  • 相关阅读:
    第二章 C++对C的拓展
    hive 问题解决 Class path contains multiple SLF4J bindings
    作为一名软件测试开发岗的面试官,我都是怎么选人的?
    不理解路径问题的大坑记录
    【月报】Aavegotchi 开发进度更新 - 2023 年 8 月
    解决Docker安装MySQL不区分大小写问题
    网站如何优化加速,让网站降低延迟
    微信小程序商城搭建,微信小程序商城源码,微信小程序商城项目
    搞什么副业可以月入过万?你们觉得抖音小店做起来能赚多少钱?
    人民法院报发文:虚拟货币属合法财产,涉案款物不可一律予以没收或者发还
  • 原文地址:https://www.cnblogs.com/zxning/p/16350147.html