微服务架构下用户登录问题
在传统的SpringBoot单体应用中,只有一台服务器,直接使用session就可以保存用户登录状态。但是session无法解决不同服务器之间的用户登录状态共享的问题。所以微服务的用户登录问题需要从以下两个方面思考:1.使用什么技术保存用户登录状态?2.微服务之间怎样共享用户信息?
在分享解决办法之前,先说明一下系统架构,看图:
请求通过Gateway进行路由分发,再由A或B服务进行处理。那Gateway和SpringCloud A 和SpringCloud B 三者的信息怎么共享呢?往下看:
①在网关服务中定义过滤器GoalFilter,这是gateway组件中的接口,校验用户是否登录,并将用户信息保存到请求头中
package com.gateway.filter;
import com.gateway.config.AuthProperties;
import com.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author pj
* @date 2024/8/6 11:19
**/
@Component
@RequiredArgsConstructor
public class LoggingFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 排除的路径放行
ServerHttpRequest request = exchange.getRequest();
if (isExcluded(request.getPath().toString())) {
return chain.filter(exchange);
}
// 获取token
HttpHeaders headers = request.getHeaders();
List<String> authorization = headers.get("authorization");
if (CollectionUtils.isEmpty(authorization)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 解析用户信息
String token = authorization.get(0);
Long userId;
try {
userId = jwtTool.parseToken(token);
} catch (Exception e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// TODO 传递用户信息给微服务
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header(("user-info"), userId.toString()))
.build();
return chain.filter(swe);
}
private boolean isExcluded(String path) {
for (String excludePath : authProperties.getExcludePaths()) {
if (antPathMatcher.match(excludePath, path)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
②在业务处理服务中定义SpringMVC提供的拦截器 HandlerInterceptor,接收网关或其他微服务传递过来的用户信息。如果有多个微服务,A和B服务想共用一个拦截器,可以新建一个通用模块,在这个模块里面定义拦截器,A和B再同时依赖于这个模块,使用SpringBoot自动装配,就可以实现拦截器共用了。
package com.hmall.common.interceptor;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author pj
* @date 2024/8/6 12:40
**/
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userInfo = request.getHeader("user-info");
if (StrUtil.isNotEmpty(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();
}
}
// 在配置类中注册拦截器
package com.hmall.common.config;
import com.hmall.common.interceptor.LoggingInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author pj
* @date 2024/8/6 12:51
**/
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 默认拦截所有路径
registry.addInterceptor(new LoggingInterceptor());
}
}
③远程调用接口时实现OpenFeign提供的RequestInterceptor接口,在发送调用请求之前拦截下来,将用户信息保存到请求头中,实现微服务之间的信息共享
package com.hmall.api.interceptor;
import com.hmall.common.utils.UserContext;
import feign.RequestInterceptor;
import feign.RequestTemplate;
/**
* @author pj
* @date 2024/8/6 13:19
**/
public class MyRequestInceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("user-info", UserContext.getUser().toString() );
}
}
// 在配置类中注册拦截器
@Bean
public RequestInterceptor myRequestInterceptor() {
return template -> {
if (UserContext.getUser() != null) {
String userInfo = UserContext.getUser().toString();
template.header("user-info", userInfo);
}
};
}