重新认识Java微服务架构-认证服务

前言

之前通过阅读《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 correlationId = new ThreadLocal();
    private static final ThreadLocal authToken = new ThreadLocal();
    private static final ThreadLocal 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 = new ThreadLocal();

    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


    org.springframework.cloud
    spring-cloud-security
    2.2.5.RELEASE


    org.springframework.cloud
    spring-cloud-starter-oauth2
    2.2.5.RELEASE

3、删除security包

总结

1、《Spring微服务实战》是本好书,不过就像hibernate一样,在国外很火到了国内就有了自己的理解了。
2、《Spring微服务实战》发布了第二版,有兴趣的可以看看。

你可能感兴趣的:(重新认识Java微服务架构-认证服务)