一、背景
最近業務要求PC端系統登入使用APP應用掃碼登入。
主要目的是:
1、簡化使用者錄入賬號密碼,達到快速登入PC;
2、賬號登入使用更加安全性;
3、為了推廣更多讓大家開啟使用APP(因為行業的特殊性,實際業務場景中大都設計師都在使用PC端設計軟體,同時也習慣了PC端下單)。
二、處理流程
1、業務流程圖
因為掃碼的時候有兩種處理邏輯,所以流程圖有業務處理方案。但不管哪種方案,背後技術處理邏輯是一樣的。
2、技術實現設計流圖程
3、處理步驟說明
a、使用者開啟PC登入頁面,PC登入頁面向認證中心發起請求,認證中心生成uuid等資訊,返回uuid等資訊給前端,前端展示一個包含uuid的二維碼。
b、PC端登入頁面定時向認證中心輪詢二維碼的狀態。
c、使用者登入移動端,開啟移動端攝像頭掃描PC端登入頁面的二維碼。
d、移動端將二維碼中包含的uuid等資訊傳送給認證中心,認證中心將二維碼狀態設定為“掃描成功”。
e、PC端登入頁面輪詢到二維碼狀態為“掃描成功”,提示“掃描成功”,以下圖片僅供參考。
f、移動端展示訊息確認彈出框,顯示“登入”、“取消登入”按鈕,同時將移動端當前登入的用賬號、當前移動端登入的token和二維碼uuid等資訊傳送給認證中心。
g、認證中心將使用者所選要登入的賬號儲存在二維碼資訊裡面,並將二維碼狀態設定為“已授權”。
h、登入頁面從輪詢二維碼不存在時,提示“二維碼已過期” ,以下圖片僅供參考。
i、登入頁面從輪詢二維碼狀態為“已取銷”時,提示“你已取消此次操作,你可再次掃描,或關閉視窗”。
j、登入頁面從輪詢二維碼狀態為“已授權”時,認證中心生成PC端登入的token,設定cookie,並向PC端前端發起重定向跳轉。
程式處理時序圖
三、程式碼實現
auth2認證最簡單的程式碼結構示例
public class Auth2Login {
public static void main(String[] args) {
//Step 1: 獲取授權請求URL
String authRequestUrl = "https://example.com/oauth/authorize";
//Step 2: 向授權伺服器傳送請求,獲取授權碼
String authCode = getAuthCode(authRequestUrl);
//Step 3: 使用授權碼,向認證伺服器傳送請求,獲取access token
String accessToken = getAccessToken(authCode);
//Step 4: 使用access token,訪問資源伺服器,進行使用者登入
String userInfo = getUserInfo(accessToken);
//Step 5: 根據user info進行使用者登入
login(userInfo);
}
public static String getAuthCode(String authRequestUrl) {
//TODO
return null;
}
public static String getAccessToken(String authCode) {
//TODO
return null;
}
public static String getUserInfo(String accessToken) {
//TODO
return null;
}
public static void login(String userInfo) {
//TODO
}
}
掃碼登入認證關鍵程式碼片段
/**
* 初始化,主要透過請求基本參婁生成UUID,並把uuid寫入redis
* @param cmd 請求引數
* @return
*/
public Response init(LoginQrCodeInitCmd cmd) {
String clientId = cmd.getClientId();
String clientRedirectUri = cmd.getClientRedirectUri();
ClientDetailsE clientDetails = oauthService.loadClientDetails(clientId);
if (clientDetails == null || clientDetails.getId() == null) {
return Response.buildFailure(AuthcenterCode.INVALID_CLIENT, String.format(AuthcenterCode.INVALID_CLIENT.getDesc(), clientId));
}
if (!clientDetails.getGrantTypes().contains(GrantType.QR_CODE.toString())) {
return Response.buildFailure(AuthcenterCode.INVALID_GRANT_TYPE, String.format(AuthcenterCode.INVALID_GRANT_TYPE.getDesc(), clientId));
}
LoginQrCodeE qrCodeE = LoginQrCodeE.instance().init(clientId, clientRedirectUri);
return DataResponse.of(BeanToolkit.instance().copy(qrCodeE, LoginQrCodeCO.class));
}
/**
* 透過UUID獲取登入二維碼
* @param uuid 唯一字串
* @return QR code物件
*/
public LoginQrCodeE getLoginQrCode(String uuid) {
return LoginQrCodeE.instance().of(uuid);
}
/**
* 透過UUID掃碼
* @param uuid 唯一字串
* @return
*/
public Response scan(String uuid) {
LoginQrCodeE.instance().scan(uuid);
return Response.buildSuccess();
}
/**
* 取消登入確認
* @param uuid 唯一字串
* @return
*/
public Response cancel(String uuid) {
LoginQrCodeE.instance().cancel(uuid);
return Response.buildSuccess();
}
/***
* 驗證登入
* @param cmd 使用者登入物件資訊
* @return 如果成功返回登入資訊結構體
*/
public Response authorize(LoginQrCodeAuthorizeCmd cmd) {
String uuid = cmd.getUuid();
String selectedAccountId = cmd.getSelectedAccountId();
String token = cmd.getToken();
//是否有掃碼
if (LoginQrCodeE.instance().of(uuid).notScanned()) {
return Response.buildFailure(AuthcenterCode.QR_CODE_NOT_SCANNED);
}
/**
* 找出token
*/
AccessTokenE accessTokenE = oauthRepository.findAccessToken(token);
if (accessTokenE == null) {
return Response.buildFailure(AuthcenterCode.INVALID_TOKEN);
}
AccountE userAccount = oauthRepository.findAccountByToken(token);
if (userAccount == null) {
// 當前令牌不存在使用者態(賬號)
return Response.buildFailure(AuthcenterCode.TOKEN_ACCOUNT_RELA_NOT_EXIST);
}
List<String> userAccountIds = accountRepository.forceGetAccountIdsByMainUserId(userAccount.getMainUserId());
if (userAccountIds == null) {
// 當前賬號異常
return Response.buildFailure(AuthcenterCode.UNKNOWN_ACCOUNT);
}
if (!userAccountIds.contains(selectedAccountId)) {
// 所選賬號與當前令牌登入人資訊不一致
return Response.buildFailure(AuthcenterCode.INVALID_SWITCH_ACCOUNT);
}
LoginQrCodeE.instance().authorize(uuid, selectedAccountId);
return Response.buildSuccess();
}
/**
* 對外提供輪旬時間服務方法,當查詢redis key=uuid是否超時
* @param uuid 使用者訪問請求的UUID
* @return 登入碼狀態物件
* @throws OAuthSystemException
*/
public LoginQrCodeE handle(String uuid) throws OAuthSystemException {
LoginQrCodeE loginQrCode = getLoginQrCode(uuid);
// 當處於“已授權”狀態時,才能觸發準備登入
if (loginQrCode.authorized()) {
return loginQrCode.ready();
}
// 當處於“準備登入”狀態時,才能觸發登入
if (loginQrCode.loginReady()) {
return login(loginQrCode);
}
return loginQrCode;
}
/**
* 掃碼登入
* @param loginQrCode 二維碼帶的物件資訊
* @return
* @throws OAuthSystemException 認證異常
*/
public LoginQrCodeE login(LoginQrCodeE loginQrCode) throws OAuthSystemException {
String clientId = loginQrCode.getClientId();
String accountId = loginQrCode.getAccountId();
ClientDetailsE clientDetails = clientDetailsRepository.findByClientId(clientId);
AccountE userAccount = accountRepository.getAccountById(accountId);
accountRepository.checkAccount(userAccount);
AuthorizeE authorize = oauthRepository.findAccountAuthorizeByAccountId(accountId);
authorizeRepository.checkAuthorizeDataIntegrity(authorize);
if (authorize == null) {
throw new UnknownAuthorizeException("Cannot find AuthorizeE mainUserId="+mainUserId);
}
AccessTokenE accessToken = oauthService.retrieveQrCodeAccessToken(clientDetails, authorize, userAccount, new HashSet<>(),
new BizCodeE(loginQrCode.getAppCode(), loginQrCode.getSubAppCode()));
return loginQrCode.login(accessToken.getToken(), accessToken.getRefreshToken(), accessToken.getCastgt());
}
程式碼僅是展示關鍵的處理過程,結構還是比較清晰的;這裡不提供完整的專案工程,因為這是公司的產權,況且每個公司的業務要求不同,大家理解後再去實現的自己掃碼認證邏輯,處理方法大同小異。