前言

之前通过阅读《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流程流转!


Awbeci
3.1k 声望213 粉丝

Awbeci