1.前文引入

        2024-5-9更新

        更新自定义过滤器

        大家可以看我之前的一篇文章,就是一个gateway的简单模版,点击查看=>创建gateway服务

2.整合springsecurity

 2.1 前言

       我们一般在微服务的开发中,除了网关之外还有一个认证服务。他们两个都需要整合springsecurity。

        为啥要这么干:因为springsecurity就是能干好多事,认证(就是账号密码)和授权(就是你能干啥)。然后网关和认证服务就是分别干这两个事的 ,所以相当于拆分了一下springsecurity的功能分别完成一件事 。

       网关统一授权:业务服务中每个微服务都需要区分不同的人干不同的事,所以就是放到网关这里进行统一授权

        认证服务就是只需要跟用户打交道,例如:登录获取token,进行修改密码,拿到用户的拥有的角色之类。

        这样我们就清楚为啥要两个地方都整合,其实就是分别用了springsecurity的一部分功能

2.2 pom文件

<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.3  自定义过滤器

        这个地方需要做一些简单的说明

1,千万别被spring管理,不然他会直接加载执行。正常的情况是这个过滤器在springconfig配置管理

2.就是如果你仅仅在http中设置了放行路径,那么你自定义的过滤器还是会执行。这个http的放行执行在springsecurity的认证和授权过滤器放行了,然后过滤器链条中还是会有你的过滤器,还是会执行。所以你还需要在过滤请求头的时候如果没有token就放行的判断.

3.如果想直接跳过所有的过滤校验可以通过:web.ignoring()这种配置来跳过。戳web和http的区别

那什么时候用web跳过,什么时候用http放行呢? 

4.自定义的过滤器  JwtAuthenticationFilter

        其中更新的部分是校验完token之后的流程,这里我们自定义的过滤器通常是在springsecurity之前,那么我们通过token校验完之后如果告知后续的过滤器我们已经通过token认证过了呢?

       网上通常都是这个,但是这个在网关这种WebFlux他就不好使了

SecurityContextHolder.getContext().setAuthentication(authentication);

这段代码进行往下传入认证的auth这个。放到一个threadlocal里面,然后后续过滤器就可以执行 

所以我有找了一个方式就是securityContextRepository。这个需要先注入再使用

public class JwtAuthenticationFilter implements WebFilter {
    Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
    private JwtTokenProvider jwtTokenProvider;
    ServerSecurityContextRepository securityContextRepository;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, ServerSecurityContextRepository securityContextRepository) {
        logger.info("========= JwtAuthenticationFilter init =========");
        this.jwtTokenProvider = jwtTokenProvider;
        this.securityContextRepository = securityContextRepository;
        logger.info("this.jwtTokenProvider ============> {}", this.jwtTokenProvider);
        logger.info("this.securityContextRepository ============> {}", this.securityContextRepository);
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        URI uri = exchange.getRequest().getURI();
        logger.info("===========JwtAuthenticationFilter=====uri===>{}", uri);
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(token)) {
            //之所以有这个地方,是因为http的放行执行放过了springsecurity的认证和授权的校验,但是他依然会走过滤器链,就会走到这个地方
            //所以我们要把自定义放行在这儿也放行
            return chain.filter(exchange);
        }
        if (jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsername(token);
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
//            SecurityContextHolder.getContext().setAuthentication(auth);
            SecurityContextImpl securityContext = new SecurityContextImpl(auth);
//            return chain.filter(exchange);
            return securityContextRepository.save(exchange, securityContext)
                    .then(chain.filter(exchange));
        } else {
            throw new RuntimeException("token is error or expire");
        }
    }
}

5.jwttoken校验的类 

package com.zs.provider;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Service;

@Service
public class JwtTokenProvider {
    private String secretKey = "secret";

    public String getUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            throw new RuntimeException("Expired or invalid JWT token");
        }
    }
}

securityconfig类

修改这个加入自定义过滤器的方法,因为我们在自定义过滤器中使用了securityContextRepository,所以我们需要在初始化的时候加入它

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
    Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    @Autowired
    private ServerSecurityContextRepository securityContextRepository;

    @Bean
    public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
        logger.info("========SecurityWebFilterChain create========");
        /**
         * 这里的这个放行只是跳过了springsecurity的权限校验。但是依然会执行自定义的过滤器。
         */
        return http
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers("/gateway/account/account/**").permitAll()
                .anyExchange().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()
                .addFilterAt(jwtAuthenticationFilter(), SecurityWebFiltersOrder.HTTP_BASIC)
                .build();
    }
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter(jwtTokenProvider, securityContextRepository);
    }
}

然后就是这个类需要交由spring管理。把这段代码扔到启动类里就行了 

@Bean
    public ServerSecurityContextRepository securityContextRepository() {
        return new WebSessionServerSecurityContextRepository();
    }

在网关这部分解决跨域问题的过滤器,这部分是抄的 ‘’芋道‘’

/**
 * 跨域 Filter
 *
 * @author 芋道源码
 */
@Component
public class CorsFilter implements WebFilter {

    private static final String ALL = "*";
    private static final String MAX_AGE = "3600L";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 非跨域请求,直接放行
        ServerHttpRequest request = exchange.getRequest();
        if (!CorsUtils.isCorsRequest(request)) {
            return chain.filter(exchange);
        }

        // 设置跨域响应头
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders headers = response.getHeaders();
        headers.add("Access-Control-Allow-Origin", ALL);
        headers.add("Access-Control-Allow-Methods", ALL);
        headers.add("Access-Control-Allow-Headers", ALL);
        headers.add("Access-Control-Max-Age", MAX_AGE);
        if (request.getMethod() == HttpMethod.OPTIONS) {
            response.setStatusCode(HttpStatus.OK);
            return Mono.empty();
        }
        return chain.filter(exchange);
    }

}

一个通用性的过滤器,gateway带的全局过滤器

@Component
public class LogGatewayFilter implements GlobalFilter, Ordered {
    Logger logger = LoggerFactory.getLogger(LogGatewayFilter.class);
    @Override
    public int getOrder() {
        // 数值越小,越先执行
        return Integer.MIN_VALUE;
    }
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        HttpMethod method = request.getMethod();
        String uri = request.getPath().pathWithinApplication().value();
        logger.info("===访问方法==method=>{}\n==访问uri=>{}", method, uri);
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            HttpStatus statusCode = response.getStatusCode();
            logger.info("response statusCode ==>{}", statusCode);
        }));
    }
}

      因为我们要做的是前后端分离的项目,所以我认为就是访问没有权限接口应该给一个报错信息,所以就是我们要取消掉springsecurity自带的没认证就跳转登录页的功能。

        这部分写好之后需要再securityconfig添加一下。咱们上面的代码已经添加了,就是这段代码

and().exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())


public class CustomAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String errorResponse = "{\"error\": \"Unauthorized access\", \"message\": \"" + ex.getMessage() + "\"}";
        return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(errorResponse.getBytes())));
    }
}

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐