Spring Cloud Gateway 整合Spring Security
做了一个Spring Cloud项目,网关采用 Spring Cloud Gateway,想要用 Spring Security 进行权限校验,由于 Spring Cloud Gateway 采用 webflux ,所以平时用的 mcv 配置是无效的,本文实现了 webflu 下的登陆校验。1. Security配置这里先贴出配置类,方便了解大致情况。其中涉及到的三个处理器均为自定义package
做了一个Spring Cloud项目,网关采用 Spring Cloud Gateway,想要用 Spring Security 进行权限校验,由于 Spring Cloud Gateway 采用 webflux ,所以平时用的 mvc 配置是无效的,本文实现了 webflu 下的登陆校验。
1. Security配置
这里先贴出配置类,方便了解大致情况。
其中涉及到的三个处理器均为自定义
package com.shop.jz.gateway.security.config;
import com.shop.jz.gateway.security.constants.Constants;
import com.shop.jz.gateway.security.handler.AuthenticationFailureHandler;
import com.shop.jz.gateway.security.handler.AuthenticationSuccessHandler;
import com.shop.jz.gateway.security.handler.ShopHttpBasicServerAuthenticationEntryPoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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;
/**
* @author:JZ
* @date:2020/5/21
*/
@Slf4j
@EnableWebFluxSecurity // 开启WebFluxSecurity,必须要添加
public class SecurityConfig {
private String permitUrls = "/gateway/login1,/test1";
/**
* 鉴权成功处理器
*/
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
/**
* 登陆验证失败处理器
*/
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
/**
* 未登录访问资源时的处理类,若无此处理类,前端页面会弹出登录窗口
*/
@Autowired
private ShopHttpBasicServerAuthenticationEntryPoint shopHttpBasicServerAuthenticationEntryPoint;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
log.info("不进行权限校验url:{}", this.permitUrls);
httpSecurity.authorizeExchange()
.pathMatchers(this.permitUrls.split(",")).permitAll()
.anyExchange().authenticated().and()
.httpBasic().and()
.formLogin().loginPage(Constants.LOGIN_URL) // 登陆地址
.authenticationSuccessHandler(authenticationSuccessHandler) // 设置鉴权成功处理器
.authenticationFailureHandler(authenticationFailureHandler) // 设置登陆验证失败处理器
.and().exceptionHandling().authenticationEntryPoint(shopHttpBasicServerAuthenticationEntryPoint)
.and().csrf().disable() // 必须支持跨域
.logout().logoutUrl(Constants.LOGOUT_URL); // 退出登陆地址
return httpSecurity.build();
}
// 密码加密方式
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. 自定义 UserDetails
在Security中用户信息需存放在 UserDetails
中,UserDetails
是一个接口,可以使用Security已经实现的 org.springframework.security.core.userdetails.User
,也可以实现 UserDetails
接口自定义用户信息类。
package com.shop.jz.gateway.security.model;
import com.jz.shop.user.dto.UserDto;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
/**
* @author:JZ
* @date:2020/5/17
*/
@Data
public class LoginUser implements UserDetails {
/**
* token
*/
private String token;
/**
* login time
*/
private Long loginTime;
/**
* expire time
*/
private Long expireTime;
/**
* Login IP address
*/
private String ip;
/**
* location
*/
private String location;
/**
* Browser type
*/
private String browser;
/**
* operating system
*/
private String os;
/**
* 用户名
*/
private String userName;
/**
* 账号密码
*/
private String userPwd;
/**
* 权限列表
*/
private Set<String> permissions;
public LoginUser() {}
public LoginUser(String userName, String userPwd, Set<String> permissions) {
this.userName = userName;
this.userPwd = userPwd;
this.permissions = permissions;
}
public LoginUser getLoginUser() {
return this;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.userPwd;
}
@Override
public String getUsername() {
return this.userName;
}
/**
* Whether the account has not expired, which cannot be verified
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* Specifies whether the user is unlocked. Locked users cannot authenticate
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* Indicates whether the user's credentials (passwords) have expired, which prevents authentication
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* Available, disabled users cannot authenticate
*/
@Override
public boolean isEnabled() {
return true;
}
}
3. 自定义获取用户信息
在 WebFlux 中Security通过调用 ReactiveUserDetailsService
接口的实现类获取用户信息,与 MVC 中的 UserDetailsService
不同。
package com.shop.jz.gateway.security.service;
import com.jz.shop.commons.execptions.BaseException;
import com.jz.shop.user.api.UserApi;
import com.jz.shop.user.dto.UserDto;
import com.shop.jz.gateway.security.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Slf4j
@Service
public class ShopUserDetailsService implements ReactiveUserDetailsService {
@Autowired
private UserApi userApi; // 自定义实现的用户信息查询的feign接口
@Override
public Mono<UserDetails> findByUsername(String username) {
try {
UserDto user = this.userApi.getUserInfoByUsername(username);
LoginUser loginUser = new LoginUser(user.getUserName(), user.getPassword(), null);
return Mono.just(loginUser);
} catch (BaseException baseException) {
log.warn(baseException.getMsg());
}
return Mono.error(new UsernameNotFoundException("User Not Found"));
}
}
4. 鉴权成功处理器
当用户名和密码通过校验后会进入 WebFilterChainServerAuthenticationSuccessHandler
,我们可以重写 onAuthenticationSuccess
方法实现自定义返回信息
package com.shop.jz.gateway.security.handler;
import com.alibaba.fastjson.JSON;
import com.jz.shop.commons.model.Result;
import com.shop.jz.gateway.security.constants.Constants;
import com.shop.jz.gateway.security.model.LoginUser;
import com.shop.jz.gateway.security.service.TokenService;
import com.shop.jz.gateway.security.utils.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
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.WebFilterChainServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
/**
* 鉴权成功处理器
* @author:JZ
* @date:2020/5/21
*/
@Slf4j
@Component
public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {
@Autowired
private TokenService tokenService;
public AuthenticationSuccessHandler() {}
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
log.info("用户:{} 登陆成功");
// 设置返回信息
HttpHeaders headers = response.getHeaders();
headers.add("Content-Type", "application/json; charset=UTF-8");
String responseJson = JSON.toJSONString(Result.success());
DataBuffer dataBuffer = null;
try {
dataBuffer = response.bufferFactory().wrap(responseJson.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return response.writeWith(Mono.just(dataBuffer));
}
}
5. 登陆验证失败处理器
当账号密码或权限验证异常时,会进入该处理器。
package com.shop.jz.gateway.security.handler;
import com.alibaba.fastjson.JSON;
import com.jz.shop.commons.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
/**
* @author:JZ
* @date:2020/5/21
*/
@Slf4j
@Component
public class AuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
log.warn("鉴权失败");
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
// 设置返回信息
HttpHeaders headers = response.getHeaders();
headers.add("Content-Type", "application/json; charset=UTF-8");
String responseJson = JSON.toJSONString(Result.fail("鉴权失败"));
DataBuffer dataBuffer = null;
try {
dataBuffer = response.bufferFactory().wrap(responseJson.getBytes("UTF-8"));
} catch (UnsupportedEncodingException ex) {
ex.printStackTrace();
}
return response.writeWith(Mono.just(dataBuffer));
}
}
6. 未登录访问资源时的处理器
package com.shop.jz.gateway.security.handler;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
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.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author:JZ
* @date:2020/5/21
*/
@Slf4j
@Component
public class ShopHttpBasicServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {
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 ShopHttpBasicServerAuthenticationEntryPoint() {}
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.put("code", "000000");
result.put("msg", "未登录鉴权");
DataBuffer dataBuffer = response.bufferFactory().wrap(result.toJSONString().getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
}
更多推荐
所有评论(0)