介绍
Spring-Cloud-Gatewway
Spring Cloud Gateway是基于Spring Boot 2.x,Spring WebFlux和Project Reactor构建的。结果,当您使用Spring Cloud Gateway时,许多您熟悉的同步库(例如,Spring Data和Spring Security)和模式可能不适用。如果您不熟悉这些项目,建议您在使用Spring Cloud Gateway之前先阅读它们的文档以熟悉一些新概念。
Spring-Security
Spring Security是一个提供身份验证,授权和保护以防止常见攻击的框架。凭借对命令式和响应式应用程序的一流支持,它是用于保护基于Spring的应用程序的事实上的标准。
Spring-Webflux
Spring框架中包含的原始Web框架Spring Web MVC是专门为Servlet API和Servlet容器而构建的。响应式堆栈Web框架Spring WebFlux在稍后的5.0版中添加。它是完全无阻塞的,支持 Reactive Streams背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。
这两个Web框架都反映了其源模块的名称(spring-webmvc和 spring-webflux),并在Spring Framework中并存。每个模块都是可选的。应用程序可以使用一个模块,也可以使用两个模块,在某些情况下,也可以使用两个模块,例如,带有react的Spring MVC控制器WebClient
。
注意
由于Web容器不同,在Gateway项目中使用的WebFlux,是不能和Spring-Web混合使用的。
Spring MVC和 WebFlux 的区别:
编码
项目环境版本
- Spring-Cloud:2020.0.1
- Spring-Boot: 2.4.3
gradle 依赖
dependencies {
implementation(
'org.springframework.cloud:spring-cloud-starter-gateway',
'org.springframework.boot:spring-boot-starter-security'
)
}
Spring-Security配置
spring security设置要采用响应式配置,基于WebFlux中WebFilter实现,与Spring MVC的Security是通过Servlet的Filter实现类似,也是一系列filter组成的过滤链。
Reactor与传统MVC配置对应:
webflux | mvc | 作用 |
---|---|---|
@EnableWebFluxSecurity | @EnableWebSecurity | 开启security配置 |
ServerAuthenticationSuccessHandler | AuthenticationSuccessHandler | 登录成功Handler |
ServerAuthenticationFailureHandler | AuthenticationFailureHandler | 登陆失败Handler |
ReactiveAuthorizationManager | AuthorizationManager | 认证管理 |
ServerSecurityContextRepository | SecurityContextHolder | 认证信息存储管理 |
ReactiveUserDetailsService | UserDetailsService | 用户登录 |
ReactiveAuthorizationManager | AccessDecisionManager | 鉴权管理 |
ServerAuthenticationEntryPoint | AuthenticationEntryPoint | 未认证Handler |
ServerAccessDeniedHandler | AccessDeniedHandler | 鉴权失败Handler |
1. Security核心配置
package com.pluto.gateway.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.LinkedList;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 10:56
* @description webflux security核心配置类
*/
@EnableWebFluxSecurity
public class WebfluxSecurityConfig {
@Resource
private DefaultAuthorizationManager defaultAuthorizationManager;
@Resource
private UserDetailsServiceImpl userDetailsServiceImpl;
@Resource
private DefaultAuthenticationSuccessHandler defaultAuthenticationSuccessHandler;
@Resource
private DefaultAuthenticationFailureHandler defaultAuthenticationFailureHandler;
@Resource
private TokenAuthenticationManager tokenAuthenticationManager;
@Resource
private DefaultSecurityContextRepository defaultSecurityContextRepository;
@Resource
private DefaultAuthenticationEntryPoint defaultAuthenticationEntryPoint;
@Resource
private DefaultAccessDeniedHandler defaultAccessDeniedHandler;
/**
* 自定义过滤权限
*/
@Value("${security.noFilter}")
private String noFilter;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
httpSecurity
// 登录认证处理
.authenticationManager(reactiveAuthenticationManager())
.securityContextRepository(defaultSecurityContextRepository)
// 请求拦截处理
.authorizeExchange(exchange -> exchange
.pathMatchers(noFilter).permitAll()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().access(defaultAuthorizationManager)
)
.formLogin()
// 自定义处理
.authenticationSuccessHandler(defaultAuthenticationSuccessHandler)
.authenticationFailureHandler(defaultAuthenticationFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(defaultAuthenticationEntryPoint)
.and()
.exceptionHandling()
.accessDeniedHandler(defaultAccessDeniedHandler)
.and()
.csrf().disable()
;
return httpSecurity.build();
}
/**
* BCrypt密码编码
*/
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* 注册用户信息验证管理器,可按需求添加多个按顺序执行
*/
@Bean
ReactiveAuthenticationManager reactiveAuthenticationManager() {
LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
managers.add(authentication -> {
// 其他登陆方式 (比如手机号验证码登陆) 可在此设置不得抛出异常或者 Mono.error
return Mono.empty();
});
// 必须放最后不然会优先使用用户名密码校验但是用户名密码不对时此 AuthenticationManager 会调用 Mono.error 造成后面的 AuthenticationManager 不生效
managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsServiceImpl));
managers.add(tokenAuthenticationManager);
return new DelegatingReactiveAuthenticationManager(managers);
}
}
2.用户认证
package com.pluto.gateway.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.io.Serializable;
import java.util.Collection;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/10 13:15
* @description 自定义用户信息
*/
public class SecurityUserDetails extends User implements Serializable {
private Long userId;
public SecurityUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Long userId) {
super(username, password, authorities);
this.userId = userId;
}
public SecurityUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, Long userId) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.userId = userId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
package com.pluto.gateway.security;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.ArrayList;
/**
* @author ceshi
* @date 2021/3/9 14:03
* @description 用户登录处理
* @version 1.0.0
*/@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public Mono<UserDetails> findByUsername(String username) {
SecurityUserDetails securityUserDetails = new SecurityUserDetails(
"user",
passwordEncoder.encode("user"),
true, true, true, true, new ArrayList<>(),
1L
);
return Mono.just(securityUserDetails);
}
}
3.1 自定义登录成功Handler
package com.pluto.gateway.security;
import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.utils.JwtTokenUtil;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 15:00
* @description 登录成功处理
*/
@Component
public class DefaultAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
/**
* token 过期时间
*/
@Value("${jwt.token.expired}")
private int jwtTokenExpired;
/**
* 刷新token 时间
*/
@Value("${jwt.token.refresh.expired}")
private int jwtTokenRefreshExpired;
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
return Mono.defer(() -> Mono.just(webFilterExchange.getExchange().getResponse()).flatMap(response -> {
DataBufferFactory dataBufferFactory = response.bufferFactory();
// 生成JWT token
Map<String, Object> map = new HashMap<>(2);
SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal();
map.put("userId", userDetails.getUserId());
map.put("username", userDetails.getUsername());
map.put("roles",userDetails.getAuthorities());
String token = JwtTokenUtil.generateToken(map, userDetails.getUsername(), jwtTokenExpired);
String refreshToken = JwtTokenUtil.generateToken(map, userDetails.getUsername(), jwtTokenRefreshExpired);
Map<String, Object> tokenMap = new HashMap<>(2);
tokenMap.put("token", token);
tokenMap.put("refreshToken", refreshToken);
DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(ResultVoUtil.success(tokenMap)).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}));
}
}
3.2 自定义登录失败Handler
package com.pluto.gateway.security;
import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import com.pluto.common.basic.vo.ResultVO;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Map;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 15:14
* @description 登录失败处理
*/
@Component
public class DefaultAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
return Mono.defer(() -> Mono.just(webFilterExchange.getExchange()
.getResponse()).flatMap(response -> {
DataBufferFactory dataBufferFactory = response.bufferFactory();
ResultVO<Map<String, Object>> resultVO = ResultVoUtil.error();
// 账号不存在
if (exception instanceof UsernameNotFoundException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_NOT_EXIST);
// 用户名或密码错误
} else if (exception instanceof BadCredentialsException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.LOGIN_PASSWORD_ERROR);
// 账号已过期
} else if (exception instanceof AccountExpiredException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_EXPIRED);
// 账号已被锁定
} else if (exception instanceof LockedException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_LOCKED);
// 用户凭证已失效
} else if (exception instanceof CredentialsExpiredException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_CREDENTIAL_EXPIRED);
// 账号已被禁用
} else if (exception instanceof DisabledException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_DISABLE);
}
DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(resultVO).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}));
}
}
3.3 自定义未认证Handler
package com.pluto.gateway.security;
import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 15:17
* @description 未认证处理
*/
@Component
public class DefaultAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
return Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(response -> {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBufferFactory dataBufferFactory = response.bufferFactory();
String result = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.USER_UNAUTHORIZED));
DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
Charset.defaultCharset()));
return response.writeWith(Mono.just(buffer));
});
}
}
3.4 自定义鉴权失败Handler
package com.pluto.gateway.security;
import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 11:12
* @description 鉴权管理
*/
@Component
public class DefaultAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
return Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBufferFactory dataBufferFactory = response.bufferFactory();
String result = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.PERMISSION_DENIED));
DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
Charset.defaultCharset()));
return response.writeWith(Mono.just(buffer));
});
}
}
4.自定义JWT Token认证管理
package com.pluto.gateway.security;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.List;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 16:27
* @description 存储认证授权的相关信息
*/
@Component
public class DefaultSecurityContextRepository implements ServerSecurityContextRepository {
public final static String TOKEN_HEADER = "Authorization";
public final static String BEARER = "Bearer ";
@Resource
private TokenAuthenticationManager tokenAuthenticationManager;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
List<String> headers = request.getHeaders().get(TOKEN_HEADER);
if (!CollectionUtils.isEmpty(headers)) {
String authorization = headers.get(0);
if (StringUtils.isNotEmpty(authorization)) {
String token = authorization.substring(BEARER.length());
if (StringUtils.isNotEmpty(token)) {
return tokenAuthenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(token, null)
).map(SecurityContextImpl::new);
}
}
}
return Mono.empty();
}
}
package com.pluto.gateway.security;
import com.pluto.common.basic.utils.JwtTokenUtil;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Collection;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 13:23
* @description token 认证处理
*/
@Component
@Primary
public class TokenAuthenticationManager implements ReactiveAuthenticationManager {
@Override
@SuppressWarnings("unchecked")
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.map(auth -> JwtTokenUtil.parseJwtRsa256(auth.getPrincipal().toString()))
.map(claims -> {
Collection<? extends GrantedAuthority> roles = (Collection<? extends GrantedAuthority>) claims.get("roles");
return new UsernamePasswordAuthenticationToken(
claims.getSubject(),
null,
roles
);
});
}
}
5.自定义鉴权管理
package com.pluto.gateway.security;
import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;
/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 13:10
* @description 用户权限鉴权处理
*/
@Component
@Slf4j
public class DefaultAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication.map(auth -> {
ServerWebExchange exchange = authorizationContext.getExchange();
ServerHttpRequest request = exchange.getRequest();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (GrantedAuthority authority : authorities) {
String authorityAuthority = authority.getAuthority();
String path = request.getURI().getPath();
// TODO
// 查询用户访问所需角色进行对比
if (antPathMatcher.match(authorityAuthority, path)) {
log.info(String.format("用户请求API校验通过,GrantedAuthority:{%s} Path:{%s} ", authorityAuthority, path));
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}).defaultIfEmpty(new AuthorizationDecision(false));
}
@Override
public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
return check(authentication, object)
.filter(AuthorizationDecision::isGranted)
.switchIfEmpty(Mono.defer(() -> {
String body = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.PERMISSION_DENIED));
return Mono.error(new AccessDeniedException(body));
})).flatMap(d -> Mono.empty());
}
}