背景
使用微信公众号实现网页授权。
开始
1.微信网页授权的官方文档
2.申请微信测试公众号
从红框进入申请页面。
填写必要的信息,注意上图红框部分的域名需要可以外网能够访问,微信需要发送请求进行验证。我用的是内网穿透实现的,下面会说。
这里需配置授权成功后回调地址的域名,注意不要写http,https!这个坑我踩过。
3.内网穿透
我用的是natapp,免费的很稳定。详细信息可以看这个兄弟写的博文。
可以看这篇博文
4.实现
大体思路简单说一下,每生成一个授权二维码就在redis生成对应的key,用户扫描授权成功后获取用户信息,同时redis对应的key设置对应的value,保证一码扫一人。长轮询实现扫码成功通知前端。
controller
@Controller
@RequestMapping("/wechat")
@Api(tags = "微信相关接口")
public class WechatController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
WechatService wechatService;
@RequestMapping("/notify")
@ResponseBody
public String wechatNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
String xml = wechatService.wechatXml(request);
String result = "";
String echostr = request.getParameter("echostr");
// 首次接入
if (echostr != null && echostr.length() > 1) {
result = echostr;
}
return result;
}
@GetMapping("/obtain_login_code")
@ApiOperation("获取登录二维码")
@ResponseBody
public Result<LoginCodeResDTO> obtainLoginCode(@RequestParam Integer proId) {
return ResultHandler.result(wechatService.obtainLoginCode(proId));
}
@GetMapping("/redirect")
public String redirectUri(@RequestParam String code, @RequestParam String state, @RequestParam Integer pid) {
try {
int flag = wechatService.redirectUri(code, state, pid);
//失效
if (flag == 0) {
return "redirect:../pages/lose.html";
}
return "redirect:../pages/success.html";
} catch (Exception e) {
logger.error("扫码失败----", e);
return "redirect:../pages/warn.html";
}
}
@PostMapping(value = "/query_code")
@ApiOperation("长轮询查询是否扫码成功")
@ResponseBody
public Result<WechatUserEntity> queryLoginState(@RequestBody QueryLoginStateReqDTO req) {
return ResultHandler.result(wechatService.queryLoginState(req));
}
}
service
@Service
public class WechatServiceImpl implements WechatService {
@Autowired
WechatBaseManager wechatBaseManager;
@Autowired
RedisTemplate redisTemplate;
@Autowired
WechatUserManager wechatUserManager;
@Autowired
ProjectWechatUserDAO projectWechatUserDAO;
@Value(value = "${wechat.redirect-url}")
private String redirectUrl;
@Override
public String wechatXml(HttpServletRequest request) throws Exception {
StringBuffer sb = new StringBuffer();
InputStream is = request.getInputStream();
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String s = "";
while ((s = br.readLine()) != null) {
sb.append(s);
}
// 微信发送过来的xml数据
String xml = sb.toString();
return xml;
}
@Override
@Transactional(rollbackFor = Exception.class)
public WechatUserEntity queryLoginState(QueryLoginStateReqDTO req) {
//生成二维码时存储用户信息的key
String scene = req.getScene();
if (redisTemplate.hasKey(scene)) {
LocalDateTime queryTime = LocalDateTime.now();
return wechatBaseManager.recurseQueryLogin(queryTime, scene);
} else {
throw new BusinessException(40010, "Redis不存在:" + scene);
}
}
@Override
public int redirectUri(String code, String state, Integer pid) {
JSONObject oauthJson = wechatBaseManager.getOpenIdByCode(code);
if (oauthJson.containsKey("openid")) {
String openid = oauthJson.getString("openid");
JSONObject userJson = wechatBaseManager.getUserInfo(openid);
if (userJson.containsKey("openid")) {
//保存用户
WechatUserEntity wechatUserEntity = wechatBaseManager.handleLoginState(state, openid, userJson);
if (wechatUserEntity.getId().equals(0)) {
return 0;
}
//保存项目用户关系
wechatUserManager.saveWechatUserAndProject(pid, wechatUserEntity.getId());
} else {
throw new BusinessException(40005, "通过openid获取用户基本信息失败");
}
} else {
throw new BusinessException(40004, "通过code换取网页授权失败");
}
return 1;
}
@Override
public LoginCodeResDTO obtainLoginCode(Integer proId) {
String redirectUri = redirectUrl + "wechat/redirect?pid=" + proId;
String state = WechatContants.PROJECTCODES + UUID.randomUUID().toString().replace("-", "");
String src = wechatBaseManager.obtainLoginCode(redirectUri, state);
LoginCodeResDTO resDTO = new LoginCodeResDTO();
resDTO.setSrc(src);
resDTO.setState(state);
redisTemplate.opsForValue().set(state, "", 2, TimeUnit.HOURS);
return resDTO;
}
}
manager
@Component
public class WechatBaseManager {
@Autowired
RestTemplate restTemplate;
@Autowired
RedisTemplate redisTemplate;
@Autowired
WechatBaseDAO wechatBaseDAO;
@Autowired
WechatUserDAO wechatUserDAO;
@Autowired
ProjectWechatUserDAO projectWechatUserDAO;
@Value(value = "${wechat.appid}")
private String appId;
@Value(value = "${wechat.appsecret}")
private String appSecret;
/**
* 更新微信accessToken
*/
public void updateAccessToken() {
String tokenUrl = String.format(WechatContants.ACCESS_TOKEN_URL, appId, appSecret);
ResponseEntity<JSONObject> resp = restTemplate.getForEntity(tokenUrl, JSONObject.class);
JSONObject resultJson = resp.getBody();
// 如果请求成功
if (null != resultJson && resultJson.containsKey("access_token")) {
String accessToken = resultJson.getString("access_token");
Integer expiresIn = resultJson.getInteger("expires_in");
String jsTicketUrl = WechatContants.JS_TICKET_URL.replace("ACCESS_TOKEN", accessToken);
resp = restTemplate.getForEntity(jsTicketUrl, JSONObject.class);
resultJson = resp.getBody();
String jsTicket = resultJson.getString("ticket");
WechatBaseEntity wechatBaseEntity = wechatBaseDAO.findByAppId(appId);
if (null == wechatBaseEntity) {
wechatBaseEntity = new WechatBaseEntity();
wechatBaseEntity.setAppId(appId);
}
wechatBaseEntity.setAccessToken(accessToken);
wechatBaseEntity.setJsTicket(jsTicket);
wechatBaseEntity.setExpiresIn(expiresIn);
wechatBaseEntity.setLastUpdateTime(new Timestamp(System.currentTimeMillis()));
wechatBaseDAO.save(wechatBaseEntity);
redisTemplate.opsForValue().set(WechatContants.ACCESS_TOKEN, accessToken, 1, TimeUnit.HOURS);
}
}
public String getAccessToken() {
if (redisTemplate.hasKey(WechatContants.ACCESS_TOKEN)) {
return redisTemplate.opsForValue().get(WechatContants.ACCESS_TOKEN).toString();
} else {
return wechatBaseDAO.findByAppId(appId).getAccessToken();
}
}
public JSONObject createTemporary(String accessToken, int expire, String sceneStr, String type) {
String requestUrl = String.format(WechatContants.QR_SCENE_URL, accessToken);
JSONObject jsonObject = new JSONObject();
jsonObject.put("expire_seconds", expire);
jsonObject.put("action_name", type);
JSONObject sceneJson = new JSONObject();
sceneJson.put("scene_str", sceneStr);
JSONObject actionInfo = new JSONObject();
actionInfo.put("scene", sceneJson);
jsonObject.put("action_info", actionInfo);
ResponseEntity<JSONObject> resp = restTemplate.postForEntity(requestUrl, jsonObject.toString(), JSONObject.class);
return resp.getBody();
}
public WechatUserEntity recurseQueryLogin(LocalDateTime queryTime, String scene) {
LocalDateTime now = LocalDateTime.now();
if (queryTime.plusSeconds(30).isAfter(now)) {
Object object = redisTemplate.opsForValue().get(scene);
if (object != null && !"".equals(object.toString())) {
WechatUserEntity member = wechatUserDAO.findById(Integer.parseInt(object.toString())).orElseThrow(() -> new BusinessException(40006, "未找到该用户"));
return member;
} else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
recurseQueryLogin(queryTime, scene);
}
} else {
throw new BusinessException(40001, "重新获取");
}
throw new BusinessException(40002, "查询失败");
}
public JSONObject getOpenIdByCode(String code) {
String oauthUrl = String.format(WechatContants.EXCHANGE_URL, appId, appSecret, code);
ResponseEntity<String> resp = restTemplate.getForEntity(oauthUrl, String.class);
return JSONObject.parseObject(resp.getBody());
}
public JSONObject getUserInfo(String openid) {
String userUrl = String.format(WechatContants.AUTHINFO_URL, getAccessToken(), openid);
ResponseEntity<JSONObject> resp = restTemplate.getForEntity(userUrl, JSONObject.class);
return resp.getBody();
}
public WechatUserEntity handleLoginState(String key, String openid, JSONObject userJson) {
WechatUserEntity member;
member = wechatUserDAO.findByOpenId(openid);
if (redisTemplate.hasKey(key)) {
Object v = redisTemplate.opsForValue().get(key);
//判断是否二维码已被扫过,扫过返回标识,暂用id代替
if (!"".equals(v) && v != null) {
WechatUserEntity entity = new WechatUserEntity();
entity.setId(0);
return entity;
}
//第一次直接保存,否则更新信息
if (null == member) {
member = new WechatUserEntity();
member.setOpenId(openid);
}
if (userJson.containsKey("nickname")) {
member.setNickName(userJson.getString("nickname"));
}
if (userJson.containsKey("headimgurl")) {
member.setHeadImgUrl(userJson.getString("headimgurl"));
}
member = wechatUserDAO.save(member);
redisTemplate.opsForValue().set(key, member.getId(), 4, TimeUnit.HOURS);
} else {
WechatUserEntity entity = new WechatUserEntity();
entity.setId(0);
return entity;
}
return member;
}
public String obtainLoginCode(String redirectUri, String state) {
return String.format(WechatContants.AUTH_URL, appId, redirectUri, WechatContants.SCOPE, state);
}
}
utils
public class MessageBean {
private String ToUserName;
private String FromUserName;
private String CreateTime;
private String MsgType;
private String content;
/**
* <p>
* 私有的构造方法
* </p>
* <p>
* 只允许用其它的构造方法
* </p>
*/
private MessageBean() {
}
public MessageBean(String ToUserName, String FromUserName, String CreateTime, String MsgType) {
this.ToUserName = ToUserName;
this.FromUserName = FromUserName;
this.CreateTime = CreateTime;
this.MsgType = MsgType;
}
public MessageBean(String ToUserName, String FromUserName, String CreateTime, String MsgType, String content) {
this.ToUserName = ToUserName;
this.FromUserName = FromUserName;
this.CreateTime = CreateTime;
this.MsgType = MsgType;
this.content = content;
}
@Override
public String toString() {
StringBuffer sBuffer = new StringBuffer();
sBuffer.append("<xml>");
sBuffer.append("<ToUserName><![CDATA[" + this.ToUserName + "]]></ToUserName>");
sBuffer.append("<FromUserName><![CDATA[" + this.FromUserName + "]]></FromUserName>");
sBuffer.append("<CreateTime>" + this.CreateTime + "</CreateTime>");
sBuffer.append("<MsgType><![CDATA[" + this.MsgType + "]]></MsgType>");
if (this.MsgType.equals(WechatContants.TEXT_MSG_TYPE)) {// 文本消息
sBuffer.append("<Content><![CDATA[" + this.content + "]]></Content>");
} else if (this.MsgType.equals(WechatContants.KF_MSG_TYPE)) {// 客服消息
if (this.content != null && !"".equals(this.content)) {
sBuffer.append("<TransInfo>");
sBuffer.append("<KfAccount><![CDATA[" + this.content + "]]></KfAccount>");
sBuffer.append("</TransInfo>");
}
} else if (this.MsgType.equals(WechatContants.IMAGE_MSG_TYPE)) {
sBuffer.append("<Image>");
sBuffer.append("<MediaId><![CDATA[" + this.content + "]]></MediaId>");
sBuffer.append("</Image>");
}
sBuffer.append("</xml>");
return sBuffer.toString();
}
public String getToUserName() {
return ToUserName;
}
public void setToUserName(String toUserName) {
ToUserName = toUserName;
}
public String getFromUserName() {
return FromUserName;
}
public void setFromUserName(String fromUserName) {
FromUserName = fromUserName;
}
public String getCreateTime() {
return CreateTime;
}
public void setCreateTime(String createTime) {
CreateTime = createTime;
}
public String getMsgType() {
return MsgType;
}
public void setMsgType(String msgType) {
MsgType = msgType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
public class WechatContants {
/**
* 存放ACCESS_TOKEN的key
*/
public final static String ACCESS_TOKEN = "yenep-accesstoken";
/**
* 存放JS_TICKET的key
*/
public final static String JSAPI_TICKET = "jsticket";
/**
* 获取access_token的接口地址(GET) 限200(次/天)
*/
public final static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
/**
* 获取jsapi_ticket的接口地址,(GET)
*/
public final static String JS_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";
/**
* 微信 二维码
*/
public static final String QR_SCENE = "QR_SCENE";
public static final String QR_STR_SCENE = "QR_STR_SCENE";
public static final String TICKET = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
public static final String QR_SCENE_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";
/**
* 微信事件
*/
public static final String LOGIN = "login";
public static final String PROJECTCODES = "projectcodes";
/**
* 消息类型
*/
public static final String KF_MSG_TYPE = "transfer_customer_service";
public static final String TEXT_MSG_TYPE = "text";
public static final String IMAGE_MSG_TYPE = "image";
/**
* 弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息
*/
public static final String SCOPE = "snsapi_userinfo";
//授权网址
public static final String AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect";
/**
* 通过code换取网页授权access_token
*/
public static final String EXCHANGE_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
/**
* 获取用户信息
*/
public static final String AUTHINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
}
public class XmlUtil {
private Document document = null;
public XmlUtil(String xmlStr) {
try {
this.document = DocumentHelper.parseText(xmlStr);
} catch (DocumentException var3) {
var3.printStackTrace();
}
}
public String getValueByNote(String targetNode) {
try {
Element root = this.document.getRootElement();
return this.getChildrenNodes(root, targetNode);
} catch (Exception var3) {
var3.printStackTrace();
return null;
}
}
public String getChildrenNodes(Element parentNode, String targetNode) {
String value = null;
boolean isFind = false;
try {
List<Element> elementList = parentNode.elements();
Iterator var6 = elementList.iterator();
while (var6.hasNext()) {
Element element = (Element) var6.next();
if (isFind) {
break;
}
if (element.getName() == targetNode) {
isFind = true;
value = element.getTextTrim();
}
if (!isFind) {
this.getChildrenNodes(element, targetNode);
}
}
} catch (Exception var8) {
var8.printStackTrace();
}
return value;
}
public Document getDocument() {
return this.document;
}
public void setDocument(Document document) {
this.document = document;
}
}
定时器
定时获取AccessToken,获取用户信息时需要。
@Configuration
@EnableScheduling
public class WechatBaseTask {
@Autowired
WechatBaseManager wechatBaseManager;
@PostConstruct
@Scheduled(cron = "0 0 0/1 * * ?")
private void tokenTask() {
wechatBaseManager.updateAccessToken();
}
}
版权声明:本文为weixin_45362084原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。