前言
之前通过阅读《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流程流转!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。