Java实现微信登陆授权流程详解
前期准备工作就不多说了,无非就是公众号平台账号、填写相关资耐心等待审核就好。
等通过之后,就可以看到测试用的 appId 和 appSecret 了,稍后我们要用到这两个 ID
接下来这里有一点要注意,在网址应用创建好之后的授权回调域填写顶级域名就好,微信文档里说的是,该域名下的所有页面都可以回调
代码实现微信授权
首先,我们要导入一些依赖,这里由于我当时找过好几个版本,依赖可能有些是多余的,都放在这里了QAQ
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.codehaus.xfire</groupId>
<artifactId>xfire-core</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
然后是是工具类:
解密手机号:GetUserInfoUtil
package com.ruoyi.common.utils;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidParameterSpecException;
import java.util.Arrays;
public class GetUserInfoUtil {
public static String getUserInfo(String encryptedData, String sessionKey, String iv) {
String result = "";
// 被加密的数据
byte[] dataByte = Base64.decode(encryptedData);
// 加密秘钥
byte[] keyByte = Base64.decode(sessionKey);
// 偏移量
byte[] ivByte = Base64.decode(iv);
try {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
parameters.init(new IvParameterSpec(ivByte));
// 初始化
cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
result = new String(resultByte, "UTF-8");
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidParameterSpecException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
}
解析微信接口:
package com.ruoyi.project.system.applet;
import org.apache.commons.httpclient.HttpStatus;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Set;
/**
1. HttpUtil工具类
*/
public class HttpUtil {
public static String doGet(String urlPath, HashMap<String, Object> params)
throws Exception {
StringBuilder sb = new StringBuilder(urlPath);
if (params != null && !params.isEmpty()) { // 说明有参数
sb.append("?");
Set<Entry<String, Object>> set = params.entrySet();
for (Entry<String, Object> entry : set) { // 遍历map里面的参数
String key = entry.getKey();
String value = "";
if (null != entry.getValue()) {
value = entry.getValue().toString();
// 转码
value = URLEncoder.encode(value, "UTF-8");
}
sb.append(key).append("=").append(value).append("&");
}
sb.deleteCharAt(sb.length() - 1); // 删除最后一个&
}
// System.out.println(sb.toString());
URL url = new URL(sb.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000); // 5s超时
conn.setRequestMethod("GET");
if (conn.getResponseCode() == HttpStatus.SC_OK) {// HttpStatus.SC_OK ==
// 200
BufferedReader reader = new BufferedReader(new InputStreamReader(
conn.getInputStream()));
StringBuilder sbs = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sbs.append(line);
}
// JSONObject jsonObject = new JSONObject(sbs.toString());
return sbs.toString();
}
return null;
}
}
接下来,就开始我们的表演了
首先,我们要通过前端给我们传的 code ,来调取接口获得自己需要的信息:
1、获取session_key
要获取用户的手机号,需要用到 session_key,那么这个 session_key 怎么获得呢,请看
@RestController
@RequestMapping("/login")
@Api(value = "微信登录模块")
public class AppletLogin implements Serializable{
private static final Logger log = LoggerFactory.getLogger(AppletLogin.class);
@Autowired
private ILoggerUserService loggerUserService;
/**
* 登录
* @param code
* @return
*/
@ApiOperation(value = "登录")
@PostMapping("/appletLogin")
public AjaxResult appletLogin(@Param("code")@RequestParam String code)
{
//校验请求参数
if(StringUtils.isEmpty(code)){
return AjaxResult.error("请求参数存在空值");
}
String AppId = "前面获取的AppId";
String AppSecret = "前面获取的AppSecret";
//请求微信服务器,用code换取openid、sessionKey
String result="";
try{
result = HttpUtil.doGet(
"https://api.weixin.qq.com/sns/jscode2session?appid="
+ AppId + "&secret="
+ AppSecret + "&js_code="
+ code
+ "&grant_type=authorization_code", null);
}
catch (Exception e) {
e.printStackTrace();
}
//解析从微信服务器上获取到的json字符串
JSONObject jsonObj = JSONObject.parseObject(result);
if(jsonObj == null){
return AjaxResult.error("服务器请求微信接口获取参数失败");
}
//封装返回的数据
HashMap<String,String> returnHashMap = new HashMap<>();
String sessionKey = jsonObj.get("session_key").toString();
String openId = jsonObj.get("openid").toString();
//GetUserInfoUtil.getUserInfo()
returnHashMap.put("sessionKey",sessionKey);
returnHashMap.put("openId",openId);
return AjaxResult.success("用户信息",returnHashMap);
}
}
这里的 AppId、AppSecret 都是一开始就知道的,我们直接放到里面,用前端传的 code 值,调用接口,返回 sessionKey、openId,这里的两个值我们下面都要用到。
2、解密手机号
解密手机号,我们要用到三个参数,sessionKey、encryptedData、iv,已知 sessionKey 我们有了,那么剩下两个呢?别急,这两个参数是前端给我们传的,我们直接拿过来用就可以了
/**
* 根据opendid 添加用户手机号
* @param sessionKey
* @param encryptedData
* @param iv
* @return
*/
@ApiOperation(value = "添加用户手机号")
@GetMapping ("/phone")
public AjaxResult phone(String sessionKey, String encryptedData, String iv){
if(StringUtils.isEmpty(sessionKey) || StringUtils.isEmpty(encryptedData) || StringUtils.isEmpty(iv)){
return AjaxResult.error("请求参数存在空值");
}
// 微信小程序--手机号解密
String json = AES.wxDecrypt(encryptedData, sessionKey, iv);
//json:{"phoneNumber":"17865181175","purePhoneNumber":"17865181175","countryCode":"86","watermark":{"timestamp":1587460231,"appid":"wxe493cddc6e0d931c"}}
if(StringUtils.isEmpty(json)){
return AjaxResult.error("未获取到手机号");
}
Map maps = (Map)JSON.parse(json);
String phoneNumber = (String)maps.get("phoneNumber");
//返回用户信息
HashMap<String,Object> returnHashMap = new HashMap<>();
returnHashMap.put("phoneNumber",phoneNumber);
return AjaxResult.success("用户信息",returnHashMap);
}
注意,这里的 AES.wxDecrypt() 方法,所用到的工具类就是我们前面有提到的,解密手机号的工具类,当然了,也可以直接写在接口里,我只是封装了一下,不然代码看起来就比较乱了。
到了这一步,用户的手机号,我们就拿到了,大功告成!
3、那么用户信息呢?
用户的昵称、头像、地址等个人信息(不包括手机号),这些我们怎么拿到呢?这里就不得不说一下微信比较坑的地方了,以前没有改版的时候,通过上面的方法,我们可以直接获取到用户的个人信息和手机号,现在不行了,要么就按上面的步骤只能拿手机号,要么就按下面的步骤,拿用户信息,请看
@RestController
@RequestMapping("/login")
@Api(value = "微信登录模块")
public class AppletLogin implements Serializable{
private static final Logger log = LoggerFactory.getLogger(AppletLogin.class);
@Autowired
private ILoggerUserService loggerUserService;
/**
* 登录
* @param code
* @return
*/
@ApiOperation(value = "登录")
@PostMapping("/appletLogin")
public AjaxResult appletLogin(@Param("accessToken")@RequestParam String accessToken, @Param("openId")@RequestParam String openId)
{
//校验请求参数
if(StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(openId)){
return AjaxResult.error("请求参数存在空值");
}
//请求微信服务器,用code换取openid
String result="";
try{
result = HttpUtil.doGet(
"https://api.weixin.qq.com/sns/userinfo?access_token="
+ accessToken + "&openid="
+ openId + "&lang=zh_CN", null);
}
catch (Exception e) {
e.printStackTrace();
}
//解析从微信服务器上获取到的json字符串
JSONObject userinfo= JSONObject.parseObject(result);
if(userinfo== null){
return AjaxResult.error("服务器请求微信接口获取参数失败");
}
/*{
"openid":" OPENID",
"nickname": NICKNAME,
"sex":"1",
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}*/
return AjaxResult.success("用户信息",userinfo);
}
}
这是返回的参数信息
这里有些小伙伴就要问了,openId 我知道,是通过 code 拿到的,那这个 accessToken 是怎么获取的呢? 坑来了!!!accessToken 需要 code 通过调用另一个接口获得,那么在已知,code 只能使用一次的情况下,我们怎么通过 code 又获取 sessionKey,又获取 accessToken 呢?
不要慌,我有完美的解决方案:那就让用户授权两次呗!
让前端通过第二次授权,去获取用户的头像、昵称,然后传给我们,我们在根据手机号、openId,对用户的信息的唯一性进行确认,并存入数据库中。
4、保存用户数据
/**
* 获取用户信息
* @return
*/
@ApiOperation(value = "获取用户信息")
@PostMapping("/getUserInfo")
public AjaxResult getUserInfo(@ApiParam("用户手机号") @RequestParam String phoneNumber,
@ApiParam("openId") @RequestParam String openId,
@ApiParam("微信昵称")@RequestParam String nickname,
@ApiParam("头像路径")@RequestParam String headimgurl,
@ApiParam(value = "上级业务员id") @RequestParam(required = false)Integer id) {
if(StringUtils.isEmpty(phoneNumber) || StringUtils.isEmpty(openId) || StringUtils.isEmpty(nickname) || StringUtils.isEmpty(headimgurl)){
return AjaxResult.error("请求参数存在空值");
}
//去数据库判断是否存在该用户
LoggerUser loggerUser = loggerUserService.selectLoggerUserByOpenId(openId);
//如果存在该用户
if(StringUtils.isNotNull(loggerUser)){
//一、用户是客户经理
CemManager manager = managerService.selectCemManagerByMobile(phoneNumber);
if (StringUtils.isNotNull(manager)){//设置客户经理id
loggerUser.setHold2(manager.getManagerId().toString());
}
//二、用户有上级业务员,且开启
if (id!=null){
CemManager byId = managerService.selectCemManagerById(id.longValue());
if (StringUtils.isNotNull(byId)){//设置上级业务员id
if (byId.getState()==0){
loggerUser.setHold1(byId.getManagerId().toString());
}
//三、用户有上级业务员,且关闭
}
}
//四、用户用户有上级业务员,但已删除
//五、用户无上级业务员
if(StringUtils.isNotEmpty(loggerUser.getMobile())){
//将用户手机号覆盖
loggerUser.setMobile(phoneNumber);
}
//昵称
loggerUser.setWxName(nickname);
//图像路径
loggerUser.setHold4(headimgurl);
//保存用户信息
loggerUserService.updateLoggerUser(loggerUser);
return AjaxResult.success(loggerUser);
}
//如果是新用户,就添加用户到数据库中
LoggerUser newLoggerUser = new LoggerUser();
//一、用户是客户经理
CemManager manager = managerService.selectCemManagerByMobile(phoneNumber);
if (StringUtils.isNotNull(manager)){//设置客户经理id
newLoggerUser.setHold2(manager.getManagerId().toString());
}
//二、用户有上级业务员,且开启
if (id!=null){
CemManager byId = managerService.selectCemManagerById(id.longValue());
if (StringUtils.isNotNull(byId)){//设置上级业务员id
if (byId.getState()==0){
newLoggerUser.setHold1(byId.getManagerId().toString());
}
//三、用户有上级业务员,且关闭
}
}
//四、用户用户有上级业务员,但已删除
//五、用户无上级业务员
//openId
newLoggerUser.setHold3(openId);
//昵称
newLoggerUser.setWxName(nickname);
//手机号
newLoggerUser.setMobile(phoneNumber);
//图像路径
newLoggerUser.setHold4(headimgurl);
//新增用户信息
loggerUserService.insertLoggerUserInfo(newLoggerUser);
return AjaxResult.success(newLoggerUser);
}
现在,我们就在用户登陆授权后,获取了一条完整的用户信息,包括手机号、昵称、头像等个人信息,当然了,可别忘记这是需要用户授权两次的,上面的步骤,就按一、二、四来就可以了,记得要跟前端小伙伴沟通好哦!
好事定律:每件事最后都会是好事,如果不是好事,说明还没到最后。