Springboot简便的配置微信小程序

Springboot简便的配置微信小程序

ShareNotes

最近在完成一个微信小程序项目,即将上线
欢迎star
Github–ShareNotes

issue

写小程序接口遇到的具体情况

  • 通过openId登录。也就是所谓的微信点击直接登录。不需要输入账户密码
  • 使用微信自带的api过滤不合法的字符或者图片
  • 商户,支付接口
  • 更多issue后期遇到情况会添加

微信直接登录

在最早的时候我帮过朋友写过一个花店卖花的程序。
最开始的时候大多数都是通过
微信官方文档API
找到api。然后RestTemplate来访问url。
RestTemplate是Spring提供的用于访问Rest服务的客户端。

@Autowird
private RestTemplate restTemplate;

WxConfig wxConfig= restTemplate.getForObject("https://api.weixin.qq.com/xxxxx/" , WxConfig.class);

十分的烦琐

引入别人写好的微信封装工具类

Github-Binary Wang

开始操作

引入依赖


    com.github.binarywang
    weixin-java-miniapp
    3.3.0



    com.github.binarywang
    weixin-java-pay
    3.3.0

jwt的依赖


    com.auth0
    java-jwt
    3.4.1

创建用户表

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(63) NOT NULL COMMENT '用户名称',
  `password` varchar(63) NOT NULL DEFAULT '' COMMENT '用户密码',
  `gender` tinyint(3) NOT NULL DEFAULT '0' COMMENT '性别:0 未知, 1男, 1 女',
  `birthday` date DEFAULT NULL COMMENT '生日',
  `last_login_time` datetime DEFAULT NULL COMMENT '最近一次登录时间',
  `last_login_ip` varchar(63) NOT NULL DEFAULT '' COMMENT '最近一次登录IP地址',
  `nickname` varchar(63) NOT NULL DEFAULT '' COMMENT '用户昵称或网络名称',
  `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '用户手机号码',
  `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '用户头像图片',
  `weixin_openid` varchar(63) NOT NULL DEFAULT '' COMMENT '微信登录openid',
  `session_key` varchar(100) NOT NULL DEFAULT '' COMMENT '微信登录会话KEY',
  `status` tinyint(3) NOT NULL DEFAULT '0' COMMENT '0 可用, 1 禁用, 2 注销',
  `add_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `deleted` tinyint(1) DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_name` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

本质是 weixin_openid,session_key。

因为登录不需要账户密码。是直接登录。所以如果第一次登录创建用户的时候username和password可以设置为 weixin_openid,或者任意值。

配置wxConfig。

如果weixin-java-miniapp依赖已经导入完成就可以开始配置你的appid和appsecret,其在到微信开发者平台里头

在之前先配置好properties(我这里是yml格式)

wx:
  app-id: xxxxx
  app-secret: xxxxx
  mch-id: xxxxxxxx
  mch-key: xxxxxx
  notify-url: http://www.example.com/wx/order/pay-notify
  # 商户证书文件路径
  # 请参考“商户证书”一节 https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
  key-path: xxxxx

获取properties的值

@Configuration
@ConfigurationProperties(prefix = "wx")
public class WxProperties {

    private String appId;

    private String appSecret;

    private String mchId;

    private String mchKey;

    private String notifyUrl;

    private String keyPath;

    public String getNotifyUrl() {
        return notifyUrl;
    }

    public void setNotifyUrl(String notifyUrl) {
        this.notifyUrl = notifyUrl;
    }

    public String getMchKey() {
        return mchKey;
    }

    public void setMchKey(String mchKey) {
        this.mchKey = mchKey;
    }

    public String getAppId() {
        return this.appId;
    }

    public void setAppId(String appId) {
    }

    public String getAppSecret() {
        return appSecret;
    }

}
    }
        this.keyPath = keyPath;
    public void setKeyPath(String keyPath) {

    }
        return keyPath;
    public String getKeyPath() {

    }
        this.mchId = mchId;
    public void setMchId(String mchId) {

    }
        return mchId;
    public String getMchId() {

    }
        this.appSecret = appSecret;
    public void setAppSecret(String appSecret) {

配置wxConfig

@Configuration
public class WxConfig {
    @Autowired
    private WxProperties properties;

    @Bean
    public WxMaConfig wxMaConfig() {
        WxMaInMemoryConfig config = new WxMaInMemoryConfig();
        config.setAppid(properties.getAppId());
        config.setSecret(properties.getAppSecret());
        return config;
    }


    @Bean
    public WxMaService wxMaService(WxMaConfig maConfig) {
        WxMaService service = new WxMaServiceImpl();
        service.setWxMaConfig(maConfig);
        return service;
    }

    @Bean
    public WxPayConfig wxPayConfig() {
        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(properties.getAppId());
        payConfig.setMchId(properties.getMchId());
        payConfig.setMchKey(properties.getMchKey());
        payConfig.setNotifyUrl(properties.getNotifyUrl());
        payConfig.setKeyPath(properties.getKeyPath());
        payConfig.setTradeType("JSAPI");
        payConfig.setSignType("MD5");
        return payConfig;
    }


    @Bean
    public WxPayService wxPayService(WxPayConfig payConfig) {
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(payConfig);
        return wxPayService;
    }
}

所有关于微信小程序配置都在这里了。
然后我们可以使用他封装好的wxMaService了。

生成(写好)对应的 实体类,mapper。

User.java

@Data
public class User {
private Integer id;
private String username;
private String password;
private Byte gender;
private Date birthday;
private Date lastLoginTime;
private String lastLoginIp;
private String nickname;
private String mobile;
private String avatar;
private String weixinOpenid;
private String sessionKey;
private Byte status;
private Date addTime;
private Date updateTime;
private Boolean deleted;
}

WxLoginInfo.java

public class WxLoginInfo {
    private String code;
    private UserDto userInfo;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public UserDto getUserInfo() {
        return userInfo;
    }

    public void setUserInfo(UserDto userInfo) {
        this.userInfo = userInfo;
    }
}

UserDto.java

@Data
public class UserDto {
    private String nickName;
    private String avatarUrl;
    private String country;
    private String province;
    private String city;
    private String language;
    private Byte gender;

UserService.java
需要放这两个方法

public interface UserService {
//根据openId查询
public User queryByOid(String openId);
//加入user表
public void add(User user);

实现
UserServiceImpl.java

@Override
public User queryByOid(String openId) {
//这里用了Example
    UserExample example = new UserExample();
    example.or().andWeixinOpenidEqualTo(openId).andDeletedEqualTo(false);
    return userMapper.selectOneByExample(example);
}
@Override
public void add(User user) {
    user.setAddTime(new Date());
    user.setUpdateTime(new Date());
    userMapper.insertSelective(user);
}

Controller

WxAuthController.java

@Slf4j
@RestController
@RequestMapping("/wx/auth")
@Validated
public class WxAuthController {

    @Autowired
    private UserService userService;

//这里是之前添加依赖里面的方法
    @Autowired
    private WxMaService wxService;

    @PostMapping("login_by_weixin")
public Object loginByWeixin(@RequestBody WxLoginInfo wxLoginInfo, HttpServletRequest request) {
    String code = wxLoginInfo.getCode();
    UserDto userDto = wxLoginInfo.getUserInfo();
    if (code == null || userDto == null) {
        return ResponseUtil.badArgument();
    }

    String sessionKey = null;
    String openId = null;
    try {
        WxMaJscode2SessionResult result = this.wxService.getUserService().getSessionInfo(code);
        sessionKey = result.getSessionKey();
        openId = result.getOpenid();
    } catch (Exception e) {
        e.printStackTrace();
    }

    if (sessionKey == null || openId == null) {
        return ResponseUtil.fail();
    }

    User user = userService.queryByOid(openId);
    if (user == null) {
        user = new User();
        user.setUsername(openId);
        user.setPassword(openId);
        user.setWeixinOpenid(openId);
        user.setAvatar(userDto.getAvatarUrl());
        user.setNickname(userDto.getNickName());
        user.setGender(userDto.getGender());
        user.setStatus((byte) 0);
        user.setLastLoginTime(new Date());
        user.setLastLoginIp(IpUtil.getIpAddr(request));
        user.setSessionKey(sessionKey);

        userService.add(user);

    } else {
        user.setLastLoginTime(new Date());
        user.setLastLoginIp(IpUtil.getIpAddr(request));
        user.setSessionKey(sessionKey);
        if (userService.updateById(user) == 0) {
            return ResponseUtil.updatedDataFailed();
        }
    }

    // token
    String token = UserTokenManager.generateToken(user.getId());

    Map result = new HashMap();
    result.put("token", token);
    result.put("userInfo", userDto);


    return ResponseUtil.ok(result);
}

这里的ResponseUtil.ok(result)是我自己的返回值封装类。
举个例子

public static Object ok(Object data) {
    Map obj = new HashMap();
    obj.put("errno", 0);
    obj.put("errmsg", "成功");
    obj.put("data", data);
    return obj;
}

JWT配置

接上面的 UserTokenManager
JwtHelper----> UserTokenManager

JwtHelper.java

public class JwtHelper {
   // 秘钥
   static final String SECRET = "YOUR-SECRET-TOKEN";
   // 签名是有谁生成
   static final String ISSUSER = "SECRET";
   // 签名的主题
   static final String SUBJECT = "this is you token";
   // 签名的观众
   static final String AUDIENCE = "MINIAPP";
  
   public String createToken(Integer userId){
      try {
          Algorithm algorithm = Algorithm.HMAC256(SECRET);
          Map map = new HashMap();
          Date nowDate = new Date();
          // 过期时间:7天2小时
          Date expireDate = getAfterDate(nowDate,0,0,7,2,0,0);
           map.put("alg", "HS256");
           map.put("typ", "JWT");
          String token = JWT.create()
             // 设置头部信息 Header
             .withHeader(map)
             // 设置 载荷 Payload
             .withClaim("userId", userId)
              .withIssuer(ISSUSER)
              .withSubject(SUBJECT)
              .withAudience(AUDIENCE)
              // 生成签名的时间 
              .withIssuedAt(nowDate)
              // 签名过期的时间 
              .withExpiresAt(expireDate)
              // 签名 Signature
              .sign(algorithm);
          return token;
      } catch (JWTCreationException exception){
         exception.printStackTrace();
      }
      return null;
   }
   
   public Integer verifyTokenAndGetUserId(String token) {
      try {
          Algorithm algorithm = Algorithm.HMAC256(SECRET);
          JWTVerifier verifier = JWT.require(algorithm)
              .withIssuer(ISSUSER)
              .build();
          DecodedJWT jwt = verifier.verify(token);
          Map claims = jwt.getClaims();
          Claim claim = claims.get("userId");
          return claim.asInt();
      } catch (JWTVerificationException exception){
//       exception.printStackTrace();
      } 
      return 0;
   }
   public  Date getAfterDate(Date date, int year, int month, int day, int hour, int minute, int second){
      if(date == null){
         date = new Date();
      }      
      Calendar cal = new GregorianCalendar();      
      cal.setTime(date);
      if(year != 0){
         cal.add(Calendar.YEAR, year);
      }
      if(month != 0){
         cal.add(Calendar.MONTH, month);
      }
      if(day != 0){
         cal.add(Calendar.DATE, day);
      }
      if(hour != 0){
         cal.add(Calendar.HOUR_OF_DAY, hour);
      }
      if(minute != 0){
         cal.add(Calendar.MINUTE, minute);
      }
      if(second != 0){
         cal.add(Calendar.SECOND, second);
      }
      return cal.getTime();
   }
}

UserTokenManager.java

public static String generateToken(Integer id) {
    JwtHelper jwtHelper = new JwtHelper();
    return jwtHelper.createToken(id);
}
public static Integer getUserId(String token) {
    JwtHelper jwtHelper = new JwtHelper();
    Integer userId = jwtHelper.verifyTokenAndGetUserId(token);
    if(userId == null || userId == 0){
        return null;
    }
    return userId;
}

小程序前端

user.js

function loginByWeixin(userInfo) {

  return new Promise(function (resolve, reject) {
    return login().then((res) => {
      //这里的api.AuthLoginByWeixin 为 localhost:8080/wx/auth/login_by_weixin
      util.request(api.AuthLoginByWeixin, {
        code: res.code,
        userInfo: userInfo
      }, 'POST').then(res => {
        if (res.errno === 0) {
          //存储用户信息
          wx.setStorageSync('userInfo', res.data.userInfo);
          wx.setStorageSync('token', res.data.token);

          resolve(res);
        } else {
          reject(res);
        }
      }).catch((err) => {
        reject(err);
      });
    }).catch((err) => {
      reject(err);
    })
  });
}

utils.js

function request(url, data = {}, method = "GET") {
  return new Promise(function (resolve, reject) {
    wx.request({
      url: url,
      data: data,
      method: method,
      header: {
        'Content-Type': 'application/json',
//刚刚你设置的token
        'YOUR-SECRET-TOKEN': wx.getStorageSync('token')
      },
      success: function (res) {

        if (res.statusCode == 200) {

          if (res.data.errno == 501) {
            // 清除登录相关内容
            try {
              wx.removeStorageSync('userInfo');
              wx.removeStorageSync('token');
            } catch (e) {
              // Do something when catch error
            }
            // 切换到登录页面
            wx.navigateTo({
              url: '/pages/ucenter/index/index'
            });
          } else {
            resolve(res.data);
          }
        } else {
          reject(res.errMsg);
        }

      },
      fail: function (err) {
        reject(err)
      }
    })
  });
}

小程序页面js的使用

user.loginByWeixin(e.detail.userInfo).then(res => {
  app.globalData.hasLogin = true;

  this.onShow();
}).catch((err) => {
  app.globalData.hasLogin = false;
  util.showErrorToast('微信登录失败');
});

关于登录就到这里了。可以打开我的GitHub源码其分别对应

Github–ShareNotes

  • 实体类和mapper还有sql
    share-Notes-db----->sql文件夹
    share-Notes-db----->src–>main–>java–>cn.sharenotes.db---->domain/mapper/model

  • wxconfig配置
    share-Notes-core----->src–>main–>java–>cn.sharenotes.core---->config
    share-Notes-core----->src–>main–>java–>cn.sharenotes.core---->utils
    share-Notes-core----->src–>main–>resources---->wxconfig.properties(我这里不是yml)

  • controller使用

share-Notes-wx-api----->src–>main–>java–>cn.sharenotes.wxapi---->web
share-Notes-wx-api----->src–>main–>java–>cn.sharenotes.wxapi---->service

  • 微信小程序
    shareNotes---->config—>api.js 地址配置
    shareNotes---->utils—>user.js /utils工具类
    shareNotes---->page---->uncenter---->index—>index.js 登录js

用binarywang的工具类使用过滤

我们回到wxconfig中

重新添加上wxmaSecCheckService


@Slf4j
@Configuration
@PropertySource(value = "classpath:wxconf.properties")
public class WxConfig {

    @Value("${APP_ID}")
    private String appId;
    @Value("${APP_SERCET}")
    private String appSecret;

    @Bean
    public WxMaConfig wxMaConfig() {
        WxMaInMemoryConfig config = new WxMaInMemoryConfig();
        config.setAppid(appId);
        config.setSecret(appSecret);
        log.info("id"+appId);
        log.info("key"+appSecret);
        return config;
    }


    @Bean
    public WxMaService wxMaService(WxMaConfig maConfig) {
        WxMaService service = new WxMaServiceImpl();
        service.setWxMaConfig(maConfig);
        return service;
    }
    @Bean
    public WxMaSecCheckService  wxMaSecCheckService(){
        WxMaSecCheckService wxMaSecCheckService = new WxMaSecCheckServiceImpl(wxMaService(wxMaConfig()));
        return wxMaSecCheckService;
    }
    @Bean
    public WxPayConfig wxPayConfig() {
        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(appId);
        payConfig.setTradeType("JSAPI");
        payConfig.setSignType("MD5");
        return payConfig;
    }


    @Bean
    public WxPayService wxPayService(WxPayConfig payConfig) {
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(payConfig);
        return wxPayService;
    }
}

使用

文字过滤

  @Autowired
    private WxMaSecCheckService wxMaSecCheckService;



if(!wxMaSecCheckService.checkMessage(JacksonUtil.parseString(body, "name"))){
            ResponseUtil.fail(500,"违法违规标题");
        }

图片

wxMaSecCheckService.checkImage(f)

他的方法都是返回布尔值。

分析原理

https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html

这里的本质是使用微信开发文档中的api


POST https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN

通过post请求这段api返回是否是内容安全。

官方文档很详细了。返回errcode和errMsg
通过为0失败为87014
Post的body中放图片。
不过我们看一下整句api,后面有一个accessToken。
这个accesstoken怎么获取呢。
回到官方文档

获取小程序全局唯一后台接口调用凭据(access_token)。调调用绝大多数后台接口时都需使用 access_token,开发者需要进行妥善保存。

api为

GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

这里需要我们的appid和appsecret。
请求后可以过去accesstoken。但是请求的token只会支持2个小时,后他将重新生成。2个小时后得再次请求。

tips

微信官方api中关于服务端的有趣api很多,可以查阅。虽然有请求次数限制。但是已经足够了

你可能感兴趣的:(生活笔记)