WeChat scan code login
1. Authorization process description
WeChat OAuth2.0 authorized login allows users to securely log in to third-party applications or websites using WeChat identities. After the WeChat user authorizes to log in to a third-party application that has access to WeChat OAuth2.0, the third party can obtain the user's interface call credentials (access_token) , through the access_token, the authorization relationship interface of the WeChat open platform can be called, so as to obtain the basic open information of WeChat users and help users realize basic open development functions, etc.
官方文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
WeChat OAuth2.0 authorization login currently supports authorization_code mode, which is suitable for application authorization with server side. The overall process of this mode is:
- The third party initiates a WeChat authorization login request. After the WeChat user allows the authorization of the third-party application, WeChat will launch the application or redirect to the third-party website with the authorization temporary ticket code parameter;
- Through the code parameter plus AppID and AppSecret, etc., the access_token is exchanged through the API;
- Use the access_token to make interface calls to obtain basic user data resources or help users implement basic operations.
Step 1: Request code
Before the third party uses the website application authorization to log in, it needs to pay attention to the corresponding web page authorization scope (scope = snsapi_login), then you can open the following link on the PC side:Remember to replace with your own parameters
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
return instructions
After the user permits authorization, it will be redirected to the redirect_uri URL with code and state parameters
redirect_uri?code=CODE&state=STATE
If the user prohibits authorization, the code parameter will not be carried after redirection, only the state parameter will be carried
redirect_uri?state=STATE
For example:
log in to the Yihaodian website application: https://passport.yhd.com/wechat/login.do
After opening, Yihaodian will generate a state parameter and jump to:
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=3d6be0a4035d839573b04816624a
415e#wechat_redirect WeChat users use WeChat to scan the QR code and confirm login, the PC terminal will Jump to: https://passport.yhd.com/wechat/callback.do?code=CODE&state=3d6be0a4035d839573b04816624a415e
Step 2: Obtain access_token through code
Get access_token through code
官方文档:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
return instructions
correctly returns:
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
Error return example:
{
"errcode":40029,"errmsg":"invalid code"}
- AppSecret is the key used by the application interface. If it is leaked, it may lead to high-risk consequences such as application data leakage and application user data leakage; stored on the client, it is very likely to be maliciously stolen (such as decompiling to obtain AppSecret);
- The access_token is the credential for the user to authorize the third-party application to initiate the interface call (equivalent to the user login state), which is stored on the client, and there may be behaviors such as user data leakage caused by maliciously obtaining the access_token, and user WeChat-related interface functions being maliciously initiated;
- The refresh_token is a long-term certificate for the user to authorize the third-party application. It is only used to refresh the access_token, but if it is leaked, it is equivalent to the leak of the access_token, and the risk is the same as above.
It is recommended to put Secret and user data (such as access_token) on the App cloud server, and the cloud will transfer the interface to call the request.
Step 3: Call the interface through access_token
After obtaining the access_token, the interface call must have the following prerequisites:
1. The access_token is valid and has not timed out;
2. The WeChat user has authorized the corresponding interface scope of the third-party application account (scope)
For the interface scope (scope), the interfaces that can be called are as follows:
2. Authorization process code
Because the AppID and AppSecret of the WeChat open platform and the WeChat public platform are different, the following configurations are required:
# 开放平台
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;
}
}
Obtained openid:openid = o9AREv7Xr22ZUk6BtVqw82bb6AFk
3. User login and logout
@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);
}
}
- Store the openID obtained above into the database
- Change the redirected address after authorization to the login address:
//用户授权同意后回调的地址,从请求参数中获取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;
}
- Request this link in the browser:
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
The third-party application requests to use the WeChat scan code to log in instead of using the password of this website:
After the user agrees to authorize, log in to the background management system of the third-party application:
4. Spring AOP checks whether the user is logged in
@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. Intercept the exception thrown when the login verification fails
Intercept and fail the login verification exception, let it jump to the login page, scan the code to log in
@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() {
}
}
The law of good things: Everything will be a good thing in the end, if it is not a good thing, it means that it is not the end yet.