Authorized login of WeChat applet - Java backend (Spring boot)

WeChat development document link: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

1. Prerequisites

  • A WeChat applet that can be tested
  • The APPID and APPscret of this WeChat applet (obtained from the developer background)

2. Development process

From the timing diagram, we can understand that the process is roughly divided into two steps:

  • The applet gets the code and passes it to the Java background
  • Get the open_id from the WeChat background interface after the Java background gets the code

2.1 Small program terminal (what the front end needs to do)


Call wx.login() on the front end of the WeChat applet to obtain a code. This code is like a key for us to obtain user information from the WeChat background server. WeChat gives the user a choice of whether to authorize or not through the process of obtaining this code. If When the user chooses authorization, a code will be returned. This code is one-time and time-limited.

Here is a simple explanation. First, the applet calls wx.login() to get the code, and then uses wx.getUserInfo() to get the user information (the request login and getUserInfo are together, and the two requests The data merged and sent to the login interface of the server), through the request, send:

1.code //Temporary login credentials
// If you do not agree to obtain user information, the following four parameters cannot be obtained
2.rawData //User non-sensitive information, such as avatars and nicknames
3.signature //sign
4.encryteDate //User sensitive information, which needs to be decrypted, (including unionID)
5.iv //vector of decryption algorithms

Give to the server, the server according to appid+secret+js_code+grant_type

Go to request, get session_key and openid (unionID cannot be obtained here), use session_key, iv to decrypt encryptedDate to get user sensitive information and unionID, and save user information to the database. Then, we save the sesssoin_key and openid and associate them with the token (custom login status), and finally return the data required by the applet to the applet side, and then use the token to maintain the user login status.
User table structure design:

CREATE TABLE `wechat_user` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `token` varchar(100) NOT NULL COMMENT 'token',
 `nickname` varchar(100) DEFAULT NULL COMMENT 'User's Nickname',
 `avatar_url` varchar(500) DEFAULT NULL COMMENT 'profile picture',
 `gender` int(11) DEFAULT NULL COMMENT 'sex 0-unknown, 1-male, 2-female',
 `country` varchar(100) DEFAULT NULL COMMENT 'Country',
 `province` varchar(100) DEFAULT NULL COMMENT 'province',
 `city` varchar(100) DEFAULT NULL COMMENT 'City',
 `mobile` varchar(100) DEFAULT NULL COMMENT 'phone number',
 `open_id` varchar(100) NOT NULL COMMENT 'Applets openId',
 `union_id` varchar(100) DEFAULT '' COMMENT 'Applets unionId',
 `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'insert time',
 `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'update time',
 `deleted_at` timestamp NULL DEFAULT NULL COMMENT 'delete time',
 PRIMARY KEY (`id`),
 KEY `idx_open_id` (`open_id`),
 KEY `idx_union_id` (`union_id`),
 KEY `idx_mobile` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='Mini Program User Table';

specific code

Note, if @Getter reports an error, delete it, add Getter and Setter yourself, and the annotation at the beginning of @Api is swagger Notes, unnecessary can be deleted
request class

@ApiModel
@Getter
@Setter
public class WechatLoginRequest {
    @NotNull(message = "code Can not be empty")
    @ApiModelProperty(value = "WeChat code", required = true)
    private String code;
    @ApiModelProperty(value = "User non-sensitive fields")
    private String rawData;
    @ApiModelProperty(value = "sign")
    private String signature;
    @ApiModelProperty(value = "User Sensitive Fields")
    private String encryptedData;
    @ApiModelProperty(value = "decryption vector")
    private String iv;
}

Non-sensitive information DO

@Getter
@Setter
public class RawDataDO {
    private String nickName;
    private String avatarUrl;
    private Integer gender;
    private String city;
    private String country;
    private String province;
}

User DO

@Getter
@Setter
public class WechatUserDO {
    private Integer id;
  
    private String token;
  
    private String nickname;
 
    private String avatarUrl;
 
    private Integer gender;
 
    private String country;
 
    private String province;
 
    private String city;
 
    private String mobile;
 
    private String openId;
 
    private String unionId;
 
    private String createdAt;
 
    private String updatedAt;
}

HttpClientUtils

public class HttpClientUtils {
  
    final static int TIMEOUT = 1000;
 
    final static int TIMEOUT_MSEC = 5 * 1000;   
  
    public static String doPost(String url, Map<String, String> paramMap) throws IOException {
        // Create Httpclient object
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";
 
        try {
            // Create Http Post request
            HttpPost httpPost = new HttpPost(url);
 
            // Create parameter list
            if (paramMap != null) {
                List<NameValuePair> paramList = new ArrayList<>();
                for (Entry<String, String> param : paramMap.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                // mock form
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }
 
            httpPost.setConfig(builderRequestConfig());
 
            // execute http request
            response = httpClient.execute(httpPost);
 
            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                throw e;
            }
        }
 
        return resultString;
    }
  
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }
}

service

public interface WechatService {
    Map<String, Object> getUserInfoMap(WechatLoginRequest loginRequest) throws Exception;
}

Service impl

@Service
public class WechatServiceImpl implements WechatService {
    private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session";
    private static final String  = "authorization_code";
  
    @Override
    public Map<String, Object> getUserInfoMap(WechatLoginRequest loginRequest) throws Exception {
        Map<String, Object> userInfoMap = new HashMap<>();
        // If the logger reports an error, just delete it, or replace it with your own log object
        logger.info("Start get SessionKey,loginRequest The data is:" + JSONObject.toJSONString(loginRequest));
        JSONObject sessionKeyOpenId = getSessionKeyOrOpenId(loginRequest.getCode());
        // ErrorCodeEnum here is a custom error field, which can be deleted and handled in your own way
        Assert.isTrue(sessionKeyOpenId != null, ErrorCodeEnum.P01.getCode());
 
        // Get openId && sessionKey
        String openId = sessionKeyOpenId.getString("openid");
        // ErrorCodeEnum here is a custom error field, which can be deleted and handled in your own way
        Assert.isTrue(openId != null, ErrorCodeEnum.P01.getCode());
        String sessionKey = sessionKeyOpenId.getString("session_key");
        WechatUserDO insertOrUpdateDO = buildWechatUserDO(loginRequest, sessionKey, openId);
 
        // Save openId and sessionKey according to code
        JSONObject sessionObj = new JSONObject();
        sessionObj.put("openId", openId);
        sessionObj.put("sessionKey", sessionKey);
        // The set method here, imports the Redis of your own project by itself, and replaces the key by itself, where 10 means 10 days
        stringJedisClientTem.set(WechatRedisPrefixConstant.USER_OPPEN_ID_AND_SESSION_KEY_PREFIX + loginRequest.getCode(),
                sessionObj.toJSONString(), 10, TimeUnit.DAYS);
 
        // Query users according to openid, the query service here is written by yourself, so it will not be posted
        WechatUserDO user = wechatUserService.getByOpenId(openId);
        if (user == null) {
            // The user does not exist, the insert user, a distributed lock is added here to prevent duplicate users from inserting, depending on your business, decide whether to use this code
            if (setLock(WechatRedisPrefixConstant.INSERT_USER_DISTRIBUTED_LOCK_PREFIX + openId, "1", 10)) {
              // The user enters the database, and the service writes it by itself
              insertOrUpdateDO.setToken(getToken())
              wechatUserService.save(insertOrUpdateDO);
              userInfoMap.put("token", insertOrUpdateDO.getToken())
            }
        } else {
            userInfoMap.put("token", wechatUser.getToken());
            // It already exists, do the existing processing, such as updating the user's avatar, nickname, etc., update according to openID, write the code here by yourself
            wechatUserService.updateByOpenId(insertOrUpdateDO);
        }
 
        return userInfoMap;
    }
  
    // The JSONObject here is Ali's fastjson, imported by maven
    private JSONObject getSessionKeyOrOpenId(String code) throws Exception {
        Map<String, String> requestUrlParam = new HashMap<>();
        // Applet appId, add it yourself
        requestUrlParam.put("appid", APPID);
        // Small program secret, add it yourself
        requestUrlParam.put("secret", SECRET);
        // The code returned by the applet
        requestUrlParam.put("js_code", code);
        // default parameters
        requestUrlParam.put("grant_type", GRANT_TYPE);
 
        // Send a post request to read and call the WeChat interface to obtain the unique ID of the openid user
        String result = HttpClientUtils.doPost(REQUEST_URL, requestUrlParam);
        return JSON.parseObject(result);
    }
  
    private WechatUserDO buildWechatUserAuthInfoDO(WechatLoginRequest loginRequest, String sessionKey, String openId){
        WechatUserDO wechatUserDO = new WechatUserDO();
        wechatUserDO.setOpenId(openId);
 
        if (loginRequest.getRawData() != null) {
            RawDataDO rawDataDO = JSON.parseObject(loginRequest.getRawData(), RawDataDO.class);
            wechatUserDO.setNickname(rawDataDO.getNickName());
            wechatUserDO.setAvatarUrl(rawDataDO.getAvatarUrl());
            wechatUserDO.setGender(rawDataDO.getGender());
            wechatUserDO.setCity(rawDataDO.getCity());
            wechatUserDO.setCountry(rawDataDO.getCountry());
            wechatUserDO.setProvince(rawDataDO.getProvince());
        }
 
        // Decrypt the encrypted information and get the unionID
        if (loginRequest.getEncryptedData() != null){
            JSONObject encryptedData = getEncryptedData(loginRequest.getEncryptedData(), sessionKey, loginRequest.getIv());
            if (encryptedData != null){
                String unionId = encryptedData.getString("unionId");
                String phone = encryptedData.getString("phoneNumber");
                wechatUserDO.setUnionId(unionId);
            }
        }
 
        return wechatUserDO;
    }
  
    private JSONObject getEncryptedData(String encryptedData, String sessionkey, String iv) {
        // encrypted data
        byte[] dataByte = Base64.decode(encryptedData);
        // encryption key
        byte[] keyByte = Base64.decode(sessionkey);
        // Offset
        byte[] ivByte = Base64.decode(iv);
        try {
            // If the key is less than 16 bits, then make up. The content of this if is very important
            int base = 16;
            if (keyByte.length % base != 0) {
                int groups = keyByte.length / base + 1;
                byte[] temp = new byte[groups * base];
                Arrays.fill(temp, (byte) 0);
                System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
                keyByte = temp;
            }
            // initialization
            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);// initialization
            byte[] resultByte = cipher.doFinal(dataByte);
            if (null != resultByte && resultByte.length > 0) {
                String result = new String(resultByte, "UTF-8");
                return JSONObject.parseObject(result);
            }
        } catch (Exception e) {
            logger.error("Error in decrypting encrypted information", e.getMessage());
        }
        return null;
    }
  
    private boolean setLock(String key, String value, long expire) throws Exception {
        boolean result = stringJedisClientTem.setNx(key, value, expire, TimeUnit.SECONDS);
        return result;
    }
  
    private String getToken() throws Exception {
        // Here you can customize the token generation strategy, you can use UUID+sale for MD5
        return "";
    }
}

Controller

@RestController("LoginController")
@RequestMapping(value = "/wechat/login")
public class LoginController {
    @Resource
    WechatService wechatService;
    
    @ApiOperation(value = "1.login interface", httpMethod = "POST")
    @PostMapping("/save")
    public Map<String, Object> login(
            @Validated @RequestBody WechatLoginRequest loginRequest) throws Exception {
 
        Map<String, Object> userInfoMap = wechatService.getUserInfoMap(loginRequest);
        return userInfoMap;
    }
}

write at the end

Some notes:

  • The code is time-sensitive, valid within 5 minutes, and can only be used once
  • The implementation of the token, as well as the expiration time of the token, whether the token is placed in the database or in the cache, does the token need to be refreshed every time you log in? For these questions, you can make your own judgments based on business needs. For simplicity, I put them directly in the database.

Tags: Java Mini Program Spring Boot

Posted by Whear on Sun, 02 Apr 2023 08:02:49 +0930