1.前文引入
2024-5-9更新
更新自定义过滤器
大家可以看我之前的一篇文章,就是一个gateway的简单模版,点击查看=>创建gateway服务
2.整合springsecurity
2.1 前言
我们一般在微服务的开发中,除了网关之外还有一个认证服务。他们两个都需要整合springsecurity。
为啥要这么干:因为springsecurity就是能干好多事,认证(就是账号密码)和授权(就是你能干啥)。然后网关和认证服务就是分别干这两个事的 ,所以相当于拆分了一下springsecurity的功能分别完成一件事 。
网关统一授权:业务服务中每个微服务都需要区分不同的人干不同的事,所以就是放到网关这里进行统一授权。
认证服务就是只需要跟用户打交道,例如:登录获取token,进行修改密码,拿到用户的拥有的角色之类。
这样我们就清楚为啥要两个地方都整合,其实就是分别用了springsecurity的一部分功能
2.2 pom文件
org.springframework.boot spring-boot-starter-securityio.jsonwebtoken jjwt0.9.1
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 Monofilter(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 Monofilter(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 Monofilter(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 Monocommence(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()))); } }
还没有评论,来说两句吧...