重新認識Java微服務架構-認證服務

Awbeci發表於2022-12-22

前言

之前透過閱讀《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包中,並且每個微服務都必須加。

如何來封裝呢?其實我程式碼已經寫好了,大家可以參考使用。
image.png
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包
image.png

總結

1、《Spring微服務實戰》是本好書,不過就像hibernate一樣,在國外很火到了國內就有了自己的理解了。
2、《Spring微服務實戰》釋出了第二版,有興趣的可以看看。

相關文章