乐趣区

关于spring-security:重新认识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<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 微服务实战》公布了第二版,有趣味的能够看看。

退出移动版