时间:2023-11-23 03:36:01 | 来源:网站运营
时间:2023-11-23 03:36:01 来源:网站运营
请问如何微信扫码登录网站同时关注公众号?:我的(文章)很长,你忍一下
本文通过一个实际的具有一定商业价值的项目,展示了 API 优先的开发方法。通过薅羊毛的方式,落地了 Free Arch 架构。
openapi: "3.0.0"info: version: 0.0.1 title: Authenticate with Wechat MP!servers: # Added by API Auto Mocking Plugin - description: SwaggerHub API Auto Mocking url: https://virtserver.swaggerhub.com/UniHeart/wechat-mp/0.0.1 - url: http://localhost:8080paths: /mp-qr: get: summary: Gets a temporary qr code with parameter operationId: mp-qr-url tags: - mp-qr responses: '200': description: Got the temporary qr code image link content: application/json: schema: $ref: '#/components/schemas/MpQR' example: expire_seconds: 60 imageUrl: https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQGT7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycnE3QWw3b3JmazMxb2FMQnh3Y1UAAgTOrmVgAwQ8AAAA sceneId: 66afab27-c8fa-417d-a28a-95d5a977e1d3 ticket: gQGT7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycnE3QWw3b3JmazMxb2FMQnh3Y1UAAgTOrmVgAwQ8AAAA url: http://weixin.qq.com/q/02rq7Al7orfk31oaLBxwc /mp-qr-scan-status: get: summary: Get the scanning status of qr code operationId: mp-qr-scan-status tags: - mp-qr parameters: - in: query name: ticket required: true description: the ticket for the qr code to query scanning status schema: type: string example: gQE48DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyb2U4U2wwb3JmazMxcS1kQ3h3YzgAAgSCjWZgAwQ8AAAA responses: '200': description: The scanning stqtus of qr code content: application/json: schema: $ref: '#/components/schemas/MpQRScanStatus' example: openId: oWFvUw5ryWycy8XoDCy1pV0SiB58 status: SCANNED /mp-message: post: summary: Receive xml messages sent from wechat mp server operationId: mp-message tags: - mp-qr requestBody: description: wechat mp messages in xml format required: true content: application/xml: schema: $ref: '#/components/schemas/xml' responses: '200': description: the message was well receivedcomponents: schemas: MpQR: type: object properties: expire_seconds: type: integer format: int64 example: 60 imageUrl: type: string example: https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQGT7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycnE3QWw3b3JmazMxb2FMQnh3Y1UAAgTOrmVgAwQ8AAAA sceneId: type: string example: 66afab27-c8fa-417d-a28a-95d5a977e1d3 ticket: type: string example: gQGT7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycnE3QWw3b3JmazMxb2FMQnh3Y1UAAgTOrmVgAwQ8AAAA url: type: string example: http://weixin.qq.com/q/02rq7Al7orfk31oaLBxwc MpQRScanStatus: type: object properties: openId: type: string example: oWFvUw5ryWycy8XoDCy1pV0SiB58 status: type: string example: SCANNED xml: type: object properties: ToUserName: type: string example: oWfv FromUserName: type: string example: 1234 CreateTime: type: number example: 1357290913 MsgType: type: string example: Text Event: type: string example: subscribe EventKey: type: string example: qrscene_123123 Ticket: type: string example: TICKET
build.gradle
文件中,增加一些依赖,主要有build.gradle
文件里添加一个任务,用来根据最新的 Swagger 文档生成相关的类型代码等: // generates the spring controller interfaces from openapi spec in src/main/resources/service.yamlopenApiGenerate { generatorName = "spring" inputSpec = "$projectDir/swagger-output/swagger.yaml" outputDir = "$buildDir/generated" apiPackage = "com.uniheart.wechatmpservice.api" invokerPackage = "com.uniheart.wechatmpservice" modelPackage = "com.uniheart.wechatmpservice.models" configOptions = [ dateLibrary: "java8", interfaceOnly: "true", ]}
这样每次文档有更新,就只需要在项目目录下跑一下命令: ./gradlew openApiGenerate
注意,我们采用了 Swagger Hub 来更新 API 文档,它有个 Sync 功能,可以在每次文档改动后点击一下,就会自动提交一个改动推送到你的 git 仓库。 WebSecurityConfig
里完成,主要代码如下: package com.uniheart.securing.web.wechat.mp;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpMethod;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .antMatchers("/mp-qr", "/mp-qr").permitAll() .antMatchers("/mp-qr-scan-status", "/mp-qr-scan-status").permitAll() .antMatchers(HttpMethod.POST, "/mp-message").permitAll() .antMatchers("/v3/api-docs", "/v3/api-docs").permitAll() .antMatchers("/swagger-ui", "/swagger-ui").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); http.csrf().disable(); }}
Controller
去实现预先定义好的 MpQrApi
即可: package com.uniheart.securing.web.wechat.mp;import com.uniheart.securing.web.wechat.mp.services.MpServiceBean;import com.uniheart.wechatmpservice.api.MpQrApi;import com.uniheart.wechatmpservice.models.MpQR;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic final class WechatMpApiController implements MpQrApi { @Autowired private MpServiceBean mpServiceBean; @Override public ResponseEntity<MpQR> mpQrUrl() { return new ResponseEntity<>(this.mpServiceBean.getMpQrCode(), HttpStatus.OK); }}
可见核心业务逻辑在 MpServiceBean
中,代码如下: package com.uniheart.securing.web.wechat.mp.services;import com.google.gson.Gson;import com.google.gson.JsonObject;import com.uniheart.securing.web.wechat.mp.Constants;import com.uniheart.wechatmpservice.models.MpQR;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.UnknownFormatConversionException;@Componentpublic class MpServiceBean { private final HttpClient httpClient; @Value("${weixin-qr-code-creation-endpoint:default-test-value}") private String qrCodeCreateUrl; @Value("${weixin-token-endpoint:default-test-value}") private String weixinAccessTokenEndpoint; public String getQrCodeCreateUrl() { return this.qrCodeCreateUrl; } public MpServiceBean() { this.httpClient = HttpClient.newHttpClient(); } public MpServiceBean(HttpClient client, String qrCodeCreateUrl, String tokenEndpoint) { this.httpClient = client; this.qrCodeCreateUrl = qrCodeCreateUrl; this.weixinAccessTokenEndpoint = tokenEndpoint; } public void setQrCodeCreateUrl(String url) { this.qrCodeCreateUrl = url; } public void setWeixinAccessTokenEndpoint(String url) { this.weixinAccessTokenEndpoint = url; } Logger logger = LoggerFactory.getLogger(MpServiceBean.class); public MpQR getMpQrCode() { var mpTokenManager = new MpTokenManager(this.weixinAccessTokenEndpoint); URI uri = URI.create(this.qrCodeCreateUrl + mpTokenManager.getAccessToken().accessToken); logger.info("Getting qr code with " + uri); var payload = WeixinQrCodeRequestPayload.getRandomInstance(); HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(payload.toJson())).uri(uri).build(); try { HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); WeixinErrorResponse errorResponse = new Gson().fromJson(response.body(), WeixinErrorResponse.class); WeixinTicketResponse ticketResponse = new Gson().fromJson(response.body(), WeixinTicketResponse.class); if (ticketResponse.ticket != null) { return new MpQR().ticket(ticketResponse.ticket).imageUrl(ticketResponse.url).expireSeconds(ticketResponse.expiresInSeconds).url(ticketResponse.url).sceneId(String.valueOf(payload.action_info.scene.scene_id)); } if (errorResponse.errcode == (40001)) { return new MpQR().ticket("test").imageUrl(Constants.FALLBACK_QR_URL); } throw new UnknownFormatConversionException(response.body()); } catch (InterruptedException ie) { System.err.println("Exception = " + ie); ie.printStackTrace(); return new MpQR().ticket("interrupted").imageUrl(Constants.FALLBACK_QR_URL); } catch (Exception ex) { System.err.println("Exception = " + ex); ex.printStackTrace(); return new MpQR().ticket("error").imageUrl(Constants.FALLBACK_QR_URL); } }}
WeixinQrCodeRequestPayload.getRandomInstance()
,会生成场景值。场景值以及带参二维码,因为每个登录请求尝试都是独立发生的, 所以应该是全局唯一;为了防止恶意者攻击,这个场景值应该具有不可猜性。前面介绍其可以使用 UUID 来满足这两点,参见《基于 keycloak 的关注公众号即登录功能的设计与实现》的具体实现,这里给出一种简便的实现,即根据当前时间来计算出一个场景值,由于精确到纳秒,所以很难重复。 package com.uniheart.securing.web.wechat.mp.services;import com.google.gson.Gson;import com.uniheart.securing.web.wechat.mp.Now;import org.joda.time.Instant;public class WeixinQrCodeRequestPayload { public String action_name; public ActionInfo action_info; public int expire_seconds; public String toJson() { return new Gson().toJson(this); } public static WeixinQrCodeRequestPayload getRandomInstance() { var timestamp = Now.instant(); var ret = new WeixinQrCodeRequestPayload(); ret.action_name = "QR_SCENE"; ret.expire_seconds = 604800; ret.action_info = new ActionInfo(); ret.action_info.scene = new Scene(); ret.action_info.scene.scene_id = timestamp.getEpochSecond() + timestamp.getNano(); return ret; }}class ActionInfo{ public Scene scene;}class Scene { public long scene_id;}
package com.uniheart.securing.web.wechat.mp.services;import com.google.gson.Gson;import com.uniheart.wechatmpservice.models.Xml;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.apache.pulsar.client.api.*;import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;@Componentpublic class MpMessageService { Logger logger = LoggerFactory.getLogger(MpMessageService.class); private final String pulsarUrl; private final String pulsarToken; private final String pulsarTopic; public MpMessageService(@Value("${pulsar-service-url}") String pulsarUrl, @Value("${pulsar-auth-token}") String pulsarToken, @Value("${pulsar-producer-topic}") String pulsarTopic) { this.pulsarUrl = pulsarUrl; this.pulsarToken = pulsarToken; this.pulsarTopic = pulsarTopic; } public void saveMessageTo(Xml message) throws PulsarClientException { var client = PulsarClient.builder().serviceUrl(pulsarUrl).authentication(AuthenticationFactory.token(pulsarToken)).build(); var producer = client.newProducer().topic(pulsarTopic).create(); producer.send(new Gson().toJson(message).getBytes()); producer.close(); client.close(); } public synchronized Xml getMessageFor(String ticket) throws PulsarClientException { var client = PulsarClient.builder().serviceUrl(pulsarUrl).authentication(AuthenticationFactory.token(pulsarToken)).build(); var consumer = client.newConsumer().topic(pulsarTopic).subscriptionName("my-subscription").subscribe(); var xml = new Xml().fromUserName("empty"); var received = false; var count = 0; do { var msg = consumer.receive(1, TimeUnit.SECONDS); count++; if (msg != null) { var json = new String(msg.getData()); try { xml = new Gson().fromJson(json, Xml.class); received = xml.getTicket().equals(ticket); if(received){ consumer.acknowledge(msg); } } catch (Exception ex) { logger.error("Failed to parse json: " + json); xml.fromUserName(json); consumer.acknowledge(msg); } } } while (!received && count < 30); consumer.close(); client.close(); return xml; }}
以上服务封装了保存和获取方法,消息接收的 Controller
调用起保存消息的方法: package com.uniheart.securing.web.wechat.mp;import com.uniheart.securing.web.wechat.mp.services.MpMessageService;import com.uniheart.wechatmpservice.api.MpMessageApi;import com.uniheart.wechatmpservice.models.Xml;import io.swagger.annotations.ApiParam;import org.apache.pulsar.client.api.PulsarClientException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RestController;import javax.validation.Valid;@RestControllerpublic class WechatMessageController implements MpMessageApi { Logger logger = LoggerFactory.getLogger(WechatMessageController.class); private final MpMessageService mpMessageService; public WechatMessageController(MpMessageService mpMessageService) { this.mpMessageService = mpMessageService; } @Override public ResponseEntity<Void> mpMessage(@ApiParam(value = "wechat mp messages in xml format", required = true) @Valid @RequestBody Xml xml) { try { this.mpMessageService.saveMessageTo(xml); logger.info("saved info: " + xml); return new ResponseEntity<>(HttpStatus.OK); } catch (PulsarClientException e) { e.printStackTrace(); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } }}
package com.uniheart.securing.web.wechat.mp;import com.uniheart.securing.web.wechat.mp.services.MpMessageService;import com.uniheart.wechatmpservice.api.MpQrScanStatusApi;import com.uniheart.wechatmpservice.models.MpQRScanStatus;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.web.bind.annotation.RestController;import java.security.Principal;import java.util.ArrayList;import java.util.List;@RestControllerpublic final class WechatMpQRScanStatusApiController implements MpQrScanStatusApi { private final MpMessageService mpMessageService; public WechatMpQRScanStatusApiController(MpMessageService mpMessageService) { this.mpMessageService = mpMessageService; } @Override public ResponseEntity<MpQRScanStatus> mpQrScanStatus(String ticket) { try { var xml = this.mpMessageService.getMessageFor(ticket); if(xml.getFromUserName().equals("empty")){ return new ResponseEntity<>(new MpQRScanStatus().openId(""), HttpStatus.REQUEST_TIMEOUT); } var user = new Object() {}; List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("WechatMP")); Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); return new ResponseEntity<>(new MpQRScanStatus().openId(xml.getFromUserName()).status("SCANNED"), HttpStatus.OK); } catch (Exception ex) { ex.printStackTrace(); return new ResponseEntity<>(new MpQRScanStatus().openId(ex.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } }}
function queryScanStatus(ticket) { var req = new XMLHttpRequest(); req.onreadystatechange = function () { if(req.readyState === 4 && req.status === 200) { const json = JSON.parse(req.responseText); if (json.status === 'SCANNED') { location.href = '/hello'; }else{ alert('发生错误(也许是超时了)!') } } }; req.open("GET", "/mp-qr-scan-status?ticket=" + ticket); req.send(); } function showQRCodeImage() { var req = new XMLHttpRequest(); req.onreadystatechange = function () { if (req.readyState === 4 && req.status === 200) { const json = JSON.parse(req.responseText); document.getElementById('wechat-mp-qr').setAttribute('src', 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + encodeURIComponent(json.ticket)); queryScanStatus(json.ticket); } }; req.open("GET", "/mp-qr", true); req.send(); } showQRCodeImage();
关键词:同时,关注,公众,请问