网关gateway和springsecurity的整合
我们一般在微服务的开发中,除了网关之外还有一个认证服务。他们两个都需要整合springsecurity。为啥要这么干:因为springsecurity就是能干好多事,认证(就是账号密码)和授权(就是你能干啥)。然后网关和认证服务就是分别干这两个事的 ,所以相当于拆分了一下springsecurity的功能分别完成一件事。网关统一授权:业务服务中每个微服务都需要区分不同的人干不同的事,所以就是放到网
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())));
}
}
更多推荐
所有评论(0)