• Springboot WebFlux集成Spring Security实现JWT认证


    我最新最全的文章都在 南瓜慢说 www.pkslow.com ,欢迎大家来喝茶!

    1 简介

    在之前的文章《Springboot集成Spring Security实现JWT认证》讲解了如何在传统的Web项目中整合Spring SecurityJWT,今天我们讲解如何在响应式WebFlux项目中整合。二者大体是相同的,主要区别在于Reactive WebFlux与传统Web的区别。

    2 项目整合

    引入必要的依赖:

    <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>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>
    

    2.1 JWT工具类

    该工具类主要功能是创建、校验、解析JWT

    @Component
    public class JwtTokenProvider {
    
        private static final String AUTHORITIES_KEY = "roles";
    
        private final JwtProperties jwtProperties;
    
        private String secretKey;
    
        public JwtTokenProvider(JwtProperties jwtProperties) {
            this.jwtProperties = jwtProperties;
        }
    
        @PostConstruct
        public void init() {
            secretKey = Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes());
        }
    
        public String createToken(Authentication authentication) {
    
            String username = authentication.getName();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            Claims claims = Jwts.claims().setSubject(username);
            if (!authorities.isEmpty()) {
                claims.put(AUTHORITIES_KEY, authorities.stream().map(GrantedAuthority::getAuthority).collect(joining(",")));
            }
    
            Date now = new Date();
            Date validity = new Date(now.getTime() + this.jwtProperties.getValidityInMs());
    
            return Jwts.builder()
                    .setClaims(claims)
                    .setIssuedAt(now)
                    .setExpiration(validity)
                    .signWith(SignatureAlgorithm.HS256, this.secretKey)
                    .compact();
    
        }
    
        public Authentication getAuthentication(String token) {
            Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();
    
            Object authoritiesClaim = claims.get(AUTHORITIES_KEY);
    
            Collection<? extends GrantedAuthority> authorities = authoritiesClaim == null ? AuthorityUtils.NO_AUTHORITIES
                    : AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesClaim.toString());
    
            User principal = new User(claims.getSubject(), "", authorities);
    
            return new UsernamePasswordAuthenticationToken(principal, token, authorities);
        }
    
        public boolean validateToken(String token) {
            try {
                Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
    
                if (claims.getBody().getExpiration().before(new Date())) {
                    return false;
                }
    
                return true;
            } catch (JwtException | IllegalArgumentException e) {
                throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
            }
        }
    
    }
    

    2.2 JWT的过滤器

    这个过滤器的主要功能是从请求中获取JWT,然后进行校验,如何成功则把Authentication放进ReactiveSecurityContext里去。当然,如果没有带相关的请求头,那可能是通过其它方式进行鉴权,则直接放过,让它进入下一个Filter

    public class JwtTokenAuthenticationFilter implements WebFilter {
    
        public static final String HEADER_PREFIX = "Bearer ";
    
        private final JwtTokenProvider tokenProvider;
    
        public JwtTokenAuthenticationFilter(JwtTokenProvider tokenProvider) {
            this.tokenProvider = tokenProvider;
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            String token = resolveToken(exchange.getRequest());
            if (StringUtils.hasText(token) && this.tokenProvider.validateToken(token)) {
                Authentication authentication = this.tokenProvider.getAuthentication(token);
                return chain.filter(exchange)
                        .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
            }
            return chain.filter(exchange);
        }
    
        private String resolveToken(ServerHttpRequest request) {
            String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(HEADER_PREFIX)) {
                return bearerToken.substring(7);
            }
            return null;
        }
    }
    

    2.3 Security的配置

    这里设置了两个异常处理authenticationEntryPointaccessDeniedHandler

    @Configuration
    public class SecurityConfig {
    
        @Bean
        SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http,
                                                    JwtTokenProvider tokenProvider,
                                                    ReactiveAuthenticationManager reactiveAuthenticationManager) {
    
            return http.csrf(ServerHttpSecurity.CsrfSpec::disable)
                    .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
                    .authenticationManager(reactiveAuthenticationManager)
                    .exceptionHandling().authenticationEntryPoint(
                            (swe, e) -> {
                swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return swe.getResponse().writeWith(Mono.just(new DefaultDataBufferFactory().wrap("UNAUTHORIZED".getBytes())));
            })
                    .accessDeniedHandler((swe, e) -> {
                swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                return swe.getResponse().writeWith(Mono.just(new DefaultDataBufferFactory().wrap("FORBIDDEN".getBytes())));
            }).and()
                    .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                    .authorizeExchange(it -> it
                            .pathMatchers(HttpMethod.POST, "/auth/login").permitAll()
                            .pathMatchers(HttpMethod.GET, "/admin").hasRole("ADMIN")
                            .pathMatchers(HttpMethod.GET, "/user").hasRole("USER")
                            .anyExchange().permitAll()
                    )
                    .addFilterAt(new JwtTokenAuthenticationFilter(tokenProvider), SecurityWebFiltersOrder.HTTP_BASIC)
                    .build();
        }
    
    
        @Bean
        public ReactiveAuthenticationManager reactiveAuthenticationManager(CustomUserDetailsService userDetailsService,
                                                                           PasswordEncoder passwordEncoder) {
            UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
            authenticationManager.setPasswordEncoder(passwordEncoder);
            return authenticationManager;
        }
    }
    

    2.4 获取JWT的Controller

    先判断对用户密码进行判断,如果正确则返回对应的权限用户,根据用户生成JWT,再返回给客户端。

    @RestController
    @RequestMapping("/auth")
    public class AuthController {
    
        @Autowired
        ReactiveAuthenticationManager authenticationManager;
    
        @Autowired
        JwtTokenProvider jwtTokenProvider;
    
        @PostMapping("/login")
        public Mono<String> login(@RequestBody AuthRequest request) {
            String username = request.getUsername();
            Mono<Authentication> authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, request.getPassword()));
    
            return authentication.map(auth -> jwtTokenProvider.createToken(auth));
        }
    }
    

    3 总结

    其它与之前的大同小异,不一一讲解了。

    代码请查看:https://github.com/LarryDpk/pkslow-samples


    欢迎关注微信公众号<南瓜慢说>,将持续为你更新...

    多读书,多分享;多写作,多整理。

  • 相关阅读:
    Mybatis用指定的环境Id配置
    Mybatis入门1
    Mybatis入门2把获取SqlSession的方法封装成工具类
    Java读取Resources文件为InputStream的几种方式
    Mybatis开启日志并使用logback
    road map main principle
    IIS使用ftp服务器遇到的问题
    学习中遇到的想法笔记
    latex related info
    2022年8月调试成功的选股公式,跳空缺口不补的公式,公式名字tkgk2
  • 原文地址:https://www.cnblogs.com/larrydpk/p/14942951.html
Copyright © 2020-2023  润新知