spring gateway
分布式开发时,微服务会有很多,但是网关是请求的第一入口,所以一般会把客户端请求的权限验证统一放在网关进行认证与鉴权。SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
注意:
由于web容器不同,在gateway项目中使用的webflux,是不能和spring-web混合使用的。
Spring MVC和WebFlux的区别
依赖:
org.springframework.cloud
spring-cloud-starter-security
org.springframework.cloud
spring-cloud-starter-gateway
配置spring security
spring security设置要采用响应式配置,基于WebFlux中WebFilter实现,与Spring MVC的Security是通过Servlet的Filter实现类似,也是一系列filter组成的过滤链。
- 部分概念是对应的:
Reactive | Web |
---|---|
@EnableWebFluxSecurity | @EnableWebSecurity |
ReactiveSecurityContextHolder | SecurityContextHolder |
AuthenticationWebFilter | FilterSecurityInterceptor |
ReactiveAuthenticationManager | AuthenticationManager |
ReactiveUserDetailsService | UserDetailsService |
ReactiveAuthorizationManager | AccessDecisionManager |
- 首先需要配置@EnableWebFluxSecurity注解,开启Spring WebFlux Security的支持
import java.util.LinkedList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
/**
* @Author: pilsy
* @Date: 2020/6/29 0029 16:54
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Autowired
private AuthenticationConverter authenticationConverter;
@Autowired
private AuthorizeConfigManager authorizeConfigManager;
@Autowired
private AuthEntryPointException serverAuthenticationEntryPoint;
@Autowired
private JsonServerAuthenticationSuccessHandler jsonServerAuthenticationSuccessHandler;
@Autowired
private JsonServerAuthenticationFailureHandler jsonServerAuthenticationFailureHandler;
@Autowired
private JsonServerLogoutSuccessHandler jsonServerLogoutSuccessHandler;
@Autowired
private AuthenticationManager authenticationManager;
private static final String[] AUTH_WHITELIST = new String[]{"/login", "/logout"};
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
SecurityWebFilterChain chain = http.formLogin()
.loginPage("/login")
// 登录成功handler
.authenticationSuccessHandler(jsonServerAuthenticationSuccessHandler)
// 登陆失败handler
.authenticationFailureHandler(jsonServerAuthenticationFailureHandler)
// 无访问权限handler
.authenticationEntryPoint(serverAuthenticationEntryPoint)
.and()
.logout()
// 登出成功handler
.logoutSuccessHandler(jsonServerLogoutSuccessHandler)
.and()
.csrf().disable()
.httpBasic().disable()
.authorizeExchange()
// 白名单放行
.pathMatchers(AUTH_WHITELIST).permitAll()
// 访问权限控制
.anyExchange().access(authorizeConfigManager)
.and().build();
// 设置自定义登录参数转换器
chain.getWebFilters()
.filter(webFilter -> webFilter instanceof AuthenticationWebFilter)
.subscribe(webFilter -> {
AuthenticationWebFilter filter = (AuthenticationWebFilter) webFilter;
filter.setServerAuthenticationConverter(authenticationConverter);
});
return chain;
}
/**
* 注册用户信息验证管理器,可按需求添加多个按顺序执行
* @return
*/
@Bean
ReactiveAuthenticationManager reactiveAuthenticationManager() {
LinkedList managers = new LinkedList<>();
managers.add(authenticationManager);
return new DelegatingReactiveAuthenticationManager(managers);
}
/**
* BCrypt密码编码
* @return
*/
@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 特殊handler的实现
- JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import io.netty.util.CharsetUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
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;
/**
* @Author: pilsy
* @Date: 2020/6/29 0029 17:39
*/
@Component
public class JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
@Override
public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
// 登录成功后可以放入一些参数到session中
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
String body = JSONObject.toJSONString(AjaxResult.ok("登录成功!"));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(CharsetUtil.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
- JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
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;
/**
* @Author: pilsy
* @Date: 2020/6/29 0029 17:44
*/
@Component
public class JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
private static final String USER_NOT_EXISTS = "用户不存在!";
private static final String USERNAME_PASSWORD_ERROR = "用户密码错误!";
private static final String USER_LOCKED = "用户锁定!";
@Override
public Mono onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
if (exception instanceof UsernameNotFoundException) {
return writeErrorMessage(response, USER_NOT_EXISTS);
} else if (exception instanceof BadCredentialsException) {
return writeErrorMessage(response, USERNAME_PASSWORD_ERROR);
} else if (exception instanceof LockedException) {
return writeErrorMessage(response, USER_LOCKED);
}
return writeErrorMessage(response, exception.getMessage());
}
private Mono writeErrorMessage(ServerHttpResponse response, String message) {
String result = JSONObject.toJSONString(AjaxResult.restResult(message, ApiErrorCode.FAILED));
DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
- JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import reactor.core.publisher.Mono;
/**
* @Author: pilsy
* @Date: 2020/7/10 0010 15:05
*/
@Component
public class JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {
@Override
public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
ServerHttpResponse response = exchange.getExchange().getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
String result = JSONObject.toJSONString(AjaxResult.restResult("注销成功", ApiErrorCode.SUCCESS));
DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(CharsetUtil.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
- AuthEntryPointException implements ServerAuthenticationEntryPoint
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
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.Flux;
import reactor.core.publisher.Mono;
/**
* 无访问权限的返回结果
*
* @author pilsy
*/
@Component
public class AuthEntryPointException implements ServerAuthenticationEntryPoint {
@Override
public Mono commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8");
AjaxResult ajaxResult = AjaxResult.restResult(e.getMessage(), ApiErrorCode.FAILED);
String body = JSONObject.toJSONString(ajaxResult);
DataBuffer wrap = exchange.getResponse().bufferFactory().wrap(body.getBytes(CharsetUtil.UTF_8));
return exchange.getResponse().writeWith(Flux.just(wrap));
}
}
- 表单登陆时security默认只会获取了username,password参数,但有时候需要一些特殊属性,所以需要覆盖默认获取的表单参数的Converter
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 将表单参数转换为AuthenticationToken
*
* @Author: pilsy
* @Date: 2020/7/15 0015 15:41
*/
@Component
public class AuthenticationConverter extends ServerFormLoginAuthenticationConverter {
private String usernameParameter = "username";
private String passwordParameter = "password";
@Override
public Mono convert(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
String tenant = headers.getFirst("_tenant");
String host = headers.getHost().getHostName();
return exchange.getFormData()
.map(data -> {
String username = data.getFirst(this.usernameParameter);
String password = data.getFirst(this.passwordParameter);
return new AuthenticationToken(username, password, tenant, host);
});
}
}
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 存储用户信息的token
*
* @Author: pilsy
* @Date: 2020/7/15 0015 16:08
*/
@SuppressWarnings("serial")
@Getter
@Setter
public class AuthenticationToken extends UsernamePasswordAuthenticationToken {
private String tenant;
private String host;
public AuthenticationToken(Object principal, Object credentials, String tenant, String host) {
super(principal, credentials);
this.tenant = tenant;
this.host = host;
}
public AuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public AuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
- 验证用户身份
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
/**
* 验证用户
*
* @Author: pilsy
* @Date: 2020/7/15 0015 16:43
*/
@Component
public class AuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
private Scheduler scheduler = Schedulers.boundedElastic();
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Autowired
private MySqlReactiveUserDetailsServiceImpl mySqlReactiveUserDetailsService;
@Override
public Mono authenticate(Authentication authentication) {
AuthenticationToken token = (AuthenticationToken) authentication;
final String username = authentication.getName();
final String presentedPassword = (String) authentication.getCredentials();
final String tenant = token.getTenant();
final String host = token.getHost();
return retrieveUser(username)
.publishOn(scheduler)
.filter(u -> passwordEncoder.matches(presentedPassword, u.getPassword()))
.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
.flatMap(u -> {
boolean upgradeEncoding = mySqlReactiveUserDetailsService != null
&& passwordEncoder.upgradeEncoding(u.getPassword());
if (upgradeEncoding) {
String newPassword = passwordEncoder.encode(presentedPassword);
return mySqlReactiveUserDetailsService.updatePassword(u, newPassword);
}
return Mono.just(u);
})
.flatMap(userDetails -> {
// 省略业务代码
return Mono.just(userDetails);
})
.map(u -> new AuthenticationToken(u, u.getPassword(), u.getAuthorities()));
}
@Override
protected Mono retrieveUser(String username) {
return mySqlReactiveUserDetailsService.findByUsername(username);
}
}
import com.gsoft.foa.gateway.repository.AccountInfoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* 身份认证类
*
* @Author: pilsy
* @Date: 2020/6/29 0029 18:01
*/
@Slf4j
@Component
public class MySqlReactiveUserDetailsServiceImpl implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
private static final String USER_NOT_EXISTS = "用户不存在!";
private final AccountInfoRepository accountInfoRepository;
public MySqlReactiveUserDetailsServiceImpl(AccountInfoRepository accountInfoRepository) {
this.accountInfoRepository = accountInfoRepository;
}
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public Mono findByUsername(String username) {
return accountInfoRepository.findByUsername(username)
.switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
.doOnNext(u -> log.info(
String.format("查询账号成功 user:%s password:%s", u.getUsername(), u.getPassword())))
.cast(UserDetails.class);
}
@Override
public Mono updatePassword(UserDetails user, String newPassword) {
return accountInfoRepository.findByUsername(user.getUsername())
.switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
.map(foundedUser -> {
foundedUser.setPassword(bCryptPasswordEncoder.encode(newPassword));
return foundedUser;
})
.flatMap(updatedUser -> accountInfoRepository.save(updatedUser))
.cast(UserDetails.class);
}
}
- 鉴权
import com.alibaba.fastjson.JSONObject;
import com.gsoft.foa.common.dto.AjaxResult;
import com.gsoft.foa.common.dto.ApiErrorCode;
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;
/**
* API请求权限校验配置类
*
* @Author: pilsy
* @Date: 2020/7/1 0001 18:27
*/
@Slf4j
@Component
public class AuthorizeConfigManager implements ReactiveAuthorizationManager {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono check(Mono 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();
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 verify(Mono authentication, AuthorizationContext object) {
return check(authentication, object)
.filter(d -> d.isGranted())
.switchIfEmpty(Mono.defer(() -> {
AjaxResult ajaxResult = AjaxResult.restResult("当前用户没有访问权限! ", ApiErrorCode.FAILED);
String body = JSONObject.toJSONString(ajaxResult);
return Mono.error(new AccessDeniedException(body));
}))
.flatMap(d -> Mono.empty());
}
}