通过微信测试公众号实现扫码登录
通过微信测试公众号实现扫码登录
一:效果展示:
我们在扫描完二维码之后会自动跳转到公众号中,然后可以看到:
二:功能实现
1:流程分析:
具体的流程就是,用户发送登录的请求,然后我们去检验是否有ticket,没有的话我们就要去获取ticket,获取ticket又需要accesstoken,我们同样要去获取,然后前端还会一直检验登录状态;
所以我们就会有三个接口,第一个是获取ticket,第二个是检查登录状态,第三个是登录成功后微信回调,我们要发送模板消息,如上图一样;
2:准备工作:
1:申请微信测试公众号
然后需要连接我们的接口,这样微信服务器才能够回调我们;
而这个接口是个网址,且必须是公网网址才能进行访问,解决办法是可以使用内网穿透;
2:内网穿透
使用natapp进行内网穿透:
因为natapp提供的免费隧道,但是免费隧道提供的域名是随机分配,有时会变动,所以我们需要购买一个隧道和一个固定的域名,这样就能通过内网穿透将本地地址映射到公网了:
像这样:
3:代码实现:
首先需要与微信服务器建立联系,要定义两个个recevice接口,一个用来微信连接验证,一个用来微信回调:
@Slf4j
@RestController()
@CrossOrigin("*")
@RequestMapping("/api/v1/weixin/portal/")
public class WeixinPortalController {
@Value("${weixin.config.originalid}")
private String originalid;
@Value("${weixin.config.token}")
private String token;
@Resource
private ILoginService loginService;
//验签
@GetMapping(value = "receive", produces = "text/plain;charset=utf-8")
public String validate(@RequestParam(value = "signature", required = false) String signature,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "nonce", required = false) String nonce,
@RequestParam(value = "echostr", required = false) String echostr) {
try {
log.info("微信公众号验签信息开始 [{}, {}, {}, {}]", signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
boolean check = SignatureUtil.check(token, signature, timestamp, nonce);
log.info("微信公众号验签信息完成 check:{}", check);
if (!check) {
return null;
}
return echostr;
} catch (Exception e) {
log.error("微信公众号验签信息失败 [{}, {}, {}, {}]", signature, timestamp, nonce, echostr, e);
return null;
}
}
//微信回调
@PostMapping(value = "receive", produces = "application/xml; charset=UTF-8")
public String post(@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
try {
log.info("接收微信公众号信息请求{}开始 {}", openid, requestBody);
// 消息转换
MessageTextEntity message = XmlUtil.xmlToBean(requestBody, MessageTextEntity.class);
if ("event".equals(message.getMsgType())&&"SCAN".equals(message.getEvent())){
loginService.saveLoginState(message.getTicket(),openid);
return buildMessageTextEntity(openid,"恭喜你,登录成功");
}
return buildMessageTextEntity(openid, "小蕊" + message.getContent());
} catch (Exception e) {
log.error("接收微信公众号信息请求{}失败 {}", openid, requestBody, e);
return "";
}
}
private String buildMessageTextEntity(String openid, String content) {
MessageTextEntity res = new MessageTextEntity();
// 公众号分配的ID
res.setFromUserName(originalid);
res.setToUserName(openid);
res.setCreateTime(String.valueOf(System.currentTimeMillis() / 1000L));
res.setMsgType("text");
res.setContent(content);
return XmlUtil.beanToXml(res);
}
}
然后因为我们要从微信获取accesstoken,和ticket所以我们需要向微信发送请求,我们可以使用Retrofit来发送请求:
public interface IWeixinApiService {
/**
* 获取access-token的请求方法
* @param grant_type
* @param appid
* @param secret
* @return
*/
//获取accesstoken
@GET("/cgi-bin/token")
Call<WeixinTokenRes>getToken(@Query(value = "grant_type")String grant_type,
@Query(value = "appid")String appid,
@Query(value = "secret")String secret
);
//获取二维码
@POST("/cgi-bin/qrcode/create")
Call<WeixinQrCodeRes>creatQrCode(@Query(value = "access_token")String accessToken,
@Body WeixinQrCodeReq weixinQrCodeReq
);
//发送模板消息
@POST("cgi-bin/message/template/send")
Call<Void>sendMessage(@Query(value = "access_token")String accessToken,
@Body WeixinTemplateVO weixinTemplateVO
);
}
然后我们需要在配置类中将Retrofit配置一下basicurl和转换器(将json转换成对象,或者将对象转换成json)
具体的如何请求我们是参靠微信的开发文档,参数的定义都是根据开发文档:
然后就是编写配置类:
@Slf4j
@Configuration
public class Retrofit2Config {
private static final String BASE_URL = "https://api.weixin.qq.com/";
//配置BASE_URL和转换器
@Bean
public Retrofit retrofit() {
return new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(JacksonConverterFactory.create()).build();
}
//创建代理对象,将接口中的方法转成http请求;
@Bean
public IWeixinApiService weixinApiService(Retrofit retrofit) {
return retrofit.create(IWeixinApiService.class);
}
}
还有一些实体类,我们都是根据文档中返回的参数,或者请求的参数进行构造的:
如WeixinQrCodeReq
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WeixinQrCodeReq {
private int expire_seconds;
private String action_name;
private ActionInfo action_info;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ActionInfo {
Scene scene;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Scene {
Long scene_id;
String scene_str;
}
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum ActionNameTypeVo{
QR_SCENE("QR_SCENE", "临时的整型参数值"),
QR_STR_SCENE("QR_STR_SCENE", "临时的字符串参数值"),
QR_LIMIT_SCENE("QR_LIMIT_SCENE", "永久的整型参数值"),
QR_LIMIT_STR_SCENE("QR_LIMIT_STR_SCENE", "永久的字符串参数值");
private String code;
private String info;
}
}
这里使用了内部类和枚举,枚举中有两个字段,分别是枚举的值,和枚举的详细信息;需要注意的是内部类必须要是静态内部类才能使用 @Builder注解;
然后就是登录接口的定义:
登录serivce中有三个方法:
1:获取ticket(先获取accesstoken在获取ticket)
2:判断是否登录
3:登录回调时,保存登录状态发送模板消息
public interface ILoginService {
String createQrCodeTicket()throws Exception;
String checkLogin(String ticket);
void saveLoginState(String ticket,String openId) throws IOException;
}
然后是实现接口:
@Service
public class ILoginServiceImpl implements ILoginService {
//从配置文件中注入
@Value("${weixin.config.app-id}")
private String appId;
@Value("${weixin.config.app-secret}")
private String appSecret;
@Value("${weixin.config.template_id}")
private String templateId;
//使用redis存储accesstoken和ticket
@Resource
private StringRedisTemplate stringRedisTemplate;
//调用接口方法发送请求
@Resource
private IWeixinApiService weixinApiService;
@Override
public String createQrCodeTicket() throws Exception {
//从缓存中尝试获取accesstoken;
String accessToken = stringRedisTemplate.opsForValue().get(appId);
//如果为空就是没有获取到,我们在向微信服务器发送请求,使用IWeixinApiService的方法
if (accessToken==null){
Call<WeixinTokenRes> call = weixinApiService.getToken("client_credential", appId, appSecret);
//发送请求,获取响应对象中的accesstoken;
WeixinTokenRes weixinTokenRes = call.execute().body();
//断言,如果为空就会抛出异常
assert weixinTokenRes!=null;
//给accesstoken赋值
accessToken=weixinTokenRes.getAccess_token();
//将token保存,一个appid有一个accesstoken
stringRedisTemplate.opsForValue().set(appId,accessToken);
}
//发送请求获取ticket
WeixinQrCodeReq weixinQrCodeReq = WeixinQrCodeReq.builder()
.expire_seconds(2592000)
.action_name(WeixinQrCodeReq.ActionNameTypeVo.QR_SCENE.getCode())
.action_info(WeixinQrCodeReq.ActionInfo.builder()
.scene(WeixinQrCodeReq.ActionInfo.Scene.builder().scene_id(10061L)
.build())
.build()).
build();
Call<WeixinQrCodeRes> codeResCall = weixinApiService.creatQrCode(accessToken, weixinQrCodeReq);
String ticket = codeResCall.execute().body().getTicket();
return ticket;
}
@Override
public String checkLogin(String ticket) {
//判断是否登录成功,只有有值才算是登录成功
return stringRedisTemplate.opsForValue().get(ticket);
}
//微信回调
@Override
public void saveLoginState(String ticket, String openId) throws IOException {
//将微信回调的参数openid传入
stringRedisTemplate.opsForValue().set(ticket,openId);
//同样的尝试获取token,没有就发送请求获取
String accessToken = stringRedisTemplate.opsForValue().get(appId);
if (accessToken==null){
Call<WeixinTokenRes> call = weixinApiService.getToken("client_credential", appId, appSecret);
WeixinTokenRes weixinTokenRes = call.execute().body();
assert weixinTokenRes!=null;
accessToken=weixinTokenRes.getAccess_token();
stringRedisTemplate.opsForValue().set(appId,accessToken);
}
//设置模板消息
HashMap<String, Map<String, String>> data = new HashMap<>();
WeixinTemplateVO.put(data, WeixinTemplateVO.TemplateKey.USER,openId);
WeixinTemplateVO.put(data, WeixinTemplateVO.TemplateKey.DATE, LocalDate.now().toString());
WeixinTemplateVO weixinTemplateVO = new WeixinTemplateVO(openId, templateId);
weixinTemplateVO.setData(data);
//设置模板消息的点击跳转地址
weixinTemplateVO.setUrl("https://gaga.plus");
Call<Void> call = weixinApiService.sendMessage(accessToken, weixinTemplateVO);
call.execute();
}
}
然后是定义controller,接收前端发送的请求;
controller中有两个方法:
一个是获取ticket,
一个是检验是否登录:
@Slf4j
@RestController()
@CrossOrigin("*")
@RequestMapping("/api/v1/login/")
public class LoginController {
@Resource
private ILoginService iLoginService;
@GetMapping("weixin_qrcode_ticket")
public Response<String> weixinQrCodeTicket() {
try {
String qrCodeTicket = iLoginService.createQrCodeTicket();
log.info("登录获取ticket,{}", qrCodeTicket);
return Response.<String>builder()
.code(Constants.ResponseCode.SUCCESS.getCode())
.data(qrCodeTicket).info(Constants.ResponseCode.SUCCESS.getInfo())
.build();
} catch (Exception e) {
log.error("登录失败,{}", e);
return Response.<String>builder()
.code(Constants.ResponseCode.UN_ERROR.getCode())
.info(Constants.ResponseCode.UN_ERROR.getInfo())
.build();
}
}
@GetMapping("check_login")
public Response<String> checkLogin(@RequestParam String ticket) {
try {
String openId = iLoginService.checkLogin(ticket);
if (StringUtils.isNotBlank(openId)) {
log.info("登录验证成功,openid,{},ticket,{}", openId, ticket);
return Response.<String>builder()
.code(Constants.ResponseCode.SUCCESS.getCode())
.data(openId).info(Constants.ResponseCode.SUCCESS.getInfo())
.build();
} else {
log.info("未登录ticket,{}", ticket);
return Response.<String>builder()
.code(Constants.ResponseCode.NO_LOGIN.getCode())
.data(openId).info(Constants.ResponseCode.NO_LOGIN.getInfo())
.build();
}
} catch (Exception e) {
log.info("登录失败,ticket,{},e,{}", ticket, e);
return Response.<String>builder()
.code(Constants.ResponseCode.UN_ERROR.getCode())
.info(Constants.ResponseCode.UN_ERROR.getInfo())
.build();
}
}
}
注意异常的处理和日志的输出,要细致一些;
原文地址:https://blog.csdn.net/2301_79748665/article/details/143716032
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!