background
Login is a basic function of a project, and it is the only necessary path to determine a user. Only after the login is successful, can the business interface be called and the data interaction with the server be carried out.
The design of a project’s login scheme needs to take into account all aspects, and small programs are especially complicated because there is an authorization step in the middle
For robustness and efficiency, we designed the entire login solution step by step. Note that the solution is based on the Taro framework and uses the React syntax
The basic flow
First take a look at the official small program login process sequence diagram
Is it complicated? Don’t be scared. In fact, we can do all kinds of data interaction and verification without strictly following the above flow chart
Leaving aside the applet layer for a moment, let’s think about what the login interface for a normal project looks like. In fact, the user takes the account and password as parameters to the server. After verification or registration, the server generates a token and returns it. Then we save the token, which is considered as a successful login
Obtaining mobile phone Number
Based on this logic, in fact, we only need to obtain the mobile phone number bound to the user to complete the login process. So how to get the user bound to wechat mobile phone number? At this time the role of the small program is reflected, we can pop up the phone number authorization window to allow the user to obtain his phone number, and then give the phone number to the back end.
However, due to the restriction of wechat privacy specification, we cannot directly get the phone number. We need to decrypt the phone number with iv+encryptedData obtained through authorization callback. The secret key used for decryption is session_key mentioned in the above flowchart, but we do not need to directly access this field in the front end. Session_key is the code obtained by wx.login() by calling the login credential verification interface of wechat. We can directly call wx.login() to get the code, send the code, IV, and encryptedData to the server, which decrypts the phone number for login.
After the server logs in, it not only returns the token, but also returns the user’s mobile phone number, which is saved in the cache. Next time the token is invalid, you can directly log in with the mobile phone number.
The login button
Another point to note is that wechat has updated the user privacy specification in recent years, and the authorization window can only be triggered by clicking the specified button, instead of being activated through API, so we need to encapsulate a login button
One thing to note
Invoking the wx.login login in the callback may refresh the login state. In this case, the sessionKey exchanged by code is not the sessionKey used for encryption, causing decryption failure. Developers are advised to login in advance
So we have to make sure that wx.login is called before the authorization popover pops up, optionally after the button component is built
// LoginButton.ts const LoginButton = React.memo((props: IProps) => { const { children, onSuccess, onClick, ... other } = props; const [code, setCode] = useState(''); useEffect(() => { if (! Wx.login () login.getwxcode ().then(wxCode => {setCode(wxCode); }); } }, [code]); const handleGetPhoneNumber = async (e: BaseEventOrig<ButtonProps.onGetPhoneNumberEventDetail>) => { const { iv, encryptedData, errMsg } = e.detail; try { if (! Code) {toast.info ('code is empty '); return; } if (errmsg.includes ('ok')) {Taro. ShowLoading ({title: 'in'}); await Login.login({ iv, encryptedData, code: code }); Taro.hideLoading(); onSuccess? . (); Toast.success(' login succeeded '); }} catch (error) {// Relogin login.getwxcode (). Then (wxCode => {setCode(wxCode); }); Taro.hideLoading(); Toast.info(error.message || error.errMsg); }}; const handleBtnClick = async event => { onClick? .(event); }; return ( // @ts-ignore <Button openType="getPhoneNumber" onClick={handleBtnClick} onGetPhoneNumber={handleGetPhoneNumber} {... other}> {children} </Button> ); });Copy the code
The login logic
class Login { ... Async login({iv, encryptedData, code}: {iv: string; encryptedData: string; code: string }) { const [err, data] = await login({ code: code, iv: iv, encryptedData: encryptedData, }); if (err) { Toast.info(err.message); throw err; } else { await this.saveLoginData(data); Async saveLoginData(loginData: IMaLoginRet) {if (! loginData) { return; } const { phone, userId, tokenId } = loginData; await Taro.setStorage({ key: StorageKey.USER_DATA, data: { phone, userId, }, }); await Taro.setStorage({ key: StorageKey.ACCESS_TOKEN, data: tokenId, }); }... }Copy the code
The flow chart
To sum up, the login of small program is still normal business login process, small program is only authorized to provide a mobile phone number for business login use, understand the principle, we can draw a basic small program login flow chart
The time when the login is triggered
With this basic login process in mind, the next question is: Where can this login process be used?
Login Scenarios
A basic applet project with at least two login scenarios:
1. The login button is triggered by clicking
This is the most common scenario. Generally, when the user does not log in, there will be a button on the page of personal center to prompt the user to log in. This button can directly use the login button encapsulated above and adjust the style
2. The page is triggered
A simple button click trigger is not enough. We hope that every page that needs to be logged in can be automatically logged in if the user has not logged in. This is especially important for pages opened through sharing
Let’s focus on the second scenario
The login process is displayed
To implement such a requirement, we need to consider three issues
1. How to check whether you have logged in
To determine whether you have logged in, you only need to check whether the token in the cache exists. As for the login expiration, we do not consider it here. To avoid multiple interface calls, the expiration will be discussed later
2. How to bypass wechat’s authorization restrictions
As mentioned above, there is no way to authorize users by calling the API directly, but only by using the button. Therefore, it is impossible for all pages to have a permanent login button for you to log in. How to do this?
To achieve this, we can use a popup window to pop up a prompt window for the user whether to log in or not, and place the login button that we have encapsulated above at the bottom, so that the user can click the login.
3. How to reuse login logic
We can’t ask every developer to write the log-in logic for one page, we need to separate the logic and be transparent to the page developer
How do you reuse it? Our first thought was to write a utility method, put the logon logic in it, and call the utility method every time the business interface is called.
What’s wrong with this approach?
- Not enough transparent
Page developers still need to call login methods in componentDidMount, which is not transparent to developers
- Difficult to set up custom pop-ups
The login popover needs to be implemented using custom popovers, and the popover component needs to be placed inside Render (), no method needs to be used inside a tool method
- Difficult to clear event listening
If the tool method sets event listening, the listener needs to be cleared at the end of the page, which also cannot be done
Hijack component
In response to the above problem, we have abandoned this unfriendlyapproach and adopted another approach: hijacking class components, using the React high-order component mechanism, by hijacking the componentDidMount, Render method of the class component to implement the login logic injection. Let’s look at the code
function WithLogin() { return function withComponent<T extends ComponentClass>(Component: T) { // @ts-ignore return class extends Component { loginConfirmRef: IConfirmRef | null = null componentDidMount = async () = > {/ / testing whether there is a token const hasToken = Login. CheckLogin (); // If (! hasToken) { this.loginConfirmRef? . Show ({title: 'you are not logged in ', message:' Log in for more information ', buttons: [{text: 'cancel ', type: 'default', onClick: () => { this.loginConfirmRef?.hide(); }, }, <LoginButton type="primary" onClick={() => {this.loginconfirmref?.hide();}} > </LoginButton>,],}); } else { super.componentDidMount?.(); }}; render() { return ( <View> {super.render()} <Confirm ref={ref => { this.loginConfirmRef = ref; }} /> </View> ); }}; }; }Copy the code
ComponentDidMount = componentDidMount = componentDidMount = componentDidMount = componentDidMount
The main purpose of hijacking Render is to embed a custom popover component into the page, which can also dynamically set the style of the unlogged page on demand
Event listeners
Let’s see, what else is missing? Do we need to do anything to log in successfully?
Yes, you need to refresh the page. Here we need to use the mechanism of event monitoring, register a successful login event monitoring, after successful login to trigger the corresponding event, the modified code is as follows
withLogin.ts
function WithLogin() { return function withComponent<T extends ComponentClass>(Component: T) { // @ts-ignore return class extends Component { loginConfirmRef: IConfirmRef | null = null; ComponentDidMount onLoginSuccess = () => {super.componentDidMount?.(); }; ComponentDidMount = async () => {// Check for token const hasToken = login.checkLogin (); // If (! hasToken) { Taro.eventCenter.on(EventName.LOGIN_SUCCESS, this.onLoginSuccess); // This. LoginConfirmRef? . Show ({title: 'you are not logged in ', message:' Log in for more information ', buttons: [{text: 'cancel ', type: 'default', onClick: () => { this.loginConfirmRef?.hide(); }, }, <LoginButton type="primary" onClick={() => {this.loginconfirmref?.hide();}} > </LoginButton>,],}); } else { super.componentDidMount?.(); }}; componentWillUnmount = () => { Taro.eventCenter.off(EventName.LOGIN_SUCCESS, this.onLoginSuccess); // Clear the listener}; render() { return ( <View> {super.render()} <Confirm ref={ref => { this.loginConfirmRef = ref; }} /> </View> ); }}; }; }Copy the code
Login.ts
async login({ iv, encryptedData, code }: { iv: string; encryptedData: string; code: string }) { const [err, data] = await maAuthorizeLogin({ code: code, iv: iv, encryptedData: encryptedData, }); if (err) { Toast.info(err.message); throw err; } else { console.log(data); await this.saveLoginData(data); Taro.eventCenter.trigger(EventName.LOGIN_SUCCESS); // Trigger a successful login event}}Copy the code
A decorator
There are times when we don’t want to use higher-order components the way they are and want a more intuitive display, and that’s where decorators come in
As shown below.
@withLogin()
class xxScreen extent Component{
}
Copy the code
Easy to understand
The flow chart
To sum up, we can draw our second flow chart
Token expired
The above is the first login process, we also need to consider: how to do when the token expires?
The token expires, but the token still exists in the cache. In this case, the page does not go through the login process, and the 401 error is reported when the service interface is adjusted
We don’t want to add a step in the appeal login process to call the interface to determine whether the token is expired. We still want fewer interfaces and a simpler process
Log back in
If you cannot change the initial login process, you can only start from the invocation result of the service interface and determine whether the error code returned by the interface is 401. If the token exists in the cache, it proves that the token has expired. In this case, you can log in again
Silent login
You can go through the initial login process again when you re-log in, but you want to minimize user operations. The re-login process should be insensitive to users
We can save the service interface first, execute the login interface with the mobile phone number saved in the cache for the first login, and call the service interface again after successful login
Request multiple business interfaces
In the case of a business interface mentioned above, if multiple interfaces are requested at the same time, it is impossible for us to call the login interface again after registering 401 for each interface. We need to build an array to save all the business interfaces, and then call all the business interfaces again after silent login
The complete code is as follows:
http.ts
async request(options: OptionType) { ... return new Promise((resolve, reject) => { Taro.request(newOptions) .then(async res => { const { statusCode } = res; const requestWithNewToken = async () => { if (! AccessToken (new HttpError(httpcode.unauthorized, ErrorMessage[httpcode.unauthorized])) {// If there is no token error, reject(new HttpError(httpcode.unauthorized, ErrorMessage[httpcode.unauthorized])); } else {try {// error await login.updateToken (); const data = await this.request(options); resolve(data); } catch (e) { reject(e); }}}; if (statusCode == HttpCode.SUCCESS) { ... } else if (statusCode === httpcode.unauthorized) {// If the login expires or is not logged in to await requestWithNewToken(); } else { reject(new HttpError(statusCode, ErrorMessage[statusCode] || ErrorMessage[HttpCode.SERVER_ERROR])); } }) .catch(err => { ... }); }); }Copy the code
Login.ts
class Login { private tokenHub = new TokenHub(); // Whether the interface is being refreshed token private tokenRefreshing = false; /** * updateToken and call interface again after updateToken */ updateToken = (): Promise<void> => { return new Promise(async (resolve, reject) => { if (this.tokenRefreshing) { this.tokenHub.addSubscribers(resolve, reject); return; } this.tokenRefreshing = true; const isSuccess = await this.reLogin(); if (isSuccess) { resolve(); this.tokenHub.notify(); } else { reject(new HttpError(HttpCode.REFRESH_TOKEN_ERROR, ErrorMessage[HttpCode.REFRESH_TOKEN_ERROR])); this.tokenHub.rejectAll( new HttpError(HttpCode.REFRESH_TOKEN_ERROR, ErrorMessage[HttpCode.REFRESH_TOKEN_ERROR]) ); } this.tokenRefreshing = false; }); }; }Copy the code
tokenHub.ts
class TokenHub { subscribers: ((params? : any) => void)[][] = []; addSubscribers(resolve: (params? : any) => void, reject: (params? : any) => void) { this.subscribers.push([resolve, reject]); } notify() { this.subscribers.forEach(callbacks => { callbacks? [0]? . (); }); this.subscribers = []; } rejectAll(params: any) { this.subscribers.forEach(callbacks => { callbacks? . [1]? .(params); }); this.subscribers = []; }}Copy the code
The flow chart
To sum up, we can draw our final flow chart