Spring Boot + OAuth2.0 实现微信扫码登录,这才叫优雅!!
共 10895字,需浏览 22分钟
·
2022-05-17 00:31
答应我, 不要再用 if (obj != null) 判空了 20个示例!详解 Java8 Stream 用法,从此告别shi山(垃圾代码) 利用Java8新特征,重构传统设计模式,你学会了吗? 竟然有一半的人不知道 for 与 foreach 的区别??? 利用多线程批量拆分 List 导入数据库,效率杠杠的!
微信开放平台:微信扫码登录功能
官方文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
1. 授权流程说明
微信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
微信用户使用微信扫描二维码并且确认登录后,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 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);
}
}
推荐一个 Spring Boot 基础教程及实战示例:
https://github.com/javastacks/spring-boot-best-practice
① 将上一步获取到的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;
}
最新面试题整理好了,大家可以在Java面试库小程序在线刷题。
第三应用请求使用微信扫码登录,而不是使用本网站的密码:
用户同意授权后登入第三方应用的后台管理系统:
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() {
}
}
原文链接:
https://blog.csdn.net/qq_42764468/article/details/107823201
版权声明:本文为CSDN博主「小小茶花女」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
最后,再给大家推荐一个GitHub项目,该项目整理了上千本常用技术PDF,技术书籍都可以在这里找到。
GitHub地址:https://github.com/hello-go-maker/cs-books
电子书已经更新好了,拿走不谢,记得点一个star,持续更新中...