WeChat(微信)でWebサイトをモーメンツ(朋友圈)に共有する際、サムネイル画像付きで表示させるには、WeChat公式のJSSDKを導入し、認証済みの公式アカウントが必要です。WeChat 6.5.5以降、JSSDKを使用しない共有は公式にサポートされていません。
前提条件
- 共有対象のWebサイトドメインがICP登録(中国国内サーバー登録)されていること
- WeChat公式アカウント管理画面でAppIDとAppSecretを取得済み
- サーバーIPがホワイトリストに登録されていること
フロントエンド実装
HTMLページにWeChat JSSDKを組み込み、シグネチャ検証とシェア設定を行います。
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
(function() {
const shareConfig = {
appId: window.WX_APP_ID,
timestamp: window.WX_TIMESTAMP,
nonceStr: window.WX_NONCE_STR,
signature: window.WX_SIGNATURE,
shareTitle: '共有タイトル',
shareImageUrl: 'https://example.com/share-thumbnail.jpg'
};
wx.config({
debug: false,
appId: shareConfig.appId,
timestamp: shareConfig.timestamp,
nonceStr: shareConfig.nonceStr,
signature: shareConfig.signature,
jsApiList: [
'updateAppMessageShareData',
'updateTimelineShareData',
'onMenuShareTimeline',
'onMenuShareAppMessage'
]
});
wx.ready(function() {
// モーメンツへの共有設定
wx.updateTimelineShareData({
title: shareConfig.shareTitle,
link: window.location.href,
imgUrl: shareConfig.shareImageUrl,
success: function() {}
});
// チャットへの共有設定
wx.updateAppMessageShareData({
title: shareConfig.shareTitle,
desc: 'シェアの説明文',
link: window.location.href,
imgUrl: shareConfig.shareImageUrl,
success: function() {}
});
});
})();
</script>
また、ページ読み込み時にサムネイル画像を認識させるため、非表示エリアに画像を配置します。
<div style="width:0;height:0;overflow:hidden;position:absolute;">
<img src="https://example.com/share-thumbnail.jpg" alt="share" />
</div>
バックエンド実装(C#)
シグネチャパッケージ生成クラス
public class WeChatSignatureService
{
private readonly string _appId;
private readonly string _appSecret;
private readonly ITokenRepository _tokenRepository;
public WeChatSignatureService(string appId, string appSecret, ITokenRepository tokenRepository)
{
_appId = appId;
_appSecret = appSecret;
_tokenRepository = tokenRepository;
}
public Dictionary<string, object> GenerateSignaturePackage(string currentUrl)
{
var jsApiTicket = GetOrRefreshJsApiTicket();
var timestamp = GenerateTimestamp();
var nonceStr = GenerateNonceString();
var rawString = $"jsapi_ticket={jsApiTicket}&noncestr={nonceStr}×tamp={timestamp}&url={currentUrl}";
var signature = ComputeSha1Hash(rawString);
return new Dictionary<string, object>
{
{ "appId", _appId },
{ "timestamp", timestamp },
{ "nonceStr", nonceStr },
{ "signature", signature },
{ "url", currentUrl }
};
}
private string GenerateNonceString(int length = 16)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
private int GenerateTimestamp()
{
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
return (int)(DateTime.UtcNow - epoch).TotalSeconds;
}
private string ComputeSha1Hash(string input)
{
using (var sha1 = SHA1.Create())
{
var bytes = Encoding.UTF8.GetBytes(input);
var hash = sha1.ComputeHash(bytes);
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
private string GetOrRefreshJsApiTicket()
{
var stored = _tokenRepository.GetJsApiTicket();
if (stored != null && stored.IsValid)
{
return stored.Ticket;
}
var accessToken = GetOrRefreshAccessToken();
var ticketJson = HttpGet($"https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={accessToken}&type=jsapi");
var ticket = JsonSerializer.Deserialize<JsApiTicketResponse>(ticketJson);
_tokenRepository.SaveJsApiTicket(new JsApiTicketData
{
Ticket = ticket.Ticket,
ExpiresAt = DateTime.Now.AddSeconds(7000)
});
return ticket.Ticket;
}
private string GetOrRefreshAccessToken()
{
var stored = _tokenRepository.GetAccessToken();
if (stored != null && stored.IsValid)
{
return stored.Token;
}
var json = HttpGet($"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={_appId}&secret={_appSecret}");
var response = JsonSerializer.Deserialize<AccessTokenResponse>(json);
_tokenRepository.SaveAccessToken(new AccessTokenData
{
Token = response.AccessToken,
ExpiresAt = DateTime.Now.AddSeconds(7000)
});
return response.AccessToken;
}
private string HttpGet(string url)
{
using (var client = new WebClient())
{
return client.DownloadString(url);
}
}
}
トークン管理リポジトリ
public interface ITokenRepository
{
AccessTokenData GetAccessToken();
void SaveAccessToken(AccessTokenData data);
JsApiTicketData GetJsApiTicket();
void SaveJsApiTicket(JsApiTicketData data);
}
public class DatabaseTokenRepository : ITokenRepository
{
private readonly IDbConnection _connection;
public DatabaseTokenRepository(IDbConnection connection)
{
_connection = connection;
}
public AccessTokenData GetAccessToken()
{
const string sql = "SELECT access_token, expires_at FROM weixin_tokens WHERE id = 1";
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = sql;
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
return new AccessTokenData
{
Token = reader.GetString(0),
ExpiresAt = reader.GetDateTime(1)
};
}
}
}
return null;
}
public void SaveAccessToken(AccessTokenData data)
{
const string sql = @"INSERT INTO weixin_tokens (id, access_token, expires_at)
VALUES (1, @Token, @ExpiresAt)
ON DUPLICATE KEY UPDATE access_token = @Token, expires_at = @ExpiresAt";
_connection.Execute(sql, new { data.Token, data.ExpiresAt });
}
public JsApiTicketData GetJsApiTicket()
{
const string sql = "SELECT jsapi_ticket, expires_at FROM weixin_jsapi_tickets WHERE id = 1";
using (var cmd = _connection.CreateCommand())
{
cmd.CommandText = sql;
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
return new JsApiTicketData
{
Ticket = reader.GetString(0),
ExpiresAt = reader.GetDateTime(1)
};
}
}
}
return null;
}
public void SaveJsApiTicket(JsApiTicketData data)
{
const string sql = @"INSERT INTO weixin_jsapi_tickets (id, jsapi_ticket, expires_at)
VALUES (1, @Ticket, @ExpiresAt)
ON DUPLICATE KEY UPDATE jsapi_ticket = @Ticket, expires_at = @ExpiresAt";
_connection.Execute(sql, new { data.Ticket, data.ExpiresAt });
}
}
public class AccessTokenData
{
public string Token { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsValid => !string.IsNullOrEmpty(Token) && DateTime.Now < ExpiresAt;
}
public class JsApiTicketData
{
public string Ticket { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsValid => !string.IsNullOrEmpty(Ticket) && DateTime.Now < ExpiresAt;
}
コントローラーでの使用例
public class ShareController : Controller
{
private readonly WeChatSignatureService _signatureService;
public ShareController(WeChatSignatureService signatureService)
{
_signatureService = signatureService;
}
public ActionResult Index()
{
var currentUrl = Request.Url.AbsoluteUri;
var signaturePackage = _signatureService.GenerateSignaturePackage(currentUrl);
ViewBag.WxAppId = signaturePackage["appId"];
ViewBag.WxTimestamp = signaturePackage["timestamp"];
ViewBag.WxNonceStr = signaturePackage["nonceStr"];
ViewBag.WxSignature = signaturePackage["signature"];
return View();
}
}
データベース設計
トークンとチケットを永続化するためのテーブル構造です。
CREATE TABLE weixin_accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
appid VARCHAR(100) NOT NULL,
appsecret VARCHAR(150) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE weixin_tokens (
id INT PRIMARY KEY,
access_token VARCHAR(500) NOT NULL,
expires_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE weixin_jsapi_tickets (
id INT PRIMARY KEY,
jsapi_ticket VARCHAR(500) NOT NULL,
expires_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
注意事項
access_tokenは1日の取得回数制限があるため、キャッシュとデータベースで管理し、有効期限内は再利用するjsapi_ticketも同様に取得回数制限があるため、適切にキャッシュする- シグネチャ生成時、URLパラメータはエンコードせずそのまま使用する
- 本番環境ではHTTPSが必須
実装完了後、WeChat開発者ツールを使用してデバッグし、各パラメータが正しく取得できているか確認してください。正常に設定されれば、共有時に指定したサムネイル画像が表示されるようになります。