博客 / 詳情

返回

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包中,並且每個微服務都必須加。

3、如何來封裝呢?
其實我代碼已經寫好了,大家可以參考使用。

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微服務實戰》發佈了第二版,有興趣的可以看看。

---------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流程流轉!

user avatar eisuto 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.