Login Flow
Flow Overview
First, the Mini-App obtains a login credential code via mos.login().
- Each invocation generates a unique code with a 5-minute validity period. This code will become invalid immediately after being verified once by the MOS interface service.
After obtaining the code, the Mini-App sends this code to its backend service.
The Mini-App backend service sends the code, appKey, and sign (signature) to the MOS interface service for login credential verification. Upon successful validation, MOS returns the current logged-in user's session_key (session information record) and openid (unique user identifier).
Upon successful user login, the Mini-App backend service may store the openid and session_key, then generate a custom authentication token to return to the Mini-App. This token can be used to query the corresponding openid and session_key. For subsequent requests, the Mini-App only needs to include this token to demonstrate authenticated status.
Tip
The Mini-App backend service can associate the obtained openid with its user system after receiving it, thereby establishing a connection between the Mini-App user and the MOS user system.
Frontend Implementation
Mini-App Frontend Example Code
// Call mos.login to get login credential code
mos.login("your_app_key").then((res) => {
// Mini-app backend service login
axios.post("https://demo-test.miniapp.xxx/api/login/miniAppLogin", {
code: res.data.code,
}).then((res1) => {
// Save Token to cache, carry it in subsequent requests
localStorage.setItem("token", res1.data);
});
});
Mini-App Backend Service Example Code
package testproject.service.impl;
import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import testproject.domain.vo.TokenVo;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.*;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
/**
* Mini-app user common service
*
* @author tom
*/
@Slf4j
@Component
public class MiniAppCommonService {
// Login signing key
public static final String LOGIN_SIGNING_KEY = "your_complex_key_it_can_be_random";
/**
* Mos mini-app call address
*/
@Value("${mos.mini-app.base-url}")
private String baseUrl;
/**
* Mos mini-app appKey
*/
@Value("${mos.mini-app.app-key}")
private String appKey;
/**
* Mos mini-app appSecret
*/
@Value("${mos.mini-app.app-secret}")
private String appSecret;
@Autowired
private Environment env;
private final RestTemplate restTemplate;
public MiniAppCommonService() {
SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
httpRequestFactory.setConnectTimeout(30000);
httpRequestFactory.setReadTimeout(60000);
this.restTemplate = new RestTemplate(httpRequestFactory);
}
/**
* Request Mos server mini-app open interface
*
* @param uri Interface URI
* @param ao Request parameters
* @param clazz Response type
* @return Response object
*/
private <T> T call(String uri, BaseSignedAo ao, Class<T> clazz) {
// Signature
ao.setAppKey(this.appKey);
Map<String, String> data = JSON.parseObject(JSON.toJSONString(ao), new TypeReference<Map<String, String>>() {
});
String sign = MiniAppUtil.generateSignature(data, this.appSecret);
ao.setSign(sign);
// Send request
String url = this.baseUrl + uri;
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, ao, String.class);
if (responseEntity.getStatusCode() != HttpStatus.OK || responseEntity.getBody() == null) {
log.error("Mini-app request exception uri={} ao={}", uri, JSON.toJSONString(ao));
return null;
}
// Parse response code(int) message(String) data(T)
JSONObject result = JSON.parseObject(responseEntity.getBody());
if ((int)result.get("code") != 200) {
log.error("Mini-app request exception uri={} ao={} result={}", uri, JSON.toJSONString(ao), JSON.toJSONString(result));
return null;
}
return JSON.to(clazz, result.get("data"));
}
public MiniAppUserBo getMiniAppUser() {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String appKey = (String) request.getAttribute("appKey");
String openid = (String) request.getAttribute("openid");
String language = request.getHeader("Lang");
LanguageEnum languageType;
if (StringUtils.isBlank(language)) {
languageType = LanguageEnum.EN_US;
} else {
languageType = LanguageEnum.getByCode(language);
}
return new MiniAppUserBo(appKey, openid, languageType);
}
public TokenVo miniAppLogin(String code) {
MosSessionVo mosSessionVo = this.code2session(code);
if (mosSessionVo == null) {
return null;
}
String token = Jwts.builder()
.setSubject(this.appKey + "-" + mosSessionVo.getOpenid())
.setIssuedAt(new Date())
// Valid for 360 days
.setExpiration(new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 360))
.signWith(SignatureAlgorithm.HS256, LOGIN_SIGNING_KEY).compact();
return new TokenVo().setToken(token);
}
public MosSessionVo code2session(String code) {
Code2SessionAo ao = new Code2SessionAo().setCode(code);
return this.call("/open-apis/mp/v1/auth/code2session", ao, MosSessionVo.class);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class MosSessionVo {
/**
* openid
*/
private String openid;
/**
* session key
*/
private String sessionKey;
}
@Data
public static class BaseSignedAo {
/**
* Mini-app appKey
*/
@NotNull
private String appKey;
/**
* Signature
*/
@NotNull
private String sign;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public static class Code2SessionAo extends BaseSignedAo {
/**
* Code obtained through mos.login
*/
@NotNull
private String code;
}
@Getter
@AllArgsConstructor
public static enum LanguageEnum {
ZH_CN("zh-CN", "zh-CN"),
ZH_HK("zh-HK", "zh-HK"),
ZH_TW("zh-TW", "zh-TW"),
EN_US("en-US", "en-US"),
KM_KH("km-KH", "km-KH"),
VI_VN("vi-VN", "vi-VN"),
TH_TH("th-TH", "th-TH"),
MS_MY("ms-MY", "ms-MY"),
JA_JP("ja-JP", "ja-JP"),
KO_KR("ko-KR", "ko-KR"),
ID_ID("id-ID", "id-ID"),
LO_LA("lo-LA", "lo-LA"),
HI_IN("hi-IN", "hi-IN"),
;
@EnumValue
@JsonValue
private final String code;
private final String desc;
public static LanguageEnum getByCode(String code) {
if (code == null || code.isEmpty()) {
return null;
}
String[] paramSplits = code.split("-");
if (paramSplits.length > 1) {
for (LanguageEnum languageEnum : LanguageEnum.values()) {
if (languageEnum.getCode().equals(code)) {
return languageEnum;
}
}
}
// Compatibility data, previously passed zh and en
else {
for (LanguageEnum languageEnum : LanguageEnum.values()) {
String[] enumSplits = languageEnum.getCode().split("-");
if (enumSplits[0].equalsIgnoreCase(code)) {
return languageEnum;
}
}
}
return null;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class MiniAppUserBo {
/**
* appKey
*/
private String appKey;
/**
* openid
*/
private String openid;
/**
* Language type zh-CN Chinese en-US English
*/
private LanguageEnum languageType;
}
public static class MiniAppUtil {
/**
* Generate signature
*/
public static String generateSignature(Map<String, String> data, String appSecret) {
// 1. Sort the parameters alphabetically
Map<String, String> sortedData = new TreeMap<>(data);
// 2. Concatenate the parameters in the format key1=value1&key2=value2...
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedData.entrySet()) {
if ("sign".equals(entry.getKey()) || entry.getValue() == null || entry.getValue().isEmpty()) {
// sign does not participate in signing && empty values do not participate in signing
continue;
}
sb.append(entry.getKey()).append("=").append(entry.getValue().trim()).append("&");
}
sb.append("secret=").append(appSecret);
// 3. Hash the concatenated string using MD5
return new MD5().digestHex(sb.toString());
}
}
}
import testproject.domain.vo.LoginAo;
import testproject.domain.vo.TokenVo;
import testproject.service.impl.MiniAppCommonService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* The type Mini-app controller.
*/
@RestController
@RequestMapping("/mp/")
public class MiniAppController {
/**
* The Mini-app common service.
*/
@Resource
private MiniAppCommonService miniAppCommonService;
/**
* Mini-app login
*
* @param param the login parameters
* @return the token
*/
@PostMapping("/login/miniAppLogin")
public TokenVo miniAppLogin(@RequestBody @Validated LoginAo param) {
// Directly call the common login method provided by the common module to get Token
TokenVo tokenVo = miniAppCommonService.miniAppLogin(param.getCode());
if(tokenVo == null) {
throw new RuntimeException("login failed, checking the parameter of code");
}
// check is the openid new in your db
// ... do it use tokenVo.getOpenId
return tokenVo.getToken();
}
}
package testproject.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author tom
*/
@Data
@Accessors(chain = true)
public class TokenVo {
/**
* token
*/
private String token;
/** the mosapp user relative id, don't share to frontend **/
private String openId;
}
package testproject.domain.vo;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* Exchange login token through code
*
* @author zhi
* @since 2024/7/16
*/
// Exchange mos user info through code input parameters
@Data
public class LoginAo {
@NotNull
// Code obtained through mos.login
private String code;
}
// Finally, need to add filter
package testproject.interceptor;
import testproject.service.impl.MiniAppCommonService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.Getter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
/**
* @author Login authentication interceptor
*/
@Getter
@Configuration
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equalsIgnoreCase("OPTIONS")) {
return true;
}
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = Jwts.parser()
.setSigningKey(MiniAppCommonService.LOGIN_SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
String[] subjects = claims.getSubject().split("-");
if (subjects.length == 2 && subjects[0] != null && subjects[1] != null && !this.isTokenExpired(token)) {
request.setAttribute("appKey", subjects[0]);
request.setAttribute("openid", subjects[1]);
return true;
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token");
return false;
}
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token");
return false;
}
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header");
return false;
}
}
private boolean isTokenExpired(String token) {
Claims claims = Jwts.parser()
.setSigningKey(MiniAppCommonService.LOGIN_SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
return claims.getExpiration().before(new Date());
}
}
package testproject.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfigure implements WebMvcConfigurer {
private static final String[] EXCLUDE_PATH_PATTERNS_FOR_SWAGGER = {"/doc.html", "/v2/api-docs", "/swagger-resources/**", "/webjars/**"};
@Override
public void addInterceptors(InterceptorRegistry registry) {
// Register custom interceptor
registry.addInterceptor(new LoginInterceptor())
// Intercept all mp mini-app requests
.addPathPatterns("/mp/**")
// Exclude swagger interface documentation
.excludePathPatterns(EXCLUDE_PATH_PATTERNS_FOR_SWAGGER)
// Exclude mini-app login path
.excludePathPatterns("/mp/login/miniAppLogin");
}
}
Spring Security Configuration
If using Spring Security for security authentication, you need to add corresponding permissions in the configuration: