• SPA单页应用前后分离微信授权


    项目基于微信公众号开发,业务完全依赖微信授权,也就是用户进入页面已经完成授权获取到用户的OpenId。

    需要有一个授权中间页:author.vue

    基本实现思路:

    • 无论使用哪个url进入页面都会先触发router.beforeEach钩子。
    • 在router.beforeEach钩子函数中判断用户是否授权。
    • 若未授权则保存用户进入的url并请求后台接口获取微信授权(window.location.href=‘后台接口’)。
    • 后台调用微信接口授权获取用户信息及openId,将openId使用JWT生成一个唯一的token令牌,并将token已参数的形式拼接到url后面,然后重定向到前端author.vue页面。
    • author页面获取url中的token参数,将token参数保存到本地缓存。
    • 获取签名用户保存的url并跳转。

    前端代码实现:

    路由index.js

    // 全局守卫,微信授权
    router.beforeEach((to, from, next) => {
      // 路由发生变化修改页面title
      if (to.meta.title) {
        document.title = to.meta.title
      }
      if (process.env.NODE_ENV !== 'development') {
        const token = window.localStorage.getItem('token')
        if (token) {
          if (to.path === '/author') {
            next({
              path: '/'
            })
          } else {
            next()
          }
        } else {
          if (to.path !== '/author') {
            // 保存用户进入的url
            window.localStorage.setItem('authUrl', to.fullPath)
            // 跳转到微信授权页面
            window.location.href = process.env.BASE_URL + '/wx/OAuth2/index'
          } else {
            next()
          }
        }
      } else {
        window.localStorage.setItem('token', 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJvUUFFYndSSU5VVlhPLVZoOWhEcDUzX3RNeEgwIn0.eShRG4fVFFv4w2gHnkyh7QDdVpG1meOHSZXOrbq-psE')
      }
      next()
    })

    Author.vue

    <template>
        <div>授权中</div>
    </template>
    
    <script>
    export default {
      name: 'Author',
      data () {
        return {
          user: null
        }
      },
      created () {
        // url中获取参数token
        const wxToken = this.$route.query.token
        // url中获取参数code
        const code = this.$route.query.code
        // 后端重定向获取参数,判断是否处理成功 200:成功
        if (wxToken && Number(code) === 200) {
          // 将token放入本地缓存
          window.localStorage.setItem('token', wxToken)
          // 从本地缓存中获取用户第一次请求页面URL
          const historyUrl = window.localStorage.getItem('authUrl')
          // 跳转页面
          this.$router.push(historyUrl)
        } else {
          // 没有拿到后台访问微信返回的token
          // 清空本地缓存
          window.localStorage.removeItem('token')
          window.localStorage.removeItem('authUrl')
        }
      }
    }
    </script>
    
    <style scoped>
    
    </style>

     后端代码实现:

    /**
         * 微信授权 --- OATH2 -- 第一种方式(推荐)
         * 第一步:前端请求-/wx/oAth2/index
         * 第二步:重定向-微信服务器
         */
        @PassToken
        @GetMapping(value = "/wx/OAuth2/index")
        public void OAth2(HttpServletResponse response) throws IOException{
            response.sendRedirect(wxMpService.oauth2buildAuthorizationUrl(baseUrl + "/wx/OAuth2/redirect",
                    WxConsts.OAuth2Scope.SNSAPI_USERINFO, null));
        }
    
        /**
         * 微信授权 -- 微信回调
         * 第一步:获取code
         * 第二步:通过code获取用户信息
         * 第三步:Jwt生成Token令牌
         * 第四步:重定向 --> 前端页面
         */
        @PassToken
        @GetMapping(value = "/wx/OAuth2/redirect")
        public void OAth2Return(HttpServletRequest request, HttpServletResponse response) throws IOException,WxErrorException{
            String code = request.getParameter("code");
            // 获取用户信息
            WxMpUser wxMpUser = wxMpService.oauth2getUserInfo(wxMpService.oauth2getAccessToken(code), null);
            log.info("[微信授权]--------拉取用户信息详细如下:{}",wxMpUser);
            //将微信用户信息入库
            wxUserInfoService.insertWxUser(wxMpUser);
            //生成token令牌
            String token = JWT.create().withAudience(wxMpUser.getOpenId()).sign(Algorithm.HMAC256(jwtSecret));
            //重定向地址
            String redirectUrl = frontUrl + "/#/author" + "?token=" + token + "&code=200";
            response.sendRedirect(redirectUrl);
        }

    后台验证用户信息

    前端获取到token令牌之后,前端每次请求,后端如何获取OpenId以及业务处理?

    基本实现思路:

    • 前端使用axios请求拦截器,判断本地缓存是否存在token,如果存在的话,则为每个Http请求赋值token。
    • 后端使用拦截器拦截有@PassToken注解以外的方法,获取token值。如果token为null,直接返回错误码以及错误信息。
    • 验证token值是否有效,如有效,则解析openId,并将openId放入request中放行。如无效,直接返回错误码以及错误信息。
    • 拦截器放行,后端可直接通过request.getAttribute("openId")获取。

    前端代码实现:

    request.js

    // 请求拦截器
    axios.interceptors.request.use(function (config) {
      config.headers['Content-Type'] = 'application/json;charset=UTF-8'
      // 判断本地缓存是否存在token,如果存在的话,则每个http header都加上token
      if (window.localStorage.getItem('token')) {
        config.headers.authorization = window.localStorage.getItem('token')
      }
      return config
    }, function (error) {
      return Promise.reject(error)
    })

    后端代码实现:

    JwtInterceptor.java

    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
            // 从 http 请求头中取出 token
            String token = httpServletRequest.getHeader("Authorization");
    
            // 如果不是映射到方法直接通过
            if(!(object instanceof HandlerMethod)){
                return true;
            }
            HandlerMethod handlerMethod=(HandlerMethod)object;
            Method method=handlerMethod.getMethod();
    
            // OPTIONS请求类型直接返回不处理
            if ("OPTIONS".equals(httpServletRequest.getMethod())){
                return false;
            }
    
            //检查是否有passToken注释,有则跳过认证
            if (method.isAnnotationPresent(PassToken.class)) {
                PassToken passToken = method.getAnnotation(PassToken.class);
                if (passToken.required()) {
                    return true;
                }
            }
    
            //校验token,并且将openId放入request中
            if (StrUtil.isNotEmpty(token)){
                // 验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
                    logger.info("token校验未通过");
                    httpServletResponse.getWriter().println(JSONUtil.toJsonStr(Result.need2BLogged()));
                    return false;
                }
    
                // 获取 token 中的 openId
                String openId;
                try {
                    openId = JWT.decode(token).getAudience().get(0);
                    httpServletRequest.setAttribute("openId",openId);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("401");
                }
            }
    
    
            //检查有没有需要用户权限的注解
            if (method.isAnnotationPresent(UserLoginToken.class)) {
                UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
                if (userLoginToken.required()) {
                    // 执行认证
                    if (token == null) {
                        throw new RuntimeException("无token,请重新登录");
                    }
                    // 获取 token 中的 openId
                    String openId;
                    try {
                        openId = JWT.decode(token).getAudience().get(0);
                    } catch (JWTDecodeException j) {
                        throw new RuntimeException("401");
                    }
    
                    // 通过 openId 查询用户是否绑定手机号
                    if (objectRedisTemplate.hasKey(userIdKey + openId)) {
                        logger.info("通过FRDIES用户拦截器");
                        return true;
                    } else {
                        logger.info("REDIS:{Redis has no user information}");
    
                        //根据 openId 查询该用户的信息
                        BaseUserInfo userInfo = baseController.getUserInfo(httpServletRequest, httpServletResponse);
                        if (userInfo != null && StrUtil.isNotEmpty(userInfo.getPhone())){
                            logger.info("通过用户拦截器");
                            return true;
                        }else{
                            // 未绑定手机用户返回
                            httpServletResponse.getWriter().println(JSONUtil.toJsonStr(Result.need2BLogged()));
                            return false;
                        }
                    }
                }
            }
    
            return true;
        }

    @PassToken

    package com.yhzy.zytx.jwt.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @ClassName PassToken
     * @Description 自定义注解(跳过验证Token)
     * @Author 天生傲骨、怎能屈服
     * @Date 2019/5/22 13:38
     * @Version 1.0
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PassToken {
        boolean required() default true;
    }

    到这里整个前后分离微信授权的流程就完了,希望可以帮助到大家!!!

    作者:张强
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    C++互斥器:Semaphores
    C++互斥器:Mutex
    内联函数(inline)
    C++显式转换
    线程同步控制
    拷贝构造函数——防篡改
    extern关键字
    虚析构、纯虚析构
    类的友元关系
    char 与 unsigned char的本质区别
  • 原文地址:https://www.cnblogs.com/itmrzhang/p/10997006.html
Copyright © 2020-2023  润新知