前言
之前通过浏览《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 微服务实战》公布了第二版,有趣味的能够看看。