微信掃碼登錄很難嗎?5步幫你搞定

語言: CN / TW / HK

關注公眾號:IT老哥,每天閲讀一篇乾貨文章,一年後你會發現一個不一樣的自己

微信開放平台:微信掃碼登錄功能

官方文檔:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

1. 授權流程説明

微信OAuth2.0授權登錄讓微信用户使用微信身份安全登錄第三方應用或網站,在微信用户授權登錄已接入微信OAuth2.0的第三方應用後,第三方可以獲取到用户的接口調用憑證(access_token),通過access_token可以進行微信開放平台授權關係接口調用,從而可實現獲取微信用户基本開放信息和幫助用户實現基礎開放功能等。

微信OAuth2.0授權登錄目前支持authorization_code模式,適用於擁有server端的應用授權。該模式整體流程為:

① 第三方發起微信授權登錄請求,微信用户允許授權第三方應用後,微信會拉起應用或重定向到第三方網站,並且帶上授權臨時票據code參數;

② 通過code參數加上AppID和AppSecret等,通過API換取access_token;

③ 通過access_token進行接口調用,獲取用户基本數據資源或幫助用户實現基本操作。

圖片

第一步:請求CODE

第三方使用網站應用授權登錄前請注意已獲取相應網頁授權作用域(scope=snsapi_login),則可以通過在PC端打開以下鏈接:https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

返回説明

用户允許授權後,將會重定向到redirect_uri的網址上,並且帶上code和state參數

``` redirect_uri?code=CODE&state=STATE

```

若用户禁止授權,則重定向後不會帶上code參數,僅會帶上state參數

``` redirect_uri?state=STATE

```

例如:登錄一號店網站應用 https://passport.yhd.com/wechat/login.do 打開後,一號店會生成state參數,跳轉到 https://open.weixin.qq.com/connect/qrconnect?appid=wxbdc5610cc59c1631&redirect_uri=https%3A%2F%2Fpassport.yhd.com%2Fwechat%2Fcallback.do&response_type=code&scope=snsapi_login&state=3d6be0a4035d839573b04816624a415e#wechat_redirect 微信用户使用微信掃描二維碼並且確認登錄後,PC端會跳轉到 https://passport.yhd.com/wechat/callback.do?code=CODE&state=3d6be0a4035d839573b04816624a415e

第二步:通過code獲取access_token

通過code獲取access_token

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

返回説明

正確的返回:

``` {  "access_token":"ACCESS_TOKEN",  "expires_in":7200,  "refresh_token":"REFRESH_TOKEN", "openid":"OPENID",  "scope":"SCOPE", "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" }

```

錯誤返回樣例:

``` {"errcode":40029,"errmsg":"invalid code"}

```

  • Appsecret 是應用接口使用密鑰,泄漏後將可能導致應用數據泄漏、應用的用户數據泄漏等高風險後果;存儲在客户端,極有可能被惡意竊取(如反編譯獲取Appsecret);

  • access_token 為用户授權第三方應用發起接口調用的憑證(相當於用户登錄態),存儲在客户端,可能出現惡意獲取access_token 後導致的用户數據泄漏、用户微信相關接口功能被惡意發起等行為;

  • refresh_token 為用户授權第三方應用的長效憑證,僅用於刷新access_token,但泄漏後相當於access_token 泄漏,風險同上。

建議將secret、用户數據(如access_token)放在App雲端服務器,由雲端中轉接口調用請求。

第三步:通過access_token調用接口

獲取access_token後,進行接口調用,有以下前提:

  1. access_token有效且未超時;

  2. 微信用户已授權給第三方應用帳號相應接口作用域(scope)。

對於接口作用域(scope),能調用的接口有以下:

圖片

2. 授權流程代碼

因為微信開放平台的AppiD和APPSecret和微信公眾平台的AppiD和AppSecret都是不同的,因此需要配置一下:

```

開放平台

wechat.open-app-id=wx6ad144e54af67d87 wechat.open-app-secret=91a2ff6d38a2bbccfb7e9f9079108e2e

```

``` @Data @Component @ConfigurationProperties(prefix = "wechat") public class WechatAccountConfig {

//公眾號appid     private String mpAppId;

//公眾號appSecret     private String mpAppSecret;

//商户號     private String mchId;

//商户祕鑰     private String mchKey;          //商户證書路徑     private String keyPath;

//微信支付異步通知     private String notifyUrl;

//開放平台id     private String openAppId;

//開放平台祕鑰     private String openAppSecret; }

```

``` @Configuration public class WechatOpenConfig {

@Autowired     private WechatAccountConfig accountConfig;

@Bean     public WxMpService wxOpenService() {         WxMpService wxOpenService = new WxMpServiceImpl();         wxOpenService.setWxMpConfigStorage(wxOpenConfigStorage());         return wxOpenService;     }

@Bean     public WxMpConfigStorage wxOpenConfigStorage() {         WxMpInMemoryConfigStorage wxMpInMemoryConfigStorage = new WxMpInMemoryConfigStorage();         wxMpInMemoryConfigStorage.setAppId(accountConfig.getOpenAppId());         wxMpInMemoryConfigStorage.setSecret(accountConfig.getOpenAppSecret());         return wxMpInMemoryConfigStorage;     } }

```

``` @Controller @RequestMapping("/wechat") @Slf4j public class WeChatController {     @Autowired     private WxMpService wxMpService;

@Autowired     private WxMpService wxOpenService;

@GetMapping("/qrAuthorize")     public String qrAuthorize() {         //returnUrl就是用户授權同意後回調的地址         String returnUrl = "http://heng.nat300.top/sell/wechat/qrUserInfo";

//引導用户訪問這個鏈接,進行授權         String url = wxOpenService.buildQrConnectUrl(returnUrl, WxConsts.QRCONNECT_SCOPE_SNSAPI_LOGIN, URLEncoder.encode(returnUrl));         return "redirect:" + url;     }

//用户授權同意後回調的地址,從請求參數中獲取code     @GetMapping("/qrUserInfo")     public String qrUserInfo(@RequestParam("code") String code) {         WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken();         try {             //通過code獲取access_token             wxMpOAuth2AccessToken = wxOpenService.oauth2getAccessToken(code);         } catch (WxErrorException e) {             log.error("【微信網頁授權】{}", e);             throw new SellException(ResultEnum.WECHAT_MP_ERROR.getCode(), e.getError().getErrorMsg());         }         //從token中獲取openid         String openId = wxMpOAuth2AccessToken.getOpenId();

//這個地址可有可無,反正只是為了拿到openid,但是如果沒有會報404錯誤,為了好看隨便返回一個百度的地址         String  returnUrl = "http://www.baidu.com";

log.info("openid={}", openId);

return "redirect:" + returnUrl + "?openid="+openId;     } }

```

請求路徑:在瀏覽器打開

https://open.weixin.qq.com/connect/qrconnect?appid=wx6ad144e54af67d87&redirect_uri=http%3A%2F%2Fsell.springboot.cn%2Fsell%2Fqr%2FoTgZpwenC6lwO2eTDDf_-UYyFtqI&response_type=code&scope=snsapi_login&state=http%3a%2f%2fheng.nat300.top%2fsell%2fwechat%2fqrUserInfo

獲取了openid:openid=o9AREv7Xr22ZUk6BtVqw82bb6AFk

圖片

3. 用户登錄和登出

``` @Controller @RequestMapping("/seller") public class SellerUserController {

@Autowired     private SellerService sellerService;

@Autowired     private StringRedisTemplate redisTemplate;

@Autowired     private ProjectUrlConfig projectUrlConfig;

@GetMapping("/login")     public ModelAndView login(@RequestParam("openid") String openid,                               HttpServletResponse response,                               Map map) {

//1. openid去和數據庫裏的數據匹配         SellerInfo sellerInfo = sellerService.findSellerInfoByOpenid(openid);         if (sellerInfo == null) {             map.put("msg", ResultEnum.LOGIN_FAIL.getMessage());             map.put("url", "/sell/seller/order/list");             return new ModelAndView("common/error");         }

//2. 設置token至redis         String token = UUID.randomUUID().toString();         //設置token的過期時間         Integer expire = RedisConstant.EXPIRE;

redisTemplate.opsForValue().set(String.format(RedisConstant.TOKEN_PREFIX, token), openid, expire, TimeUnit.SECONDS);

//3. 設置token至cookie         CookieUtil.set(response, CookieConstant.TOKEN, token, expire);

return new ModelAndView("redirect:" + "http://heng.nat300.top/sell/seller/order/list");     }

@GetMapping("/logout")     public ModelAndView logout(HttpServletRequest request,                        HttpServletResponse response,                        Map map) {         //1. 從cookie裏查詢         Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);         if (cookie != null) {             //2. 清除redis             redisTemplate.opsForValue().getOperations().delete(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));

//3. 清除cookie             CookieUtil.set(response, CookieConstant.TOKEN, null, 0);         }

map.put("msg", ResultEnum.LOGOUT_SUCCESS.getMessage());         map.put("url", "/sell/seller/order/list");         return new ModelAndView("common/success", map);     } }

```

① 將上一步獲取到的openid存入數據庫

圖片

② 將授權後跳轉的地址改為登錄地址

```  //用户授權同意後回調的地址,從請求參數中獲取code     @GetMapping("/qrUserInfo")     public String qrUserInfo(@RequestParam("code") String code) {         WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken();         try {             //通過code獲取access_token             wxMpOAuth2AccessToken = wxOpenService.oauth2getAccessToken(code);         } catch (WxErrorException e) {             log.error("【微信網頁授權】{}", e);             throw new SellException(ResultEnum.WECHAT_MP_ERROR.getCode(), e.getError().getErrorMsg());         }         //從token中獲取openid         String openId = wxMpOAuth2AccessToken.getOpenId();

//授權成功後跳轉到賣家系統的登錄地址         String  returnUrl = "http://heng.nat300.top/sell/seller/login";

log.info("openid={}", openId);

return "redirect:" + returnUrl + "?openid="+openId;     }

```

③ 在瀏覽器請求這個鏈接:https://open.weixin.qq.com/connect/qrconnect?appid=wx6ad144e54af67d87&redirect_uri=http%3A%2F%2Fsell.springboot.cn%2Fsell%2Fqr%2FoTgZpwenC6lwO2eTDDf_-UYyFtqI&response_type=code&scope=snsapi_login&state=http%3a%2f%2fheng.nat300.top%2fsell%2fwechat%2fqrUserInfo

第三應用請求使用微信掃碼登錄,而不是使用本網站的密碼:

圖片

用户同意授權後登入第三方應用的後台管理系統:

圖片

圖片

4. Spring AOP校驗用户有沒有登錄

``` @Aspect @Component @Slf4j public class SellerAuthorizeAspect {

@Autowired     private StringRedisTemplate redisTemplate;

@Pointcut("execution(public * com.hh.controller.Seller.(..))" +     "&& !execution(public * com.hh.controller.SellerUserController.*(..))")     public void verify() {}

@Before("verify()")     public void doVerify() {                  ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();         HttpServletRequest request = attributes.getRequest();

//查詢cookie         Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);         //如果cookie中沒有token説明已經登出或者根本沒有登錄         if (cookie == null) {             log.warn("【登錄校驗】Cookie中查不到token");             //校驗不通過,拋出異常             throw new SellerAuthorizeException();         }

//去redis裏查詢         String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));         //如果redis中沒有對應的openid,同樣表示登出或者根本沒有登錄         if (StringUtils.isEmpty(tokenValue)) {             log.warn("【登錄校驗】Redis中查不到token");             throw new SellerAuthorizeException();         }     } }

```

5. 攔截登錄校驗不通過拋出的異常

攔截及登錄校驗不通過的異常,讓其跳轉到登錄頁面,掃碼登錄

``` @ControllerAdvice public class SellExceptionHandler {     //攔截登錄異常     @ExceptionHandler(value = SellerAuthorizeException.class)     public ModelAndView handlerAuthorizeException() {         //攔截異常後,跳轉到登錄界面         return new ModelAndView("redirect:".concat("https://open.weixin.qq.com/connect/qrconnect?" +                 "appid=wx6ad144e54af67d87" +                 "&redirect_uri=http%3A%2F%2Fsell.springboot.cn%2Fsell%2Fqr%2F" +                 "oTgZpwenC6lwO2eTDDf_-UYyFtqI" +                 "&response_type=code&scope=snsapi_login" +                 "&state=http%3a%2f%2fheng.nat300.top%2fsell%2fwechat%2fqrUserInfo"));     }     @ExceptionHandler(value = SellException.class)     @ResponseBody     public ResultVO handlerSellerException(SellException e) {         return ResultVOUtil.error(e.getCode(), e.getMessage());     }     @ExceptionHandler(value = ResponseBankException.class)     @ResponseStatus(HttpStatus.FORBIDDEN)     public void handleResponseBankException() {     } }

```

關注公眾號:IT老哥,每天閲讀一篇乾貨文章,一年後你會發現一個不一樣的自己