• Spring Boot 实现微信扫码登录,真香。。


    微信开放平台:微信扫码登录功能

    官方文档: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<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() {
        }
    }
    

    原文链接:

    https://blog.csdn.net/qq_42764468/article/details/107823201

    版权声明:本文为CSDN博主「小小茶花女」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

    近期热文推荐:

    1.1,000+ 道 Java面试题及答案整理(2022最新版)

    2.劲爆!Java 协程要来了。。。

    3.Spring Boot 2.x 教程,太全了!

    4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

    5.《Java开发手册(嵩山版)》最新发布,速速下载!

    觉得不错,别忘了随手点赞+转发哦!

  • 相关阅读:
    【数据结构】并查集
    项目管理【12】 | 项目范围管理-收集需求
    项目管理【11】 | 项目范围管理-规划范围管理
    项目管理【10】 | 项目范围管理-范围管理概述
    Visual Studio代码远程调试方法
    项目管理【09】 | 项目整体管理-结束项目或阶段
    操作系统【2】Linux系统安装
    操作系统【1】Linux基础知识
    移动端开发案例【2】头部组件开发
    移动端开发案例【1】全局样式配置
  • 原文地址:https://www.cnblogs.com/javastack/p/16178979.html
Copyright © 2020-2023  润新知