自学内容网 自学内容网

通过微信测试公众号实现扫码登录

通过微信测试公众号实现扫码登录

一:效果展示:

我们在扫描完二维码之后会自动跳转到公众号中,然后可以看到:

二:功能实现

1:流程分析:

具体的流程就是,用户发送登录的请求,然后我们去检验是否有ticket,没有的话我们就要去获取ticket,获取ticket又需要accesstoken,我们同样要去获取,然后前端还会一直检验登录状态;

所以我们就会有三个接口,第一个是获取ticket,第二个是检查登录状态,第三个是登录成功后微信回调,我们要发送模板消息,如上图一样;

2:准备工作:

1:申请微信测试公众号

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后需要连接我们的接口,这样微信服务器才能够回调我们;

而这个接口是个网址,且必须是公网网址才能进行访问,解决办法是可以使用内网穿透;

2:内网穿透

使用natapp进行内网穿透:

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)

具体的如何请求我们是参靠微信的开发文档,参数的定义都是根据开发文档:

开始开发 / 获取 Access token

账号管理 / 生成带参数的二维码

然后就是编写配置类:

@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)!