前言
之前通過閲讀《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包中,並且每個微服務都必須加。
3、如何來封裝呢?
其實我代碼已經寫好了,大家可以參考使用。
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微服務實戰》發佈了第二版,有興趣的可以看看。
---------2023-5-7更新--------
上面的代碼是微服務獲取用户的代碼,還缺少spring-gateway保存用户信息的代碼,如下所示:
// 從redis中獲取token信息
Map<Object, Object> result = globalCache.hmget(session);
String accessToken = result.get("access_token").toString();
HashMap authinfo = getAuthenticationInfo(accessToken);
ObjectMapper mapper = new ObjectMapper();
String authinfoJson = mapper.writeValueAsString(authinfo);
// 注意:這裏保存的key: user,value:userinfo保存到請求頭中供下游微服務獲取,否則獲取用户信息失敗
request.mutate().header(FilterUtils.USER, authinfoJson);
private HashMap getAuthenticationInfo(String authToken) {
JSONObject authinfo = decodeJWT(authToken);
String uid = authinfo.getString("uid");
String userName = authinfo.getString("userName");
String email = authinfo.getString("email");
String avatar = authinfo.getString("avatar");
String mobile = authinfo.getString("mobile");
HashMap user = new HashMap();
user.put("uid", uid);
user.put("userName", userName);
user.put("email", email);
user.put("avatar", avatar);
user.put("mobile", mobile);
// {"user_name":"zhangwei","scope":["all"],"authorities":["ROLE_USER"],"jti":"799ac2d0-0662-4fcd-a567-fa93356362a0","client_id":"test-seaurl"}
return user;
}
---------2023-6-28更新--------
再來更新下流程:
1、用户登錄的時候獲取到jwt token
2、spring-gateway攔截用户的請求接口,並獲取到jwt token並放入到請求頭header['user']中
3、下游微服務UserContextFilter攔截並從請求頭header['user']中取出並解析用户信息到UserContext中
4、下游微服務接口就可以從LoginUser loginUser = UserContext.getUser();中獲取用户信息了
上面就是一整套token流程流轉!