前言
之前透過閱讀《Spring微服務實戰》寫過關於spring-cloud+spring-security+oauth2的認證服務和資源服務文章,以及寫過關於spring-gateway做token校驗的文章,但是在實戰過程中還是發現一些問題,於是透過跟朋友溝通收穫了不了新知識,之前的框架設計有問題,想透過這篇文章重新梳理下校驗和認證流程。
遇到的問題
1、Feign呼叫問題:之前所有微服務都做成了資源服務,這樣feign呼叫的時候還要校驗token,影響執行效率
2、Gateway閘道器問題:spring-gateway校驗了token並把token透過authorization做為請求頭下發到下游微服務,下游服務又校驗了一遍token,影響執行效率
3、全域性資訊問題:如獲取使用者資訊,微服務api介面透過OAuth2Authentication
獲取使用者名稱,再透過UserService
獲取使用者資訊,這樣做再次降低執行效率
如何去解決?
綜合上面三點問題,提出了相對應的解決方案:
1、微服務不需要做成資源服務(不需要校驗authorization),微服務的許可權還有統一處理啥的都在閘道器裡做,這樣feign呼叫的時候也就不需要校驗token了。
2、上面說過微服務已經不是資源服務,那麼也不存在再次檢驗token的問題了,雖然如此,但是你可以透過spring-gateway來做統一授權達到控制外界的訪問。
3、spring-gateway校驗token和封裝使用者資訊到請求頭header中,下游服務透過header中的使用者資訊統一儲存到Context中
注意:
這裡有個問題:
A服務有個Controller方法叫saveUserEvent
,feign透過/gateway-name/a/saveUserEvent
路由呼叫(feign呼叫api介面的時候不存在token校驗問題),但是沒有了資源服務的token限制,外面當然也可以透過gateway呼叫這個介面,所以這裡遇到的問題就是:如何既保證feign的順利呼叫又不能讓外面請求呼叫呢?
針對這個問題答案是:透過gateway的路由黑名單把不想暴露給外面的api介面排除到路由外面,這樣即保證了外面就再也請求不到這個介面了,又保證了服務內透過feign呼叫的資料安全性。
操作
針對上面的三個問題,我們來重新架構一下我們的微服務。
1、spring-gateway認證服務操作流程
cookie和spring-gateway結合做使用者認證服務,這個是透過cookie和set-cookie來達到token傳遞的效果,後面單獨寫一篇文章講解。
2、封裝使用者資訊到Context中
注意:封裝好的Context可以單獨放到context包中,並且每個微服務都必須加。
如何來封裝呢?其實我程式碼已經寫好了,大家可以參考使用。
UserContext.java
@Component
public class UserContext {
public static final String CORRELATION_ID = "correlation-id";
public static final String AUTH_TOKEN = "authorization";
public static final String USER = "user";
private static final ThreadLocal<String> correlationId = new ThreadLocal<String>();
private static final ThreadLocal<String> authToken = new ThreadLocal<String>();
private static final ThreadLocal<LoginUser> user = new ThreadLocal<>();
public static String getCorrelationId() {
return correlationId.get();
}
public static void setCorrelationId(String cid) {
correlationId.set(cid);
}
public static String getAuthToken() {
return authToken.get();
}
public static void setAuthToken(String token) {
authToken.set(token);
}
public static LoginUser getUser() {
return user.get();
}
public static void setUser(LoginUser u) {
user.set(u);
}
public static HttpHeaders getHttpHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(CORRELATION_ID, getCorrelationId());
return httpHeaders;
}
}
UserContextFilter.java
@Component
public class UserContextFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
ObjectMapper mapper = new ObjectMapper();
String userJson = httpServletRequest.getHeader(UserContext.USER);
if(StringUtils.hasLength(userJson)){
LoginUser userMap = mapper.readValue(userJson, LoginUser.class);
UserContextHolder.getContext().setUser(userMap);
}
UserContextHolder.getContext().setCorrelationId( httpServletRequest.getHeader(UserContext.CORRELATION_ID) );
UserContextHolder.getContext().setAuthToken( httpServletRequest.getHeader(UserContext.AUTH_TOKEN) );
logger.debug("---Incoming Correlation id: {}---" ,UserContextHolder.getContext().getCorrelationId());
// logger.debug("---Incoming Authorization token: {}---" ,UserContextHolder.getContext().getAuthToken());
filterChain.doFilter(httpServletRequest, servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
UserContextHolder.java
public class UserContextHolder {
private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
public static final UserContext getContext(){
UserContext context = userContext.get();
if (context == null) {
context = createEmptyContext();
userContext.set(context);
}
return userContext.get();
}
public static final void setContext(UserContext context) {
Assert.notNull(context, "Only non-null UserContext instances are permitted");
userContext.set(context);
}
public static final UserContext createEmptyContext(){
return new UserContext();
}
}
UserContextInterceptor.java
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(UserContextInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
LoginUser user = UserContextHolder.getContext().getUser();
ObjectMapper mapper = new ObjectMapper();
String userInfo = mapper.writeValueAsString(user);
headers.add(UserContext.USER, userInfo);
return execution.execute(request, body);
}
}
程式碼就這麼多了,接下來我們看下如何使用?
XxxController.java
public ResponseEntity<?> addLikeUrl(){
LoginUser loginUser = UserContext.getUser();
if (loginUser == null) {
return ResponseEntity.ok(new ResultInfo<>(ResultStatus.USER_NOT_FOUND));
}
}
這裡的UserContext.getUser
方法就可以獲取到全域性的登入的使用者了。
3、如果你之前參考了我上面的兩篇文章構建了認證和資源服務,那麼你現在可以把之前的程式碼去掉了,否則略過該過程。
3.1、去掉@EnableResourceServer
@SpringBootApplication
// 資源保護服務
@EnableResourceServer
// 服務發現
@EnableDiscoveryClient
// 啟用feign
@EnableFeignClients
@RefreshScope
public class AccountServiceApplication {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class,args);
}
}
3.2、去掉spring-cloud-security和spring-cloud-starter-oauth2
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
3、刪除security包
總結
1、《Spring微服務實戰》是本好書,不過就像hibernate一樣,在國外很火到了國內就有了自己的理解了。
2、《Spring微服務實戰》釋出了第二版,有興趣的可以看看。