微信開放平臺:微信掃碼登入功能
官方文件: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後,進行介面呼叫,有以下前提:
- access_token有效且未超時;
- 微信使用者已授權給第三方應用帳號相應介面作用域(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<String, Object> 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<String, Object> 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() {
}
}
來源:hengheng.blog.csdn.net/article/details/107823201