企业微信 API 与内部系统集成时的 OAuth2.0 安全上下文传递机制
集成场景与安全挑战
在企业内部系统(如 HR、CRM、OA)对接企业微信时,常需通过 OAuth2.0 实现用户身份认证与授权。企业微信作为授权服务器,颁发临时 code,内部系统需用其换取 access_token 与用户信息。然而,在微服务架构下,认证上下文(如用户 ID、部门、权限范围)需在多个服务间安全传递,避免重复鉴权或信息泄露。本文聚焦于基于 JWT 的安全上下文封装与透传机制。
OAuth2.0 授权流程简述
- 用户访问内部系统,重定向至企业微信授权页;
- 用户同意后,企业微信回调携带
code; - 后端用
code + corp_id + secret换取access_token和UserId; - 查询用户详细信息,构建安全上下文;
- 将上下文注入后续请求链路。
上下文模型定义
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();
}
}
该机制确保企业微信认证信息在内部系统中安全、高效传递,同时满足审计与权限控制需求,已在多个大型政企项目中落地验证。