https://blog.csdn.net/MongolianWolf/article/details/94329980
文章目錄
引言
Spring Security
技術環境
集成步驟
高階用法
集成效果展示
參考資料
引言
由於目前網上大部分spring security的集成都是基於傳統的spring servlet機制,而spring cloud gateway 採用webflux作爲底層web技術支持,不支持servlet,筆者在集成的過程中走了很多彎路,所以特地寫一篇spring cloud gateway和security的集成實踐博客,如有錯誤,歡迎指正。
Spring Security
spring security 爲spring提供了一套web安全性的完整框架,主要包含用戶認證和用戶授權。在用戶認證方面,Spring Security 支持主流的驗證方式,包括HttpBasic、Http表單認證、Http摘要認證、OpenId(如Oauth)和LDAP。本文實現的功能是gateway網關集成security,前端利用form表單進行登陸認證後返回基於一個用戶名和密碼的加密串,後續前端調用其他接口需利用httpbasic攜帶加密串的方式進行認證和授權。
技術環境
jdk 1.8
spring-boot 2.1.4.RELEASE
spring-cloud Greenwich.RELEASE
集成步驟
(1)創建spring boot工程,引入cloud gateway 和security 的jar包依賴,核心依賴包如圖:
注意:cloud gateway 不能和spring-web混合使用,cloud gateway採用的webflux技術,不能再引入spring-web包。
(2)編寫securtiy的核心認證授權配置
如下,創建security的核心安全配置類SecurityConfig並自定義SecurityWebFilterChain,在webflux環境下要生效必須用註解@EnableWebFluxSecurity使其生效:
@EnableWebFluxSecurity
public class SecurityConfig {
//security的鑑權排除的url列表
private static final String[] excludedAuthPages = {
"/auth/login",
"/auth/logout",
"/health",
"/api/socket/**"
};
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers(excludedAuthPages).permitAll() //無需進行權限過濾的請求路徑
.pathMatchers(HttpMethod.OPTIONS).permitAll() //option 請求默認放行
.anyExchange().authenticated()
.and()
.httpBasic()
.and()
.formLogin() //啓動頁面表單登陸,spring security 內置了一個登陸頁面/login
.and().csrf().disable()//必須支持跨域
.logout().disable();
return http.build();
}
}
配置文件中添加以下security的用戶名和密碼,訪問受權限保護的頁面即會進入security的登陸認證頁面,只有輸入配置的用戶名和密碼後才能繼續訪問其他頁面。
#security 配置
spring.security.user.name=admin
spring.security.user.password=123456
配置後,啓動spring boot 程序,輸入需授權的url,則會彈出以下頁面,用戶名密碼輸入登陸成功後即可正常訪問其他受保護頁面
注:此功能爲spring security 內置的formLogin默認基於用戶名和密碼的認證授權(表單登陸)功能,需開啓formLogin()功能。
高階用法
現在項目開發都是前後端分離模式,對於前後端分離security的默認配置則不能滿足認證和授權的需求。下面講解前端通過form的login表單ajax提交給網關security的認證接口,認證成功後security在響應header中返回基於username:password的base64加密串token,後續前端再調用其他接口需基於http basci的安全機制進行授權(即在header中添加Authorization=basic token,spring security在收到請求後通過ServerHttpBasicAuthenticationConverter解析用戶認證信息,決定是否授權通過。
主要修改如下:
(1)自定義用戶認證邏輯
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers(excludedAuthPages).permitAll() //無需進行權限過濾的請求路徑
.pathMatchers(HttpMethod.OPTIONS).permitAll() //option 請求默認放行
.anyExchange().authenticated()
.and()
.httpBasic()
.and()
.formLogin().loginPage("/auth/login")
.authenticationSuccessHandler(authenticationSuccessHandler) //認證成功
.authenticationFailureHandler(authenticationFaillHandler) //登陸驗證失敗
.and().exceptionHandling().authenticationEntryPoint(customHttpBasicServerAuthenticationEntryPoint) //基於http的接口請求鑑權失敗
.and() .csrf().disable()//必須支持跨域
.logout().disable();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); //默認不加密
}
security默認認證響應信息爲text/html,前後端分離一般返回json,此處自定義了認證成功和失敗的響應處理、鑑權失敗時的處理。
認證成功處理器authenticationSuccessHandler,繼承security對gateway支持的認證成功處理器WebFilterChainServerAuthenticationSuccessHandler,並覆蓋其onAuthenticationSuccess方法,本例中認證成功在請求頭中返回Authorization(用戶名和密碼的base加密信息),代碼如下:
@Component
public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication){
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
//設置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//設置body
WsResponse wsResponse = WsResponse.success();
byte[] dataBytes={};
ObjectMapper mapper = new ObjectMapper();
try {
User user=(User)authentication.getPrincipal();
AuthUserDetails userDetails=buildUser(user);
byte[] authorization=(userDetails.getUsername()+":"+userDetails.getPassword()).getBytes();
String token= Base64.getEncoder().encodeToString(authorization);
httpHeaders.add(HttpHeaders.AUTHORIZATION, token);
wsResponse.setResult(userDetails);
dataBytes=mapper.writeValueAsBytes(wsResponse);
}
catch (Exception ex){
ex.printStackTrace();
JsonObject result = new JsonObject();
result.addProperty("status", MessageCode.COMMON_FAILURE.getCode());
result.addProperty("message", "授權異常");
dataBytes=result.toString().getBytes();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
return response.writeWith(Mono.just(bodyDataBuffer));
}
private AuthUserDetails buildUser(User user){
AuthUserDetails userDetails=new AuthUserDetails();
userDetails.setUsername(user.getUsername());
userDetails.setPassword(user.getPassword().substring(user.getPassword().lastIndexOf("}")+1,user.getPassword().length()));
return userDetails;
}
其中AuthUserDetails 爲security維護的用戶信息接口UserDetails的自定義實現類,封裝了用戶賬戶和權限信息.
認證失敗處理器authenticationFaillHandler,實現ServerAuthenticationFailureHandler並覆蓋其onAuthenticationFailure自定義認證失敗的處理邏輯,本例中僅返回認證失敗的響應信息:
@Component
public class AuthenticationFaillHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
//設置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//設置body
WsResponse<String> wsResponse = WsResponse.failure(MessageCode.COMMON_AUTHORIZED_FAILURE);
byte[] dataBytes={};
try {
ObjectMapper mapper = new ObjectMapper();
dataBytes=mapper.writeValueAsBytes(wsResponse);
}
catch (Exception ex){
ex.printStackTrace();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
認證成功後訪問新的接口需在請求頭中添加基於httpbasic的認證鑑權信息,服務端收到請求後通過識別爲httpbasic的鑑權信息,通過ServerHttpBasicAuthenticationConverter提取用戶名和密碼後進行鑑權,鑑權通過放行請求。
此處自定義鑑權失敗時的處理邏輯CustomHttpBasicServerAuthenticationEntryPoint,只需繼承默認的httpbasic鑑權失敗處理器HttpBasicServerAuthenticationEntryPoint並覆蓋其commence方法即可:
@Component
public class CustomHttpBasicServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint /* implements ServerAuthenticationEntryPoint */{
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
private static final String DEFAULT_REALM = "Realm";
private static String WWW_AUTHENTICATE_FORMAT = "Basic realm=\"%s\"";
private String headerValue = createHeaderValue("Realm");
public CustomHttpBasicServerAuthenticationEntryPoint() {
}
public void setRealm(String realm) {
this.headerValue = createHeaderValue(realm);
}
private static String createHeaderValue(String realm) {
Assert.notNull(realm, "realm cannot be null");
return String.format(WWW_AUTHENTICATE_FORMAT, new Object[]{realm});
}
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
response.getHeaders().set(HttpHeaders.AUTHORIZATION, this.headerValue);
JsonObject result = new JsonObject();
result.addProperty("status", MessageCode.COMMON_AUTHORIZED_FAILURE.getCode());
result.addProperty("message", MessageCode.COMMON_AUTHORIZED_FAILURE.getMsg());
byte[] dataBytes=result.toString().getBytes();
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
由於security在認證時必須採用一種密碼加密方式,在security5中默認的BCryptPasswordEncoder是隨機鹽的加密方式,且刪除了原有低版本的md5的encoder,所以此處需配置不加密模式,即NoOpPasswordEncoder,在後續用戶查找邏輯時可添加自定義的用戶密碼加密規則,只需和前端規則一致即可。
(2)定義用戶查找邏輯
security 的認證和授權都離不開系統中的用戶,實際用戶都來自db,本例中採用的是系統配置的默認用戶。
UserDetailsRepositoryReactiveAuthenticationManager作爲security的核心認證管理器,並調用userDetailsService去查找用戶,本集成環境中自定義用戶查找邏輯需實現ReactiveUserDetailsService接口並覆蓋findByUsername(通過用戶名查找用戶)方法,核心代碼如下:
@Component
public class SecurityUserDetailsService implements ReactiveUserDetailsService {
@Value("${spring.security.user.name}")
private String userName;
@Value("${spring.security.user.password}")
private String password;
@Override
public Mono<UserDetails> findByUsername(String username) {
//todo 預留調用數據庫根據用戶名獲取用戶
if(StringUtils.equals(userName,username)){
UserDetails user = User.withUsername(userName)
.password(MD5Encoder.encode(password,username))
.roles("admin").authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("admin"))
.build();
return Mono.just(user);
}
else{
return Mono.error(new UsernameNotFoundException("User Not Found"));
}
}
}
說明:爲避免密碼在系統中明文傳輸,前端傳入的密碼通過md5加鹽username的方式傳入後臺,所以security用戶查找邏輯也需要對配置的密碼做統一的處理,固此處加入了md5加密工具。
(3)其他擴展
security 和webflux的集成核心是AuthenticationWebFilter 過濾器,可查看此過濾器關聯的內部接口自定義邏輯。
httpbasic認證方式的核心配置在ServerHttpSecurity中HttpBasicSpec的configure方法
集成效果展示
1.用戶在前端輸入用戶名和加密後的密碼後以表單方式提交給formlogin認證接口:
可以看到認證成功後響應header中有Authorization信息:
2.訪問新的鑑權的接口只需在header中添加基於Authorization的httpbasic認證信息:
如果輸入錯誤的httpbasic 用戶認證信息:
項目源碼:
https://github.com/DarrenJiang1990/awesome-gateway-securtity
如有錯誤,歡迎指正和交流
參考資料
https://www.jb51.net/article/140429.htm
https://www.naturalprogrammer.com/blog/18149/reactive-spring-security-webflux-rest-web-services
https://www.sudoinit5.com/post/spring-reactive-auth-forms/#customized-webflux-form-authentication
https://blog.csdn.net/Dongguabai/article/details/80932225
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc-webflux
spring security的用戶名密碼驗證規則: https://blog.csdn.net/qq924862077/article/details/83027033
https://github.com/eugenp/tutorials/tree/master/spring-5-reactive-security
————————————————
版權聲明:本文爲CSDN博主「武陵曉生」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/MongolianWolf/article/details/94329980