企业微信 API 与内部系统集成时的 OAuth2.0 安全上下文传递机制

企业微信 API 与内部系统集成时的 OAuth2.0 安全上下文传递机制

集成场景与安全挑战

在企业内部系统(如 HR、CRM、OA)对接企业微信时,常需通过 OAuth2.0 实现用户身份认证与授权。企业微信作为授权服务器,颁发临时 code,内部系统需用其换取 access_token 与用户信息。然而,在微服务架构下,认证上下文(如用户 ID、部门、权限范围)需在多个服务间安全传递,避免重复鉴权或信息泄露。本文聚焦于基于 JWT 的安全上下文封装与透传机制。

OAuth2.0 授权流程简述

  1. 用户访问内部系统,重定向至企业微信授权页;
  2. 用户同意后,企业微信回调携带 code
  3. 后端用 code + corp_id + secret 换取 access_tokenUserId
  4. 查询用户详细信息,构建安全上下文;
  5. 将上下文注入后续请求链路。

上下文模型定义

java 复制代码
package wlkankan.cn.security;

import java.util.List;

public class WeComSecurityContext {
    private String userId;
    private String openId;
    private String corpId;
    private List<String> departments;
    private long issuedAt;
    private String scope;

    // 构造器、getter/setter 省略
    public static WeComSecurityContext fromWeComUser(wlkankan.cn.model.WecomUser user, String corpId, String scope) {
        WeComSecurityContext ctx = new WeComSecurityContext();
        ctx.userId = user.getUserId();
        ctx.openId = user.getOpenId();
        ctx.corpId = corpId;
        ctx.departments = user.getDepartmentIds();
        ctx.scope = scope;
        ctx.issuedAt = System.currentTimeMillis();
        return ctx;
    }
}

JWT 上下文序列化与验证

使用 HS256 签名确保上下文不可篡改:

java 复制代码
package wlkankan.cn.security.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import wlkankan.cn.security.WeComSecurityContext;

import java.util.Date;

public class WeComJwtTokenService {
    private static final String SECRET_KEY = "your_strong_secret_key_2026";
    private static final String ISSUER = "wlkankan.cn";
    private static final long EXPIRE_MS = 2 * 3600 * 1000; // 2小时

    public String generateToken(WeComSecurityContext ctx) {
        return JWT.create()
            .withIssuer(ISSUER)
            .withSubject(ctx.getUserId())
            .withClaim("corpId", ctx.getCorpId())
            .withClaim("openId", ctx.getOpenId())
            .withClaim("departments", ctx.getDepartments())
            .withClaim("scope", ctx.getScope())
            .withIssuedAt(new Date(ctx.getIssuedAt()))
            .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE_MS))
            .sign(Algorithm.HMAC256(SECRET_KEY));
    }

    public WeComSecurityContext parseToken(String token) {
        try {
            DecodedJWT jwt = JWT.require(Algorithm.HMAC256(SECRET_KEY))
                .withIssuer(ISSUER)
                .build()
                .verify(token);

            WeComSecurityContext ctx = new WeComSecurityContext();
            ctx.setUserId(jwt.getSubject());
            ctx.setCorpId(jwt.getClaim("corpId").asString());
            ctx.setOpenId(jwt.getClaim("openId").asString());
            ctx.setDepartments(jwt.getClaim("departments").asList(String.class));
            ctx.setScope(jwt.getClaim("scope").asString());
            ctx.setIssuedAt(jwt.getIssuedAt().getTime());
            return ctx;
        } catch (JWTVerificationException e) {
            throw new IllegalArgumentException("Invalid or expired token", e);
        }
    }
}

OAuth2.0 回调处理与 Token 注入

java 复制代码
package wlkankan.cn.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import wlkankan.cn.client.WecomApiClient;
import wlkankan.cn.model.WecomUser;
import wlkankan.cn.security.WeComSecurityContext;
import wlkankan.cn.security.jwt.WeComJwtTokenService;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@RestController
public class OAuthCallbackController {

    private final WecomApiClient wecomClient = new wlkankan.cn.client.WecomApiClient();
    private final WeComJwtTokenService tokenService = new wlkankan.cn.security.jwt.WeComJwtTokenService();

    @GetMapping("/oauth/callback")
    public void handleCallback(@RequestParam String code, HttpServletResponse resp) throws IOException {
        // 1. 用 code 换取 access_token 和 UserId
        String accessToken = wecomClient.getAccessToken();
        String userId = wecomClient.getUserIdByCode(accessToken, code);

        // 2. 获取用户详情
        WecomUser user = wecomClient.getUserDetail(accessToken, userId);

        // 3. 构建安全上下文
        WeComSecurityContext ctx = WeComSecurityContext.fromWeComUser(user, "your_corp_id", "snsapi_base");

        // 4. 生成 JWT Token
        String jwt = tokenService.generateToken(ctx);

        // 5. 写入 Cookie 或重定向携带(示例:写 Cookie)
        resp.setHeader("Set-Cookie", "wecom_ctx=" + jwt + "; Path=/; HttpOnly; SameSite=Lax");
        resp.sendRedirect("/dashboard");
    }
}

微服务间上下文透传

在 Feign 或 RestTemplate 调用时,自动附加 X-Wecom-Context 头:

java 复制代码
package wlkankan.cn.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import wlkankan.cn.security.jwt.WeComJwtTokenService;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

public class WeComContextFeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            HttpServletRequest req = attrs.getRequest();
            Cookie[] cookies = req.getCookies();
            if (cookies != null) {
                for (Cookie c : cookies) {
                    if ("wecom_ctx".equals(c.getName())) {
                        // 验证并透传(也可直接透传,由下游验证)
                        template.header("X-Wecom-Context", c.getValue());
                        break;
                    }
                }
            }
        }
    }
}

下游服务解析头并还原上下文:

java 复制代码
package wlkankan.cn.filter;

import org.springframework.web.filter.OncePerRequestFilter;
import wlkankan.cn.security.SecurityContextHolder;
import wlkankan.cn.security.jwt.WeComJwtTokenService;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WeComContextFilter extends OncePerRequestFilter {
    private final WeComJwtTokenService tokenService = new wlkankan.cn.security.jwt.WeComJwtTokenService();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = request.getHeader("X-Wecom-Context");
        if (token != null) {
            try {
                wlkankan.cn.security.WeComSecurityContext ctx = tokenService.parseToken(token);
                SecurityContextHolder.setContext(ctx);
            } catch (Exception e) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        chain.doFilter(request, response);
        SecurityContextHolder.clear();
    }
}

上下文持有器

java 复制代码
package wlkankan.cn.security;

public class SecurityContextHolder {
    private static final ThreadLocal<WeComSecurityContext> context = new ThreadLocal<>();

    public static void setContext(WeComSecurityContext ctx) {
        context.set(ctx);
    }

    public static WeComSecurityContext getContext() {
        return context.get();
    }

    public static void clear() {
        context.remove();
    }
}

该机制确保企业微信认证信息在内部系统中安全、高效传递,同时满足审计与权限控制需求,已在多个大型政企项目中落地验证。

相关推荐
Konata122 小时前
实现进阶的C/S通信
java·开发语言
初听于你2 小时前
Java 泛型详解
java·开发语言·windows·java-ee
rainbow68892 小时前
Java17新特性深度解析
java·开发语言·python
爬山算法2 小时前
Hibernate(79)如何在ETL流程中使用Hibernate?
java·hibernate·etl
rainbow68892 小时前
Java实战:5230台物联网设备时序数据处理方案
java
爬山算法2 小时前
Hibernate(80) 如何在数据迁移中使用Hibernate?
java·oracle·hibernate
子木鑫2 小时前
[SUCTF2019 & GXYCTF2019] 文件上传绕过实战:图片马 + .user.ini / .htaccess 构造 PHP 后门
android·开发语言·安全·php
Elias不吃糖2 小时前
Day1 项目启动记录(KnowledgeDock)
java·springboot·登陆·项目启动
belldeep2 小时前
Java:Tomcat 9, flexmark 0.6 和 mermaid.min.js 10.9 实现 Markdown 中 Mermaid 图表的渲染
java·tomcat·mermaid·flexmark