spring gateway
分布式開發(fā)時(shí)储耐,微服務(wù)會(huì)有很多揍诽,但是網(wǎng)關(guān)是請(qǐng)求的第一入口籽慢,所以一般會(huì)把客戶端請(qǐng)求的權(quán)限驗(yàn)證統(tǒng)一放在網(wǎng)關(guān)進(jìn)行認(rèn)證與鑒權(quán)酸役。SpringCloud Gateway 作為 Spring Cloud 生態(tài)系統(tǒng)中的網(wǎng)關(guān)住诸,目標(biāo)是替代 Zuul,為了提升網(wǎng)關(guān)的性能涣澡,SpringCloud Gateway是基于WebFlux框架實(shí)現(xiàn)的贱呐,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
注意:
由于web容器不同入桂,在gateway項(xiàng)目中使用的webflux奄薇,是不能和spring-web混合使用的。
Spring MVC和WebFlux的區(qū)別
依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
配置spring security
spring security設(shè)置要采用響應(yīng)式配置事格,基于WebFlux中WebFilter實(shí)現(xiàn)惕艳,與Spring MVC的Security是通過(guò)Servlet的Filter實(shí)現(xiàn)類似搞隐,也是一系列filter組成的過(guò)濾鏈。
- 部分概念是對(duì)應(yīng)的:
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)
// 無(wú)訪問(wèn)權(quán)限handler
.authenticationEntryPoint(serverAuthenticationEntryPoint)
.and()
.logout()
// 登出成功handler
.logoutSuccessHandler(jsonServerLogoutSuccessHandler)
.and()
.csrf().disable()
.httpBasic().disable()
.authorizeExchange()
// 白名單放行
.pathMatchers(AUTH_WHITELIST).permitAll()
// 訪問(wèn)權(quán)限控制
.anyExchange().access(authorizeConfigManager)
.and().build();
// 設(shè)置自定義登錄參數(shù)轉(zhuǎn)換器
chain.getWebFilters()
.filter(webFilter -> webFilter instanceof AuthenticationWebFilter)
.subscribe(webFilter -> {
AuthenticationWebFilter filter = (AuthenticationWebFilter) webFilter;
filter.setServerAuthenticationConverter(authenticationConverter);
});
return chain;
}
/**
* 注冊(cè)用戶信息驗(yàn)證管理器劣纲,可按需求添加多個(gè)按順序執(zhí)行
* @return
*/
@Bean
ReactiveAuthenticationManager reactiveAuthenticationManager() {
LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
managers.add(authenticationManager);
return new DelegatingReactiveAuthenticationManager(managers);
}
/**
* BCrypt密碼編碼
* @return
*/
@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 特殊handler的實(shí)現(xiàn)
- 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<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
// 登錄成功后可以放入一些參數(shù)到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 = "用戶密碼錯(cuò)誤癞季!";
private static final String USER_LOCKED = "用戶鎖定!";
@Override
public Mono<Void> 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<Void> 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<Void> 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;
/**
* 無(wú)訪問(wèn)權(quán)限的返回結(jié)果
*
* @author pilsy
*/
@Component
public class AuthEntryPointException implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> 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<String> 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));
}
}
- 表單登陸時(shí)security默認(rèn)只會(huì)獲取了username倘潜,password參數(shù)绷柒,但有時(shí)候需要一些特殊屬性,所以需要覆蓋默認(rèn)獲取的表單參數(shù)的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;
/**
* 將表單參數(shù)轉(zhuǎn)換為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<Authentication> 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;
/**
* 存儲(chǔ)用戶信息的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);
}
}
- 驗(yàn)證用戶身份
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;
/**
* 驗(yàn)證用戶
*
* @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<Authentication> 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 -> {
// 省略業(yè)務(wù)代碼
return Mono.just(userDetails);
})
.map(u -> new AuthenticationToken(u, u.getPassword(), u.getAuthorities()));
}
@Override
protected Mono<UserDetails> 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;
/**
* 身份認(rèn)證類
*
* @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<UserDetails> findByUsername(String username) {
return accountInfoRepository.findByUsername(username)
.switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(USER_NOT_EXISTS))))
.doOnNext(u -> log.info(
String.format("查詢賬號(hào)成功 user:%s password:%s", u.getUsername(), u.getPassword())))
.cast(UserDetails.class);
}
@Override
public Mono<UserDetails> 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);
}
}
- 鑒權(quán)
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請(qǐng)求權(quán)限校驗(yàn)配置類
*
* @Author: pilsy
* @Date: 2020/7/1 0001 18:27
*/
@Slf4j
@Component
public class AuthorizeConfigManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private 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();
if (antPathMatcher.match(authorityAuthority, path)) {
log.info(String.format("用戶請(qǐng)求API校驗(yàn)通過(guò)废睦,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(d -> d.isGranted())
.switchIfEmpty(Mono.defer(() -> {
AjaxResult<String> ajaxResult = AjaxResult.restResult("當(dāng)前用戶沒(méi)有訪問(wèn)權(quán)限! ", ApiErrorCode.FAILED);
String body = JSONObject.toJSONString(ajaxResult);
return Mono.error(new AccessDeniedException(body));
}))
.flatMap(d -> Mono.empty());
}
}