spring gateway集成spring security


spring gateway

分布式開發時,微服務會有很多,但是網關是請求的第一入口,所以一般會把客戶端請求的權限驗證統一放在網關進行認證與鑒權。SpringCloud Gateway 作為 Spring Cloud 生態系統中的網關,目標是替代 Zuul,為了提升網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。

注意:

由於web容器不同,在gateway項目中使用的webflux,是不能和spring-web混合使用的。

 

Spring MVC和WebFlux的區別
 
11772383-b70d80a3893f3a04.png

依賴:

        <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設置要采用響應式配置,基於WebFlux中WebFilter實現,與Spring MVC的Security是通過Servlet的Filter實現類似,也是一系列filter組成的過濾鏈。

  1. 部分概念是對應的:
Reactive Web
@EnableWebFluxSecurity @EnableWebSecurity
ReactiveSecurityContextHolder SecurityContextHolder
AuthenticationWebFilter FilterSecurityInterceptor
ReactiveAuthenticationManager AuthenticationManager
ReactiveUserDetailsService UserDetailsService
ReactiveAuthorizationManager AccessDecisionManager
  1. 首先需要配置@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<ReactiveAuthenticationManager> managers = new LinkedList<>(); managers.add(authenticationManager); return new DelegatingReactiveAuthenticationManager(managers); } /** * BCrypt密碼編碼 * @return */ @Bean public BCryptPasswordEncoder bcryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } 
  1. 特殊handler的實現
  • JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler
  • JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler
  • JsonServerLogoutSuccessHandler implements ServerLogoutSuccessHandler
  • AuthEntryPointException implements ServerAuthenticationEntryPoint
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)); } } 
  1. 表單登陸時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<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; /** * 存儲用戶信息的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); } } 
  1. 驗證用戶身份
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 */ 
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<UserDetails> 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<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); } } 
  1. 鑒權

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<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("用戶請求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(d -> d.isGranted()) .switchIfEmpty(Mono.defer(() -> { AjaxResult<String> ajaxResult = AjaxResult.restResult("當前用戶沒有訪問權限! ", ApiErrorCode.FAILED); String body = JSONObject.toJSONString(ajaxResult); return Mono.error(new AccessDeniedException(body)); })) .flatMap(d -> Mono.empty()); } }


作者:pilisiyang
鏈接:https://www.jianshu.com/p/acb2c3ec6401
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM