0
点赞
收藏
分享

微信扫一扫

SpringBoot项目整合微信扫码登录功能流程解析、使用其完成登录流程

(目录)

项目整合微信登录功能

一、准备工作

1.准备工作

appid appsecret 需要去微信开发平台注册 https://open.weixin.qq.com/

  1. 注册
  2. 完善开发者资料
  3. 开发者资质认证
  4. 创建网站应用
  5. 熟悉微信扫码登录流程

参考文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=e547653f995d8f402704d5cb2945177dc8aa4e7e&lang=zh_CN

获取access_token时序图

image-20230219163321179

image-20230219160250155

二、后端开发

(1)生成返回前端的URL,前端生成二维码

1、在项目用户模块的application.properties配置文件中添加相关配置信息

# 微信开放平台 appid
wx.open.app_id=你的appid

# 微信开放平台 appsecret
wx.open.app_secret=你的appsecret

# 微信开放平台 重定向url
wx.open.redirect_url=http://你的服务器名称/api/ucenter/wx/callback

2、在用户模块创建utils包并创建ConstantWeChatUtils常量类

@Component
public class ConstantWeChatUtils implements InitializingBean {

    @Value("${wx.open.app_id}")
    private String appid;
    @Value("${wx.open.app_secret}")
    private String appsecret;
    @Value("${wx.open.redirect_url}")
    private String redirectUrl;

    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;
    public static String WX_OPEN_REDIRECT_URL;

    @Override
    public void afterPropertiesSet() throws Exception {
        WX_OPEN_APP_ID = appid;
        WX_OPEN_APP_SECRET = appsecret;
        WX_OPEN_REDIRECT_URL = redirectUrl;
    }
}

3、开通内网穿透隧道,指向本地微信扫码登录功能所在模块的端口号。

目前没有上云,使用内网穿透映射到公网

①登录ngrok官网

②注册账号,依据个人情况申请开通隧道。 image-20230219160707490

③填写隧道相关配置 image-20230219160717020

④申请成功后下载Ngrok客户端,并启动Ngrok

image-20230219160728598

4、生成微信扫描二维码

①访问微信提供的固定的地址,向地址里面拼接参数,二维码就可以生成出来

https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

参数说明:

参数 是否必须 说明
appid 应用唯一标识
redirect_uri 请使用urlEncode对链接进行处理
response_type 填code
scope 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即
state 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验

返回说明:

用户允许授权后,将会重定向到redirect_uri的网址上,并且带上code和state参数

redirect_uri?code=CODE&state=STATE

若用户禁止授权,则重定向后不会带上code参数,仅会带上state参数

redirect_uri?state=STATE

②创建生成二维码的方法(Controller)

@Controller
@RequestMapping("/api/ucenter/wx")
@Api(tags = "微信二维码生成接口")
@CrossOrigin
public class WxApiController {
    
    @ApiOperation(value = "生成微信扫码登录二维码")
    @GetMapping("login")
    public String genQrConnect() {
        //定义微信生成二维码固定地址
        //向地址里面拼接参数
        //%s 相当于是占位符
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
            "?appid=%s" +
            "&redirect_uri=%s" +
            "&response_type=code" +
            "&scope=snsapi_login" +
            "&state=%s" +
            "#wechat_redirect";
        try {
            //redirecturl地址进行urlEncode编码
            String redirectUrl = ConstantWeChatUtils.WX_OPEN_REDIRECT_URL;
            redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");

            //防止csrf攻击(跨站请求伪造攻击)
            //String state = UUID.randomUUID().toString().replaceAll("-", "");//一般情况下会使用一个随机数
            String state = "onlineeducation";//此处state设置的是我内网穿透中的前置域名
            //向%s位置传递参数值
            String formatUrl = String.format(
                baseUrl,
                ConstantWeChatUtils.WX_OPEN_APP_ID,
                redirectUrl,
                state
            );

            //重定向到拼接好的地址里面
            return "redirect:"+formatUrl;
        }catch(Exception e) {
            return null;
        }
    }
}

5. 测试结果(生成二维码)

测试结果如下:

image-20230219161829204

(2)扫描后,回调URL

1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

image-20230219162043654

1.模块添加依赖

<!--httpclient-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.1</version>
</dependency>
<!--commons-io-->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
<!--gson-->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
</dependency>

2.创建httpclient工具类,用于后台请求对应的地址

package com.sunset.educenter.utils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 *  依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
 * @author sunset
 *
 */
public class HttpClientUtils {

    public static final int connTimeout=10000;
    public static final int readTimeout=10000;
    public static final String charset="UTF-8";
    private static HttpClient client = null;

    static {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(128);
        cm.setDefaultMaxPerRoute(128);
        client = HttpClients.custom().setConnectionManager(cm).build();
    }

    public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String get(String url) throws Exception {
        return get(url, charset, null, null);
    }

    public static String get(String url, String charset) throws Exception {
        return get(url, charset, connTimeout, readTimeout);
    }

    /**
     * 发送一个 Post 请求, 使用指定的字符集编码.
     *
     * @param url
     * @param body RequestBody
     * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
     * @param charset 编码
     * @param connTimeout 建立链接超时时间,毫秒.
     * @param readTimeout 响应超时时间,毫秒.
     * @return ResponseBody, 使用指定的字符集编码.
     * @throws ConnectTimeoutException 建立链接超时异常
     * @throws SocketTimeoutException  响应超时
     * @throws Exception
     */
    public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
            throws ConnectTimeoutException, SocketTimeoutException, Exception {
        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        String result = "";
        try {
            if (StringUtils.isNotBlank(body)) {
                HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
                post.setEntity(entity);
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());

            HttpResponse res;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }


    /**
     * 提交form表单
     *
     * @param url
     * @param params
     * @param connTimeout
     * @param readTimeout
     * @return
     * @throws ConnectTimeoutException
     * @throws SocketTimeoutException
     * @throws Exception
     */
    public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        try {
            if (params != null && !params.isEmpty()) {
                List<NameValuePair> formParams = new ArrayList<NameValuePair>();
                Set<Entry<String, String>> entrySet = params.entrySet();
                for (Entry<String, String> entry : entrySet) {
                    formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                }
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
                post.setEntity(entity);
            }

            if (headers != null && !headers.isEmpty()) {
                for (Entry<String, String> entry : headers.entrySet()) {
                    post.addHeader(entry.getKey(), entry.getValue());
                }
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());
            HttpResponse res = null;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null
                    && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
    }




    /**
     * 发送一个 GET 请求
     *
     * @param url
     * @param charset
     * @param connTimeout  建立链接超时时间,毫秒.
     * @param readTimeout  响应超时时间,毫秒.
     * @return
     * @throws ConnectTimeoutException   建立链接超时
     * @throws SocketTimeoutException   响应超时
     * @throws Exception
     */
    public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
            throws ConnectTimeoutException,SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpGet get = new HttpGet(url);
        String result = "";
        try {
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            get.setConfig(customReqConf.build());

            HttpResponse res = null;

            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(get);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(get);
            }

            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            get.releaseConnection();
            if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }


    /**
     * 从 response 里获取 charset
     *
     * @param ressponse
     * @return
     */
    @SuppressWarnings("unused")
    private static String getCharsetFromResponse(HttpResponse ressponse) {
        // Content-Type:text/html; charset=GBK
        if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
            String contentType = ressponse.getEntity().getContentType().getValue();
            if (contentType.contains("charset=")) {
                return contentType.substring(contentType.indexOf("charset=") + 8);
            }
        }
        return null;
    }



    /**
     * 创建 SSL连接
     * @return
     * @throws GeneralSecurityException
     */
    private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
                    return true;
                }
            }).build();

            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }

                @Override
                public void verify(String host, SSLSocket ssl)
                        throws IOException {
                }

                @Override
                public void verify(String host, X509Certificate cert)
                        throws SSLException {
                }

                @Override
                public void verify(String host, String[] cns,
                                   String[] subjectAlts) throws SSLException {
                }

            });

            return HttpClients.custom().setSSLSocketFactory(sslsf).build();

        } catch (GeneralSecurityException e) {
            throw e;
        }
    }

}

3.在WxApiController中创建回调方法

@Autowired
private UcenterMemberService memberService;//此处注入的为项目中用户模块的service

/**
* 1、获取回调参数
* 2、从redis中读取state进行比对,异常则拒绝调用
* 3、向微信的授权服务器发起请求,使用临时票据换取access_token
* 4、使用上一步获取的openid查询数据库,判断当前用户是否已注册,如果已注册则直接进行登录操作
* 5、如果未注册,则使用openid和access_token向微信的资源服务器发起请求,请求获取微信的用户信息
*   5.1、将获取到的用户信息存入数据库 
*   5.2、然后进行登录操作
*
* @param code
* @param state
* @return
*/
@GetMapping("callback")
@ApiOperation(value = "扫描成功后的回调方法")
public String callback(String code,String state) {
    //code参数:临时票据,随机字符串,类似于手机验证码
    //state参数:生成二维码传递state值
    //1 获取code临时票据
    //2 请求微信固定地址,得到acess_token和openid
    String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
            "?appid=%s" +
            "&secret=%s" +
            "&code=%s" +
            "&grant_type=authorization_code";
    //拼接参数
    baseAccessTokenUrl = String.format(
            baseAccessTokenUrl,
            ConstantWeChatUtils.WX_OPEN_APP_ID,
            ConstantWeChatUtils.WX_OPEN_APP_SECRET,
            code
    );
    try {
        //请求这个带参数地址,得到acess_token和openid
        //使用httpclient
        String accessTokenResult = HttpClientUtils.get(baseAccessTokenUrl);
        // System.out.println("*********************accessTokenResult: "+accessTokenResult);
        //得到acess_token和openid
        Gson gson = new Gson();
        //把accessTokenResult字符串转换map类型
        HashMap accessTokenMap = gson.fromJson(accessTokenResult, HashMap.class);
        String access_token = (String)accessTokenMap.get("access_token");
        String openid = (String)accessTokenMap.get("openid");

        //3 拿着acess_token和openid再去请求微信固定地址,得到扫描人信息
        String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                "?access_token=%s" +
                "&openid=%s";
        baseUserInfoUrl = String.format(
                baseUserInfoUrl,
                access_token,
                openid
        );
        // 请求地址
        String userInfoResult = HttpClientUtils.get(baseUserInfoUrl);
        // 解析响应
        HashMap userInfoMap = gson.fromJson(userInfoResult, HashMap.class);
        String nickname = (String)userInfoMap.get("nickname");
        String headimgurl = (String)userInfoMap.get("headimgurl");

        //4 把获取微信扫描人信息添加数据库里面
        //添加信息之前判断,根据openid进行判断,如果表存储相同用户信息不需要添加
        //需要给用户设置哪些信息根据自己的表字段进行设置
        UcenterMember member = memberService.getUserInfoByOpenId(openid);
        if(member == null) {//表不存在相同用户,进行添加
            member = new UcenterMember();
            member.setOpenid(openid);
            member.setNickname(nickname);
            member.setAvatar(headimgurl);
            memberService.save(member);
        }

    }catch(Exception e) {

    }
    return null;
}

其中 accessTokenResult中得到的数据为:

image-20230219162528350

userInfoResult中得到的数据为: image-20230219162548761

4.完善业务层,持久化到数据库

添加信息之前通过openid判断用户信息是否存在的代码


@Override
public UcenterMember getUserInfoByOpenId(String openid) {
    QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
    wrapper.eq("openid",openid);
    UcenterMember member = baseMapper.selectOne(wrapper);
    return member;
}

(3)整合JWT令牌

1.加入jwt工具依赖

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

2.在utils包中创建JWT工具类

public class JwtUtils {
    public static final String SUBJECT = "test";//名字随意
    //秘钥
    public static final String APPSECRET = "test";//名字随意
    public static final long EXPIRE = 1000 * 60 * 30;  //过期时间,毫秒,30分钟
    /**
 * 根据对象生成jwt的字符串
 *
 * @param member
 * @return
 */
    public static String geneJsonWebToken(UcenterMember member) {

        if (member == null || StringUtils.isEmpty(member.getId())
            || StringUtils.isEmpty(member.getNickname())
            || StringUtils.isEmpty(member.getAvatar())) {
            return null;
        }
        String token = Jwts.builder().setSubject(SUBJECT)

            .claim("id", member.getId())
            .claim("nickname", member.getNickname())
            .claim("avatar", member.getAvatar())

            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
            .signWith(SignatureAlgorithm.HS256, APPSECRET).compact();

        return token;
    }

    /**
 * 根据jwt的token字符串,从字符串获取用户信息
 *
 * @param token
 * @return
 */
    public static Claims checkJWT(String token) {
        //claims类似于map集合
        Claims claims = Jwts.parser().setSigningKey(APPSECRET).parseClaimsJws(token).getBody();
        return claims;
    }

}

3.callback中生成jwt

在WxApiController.java的callback方法的最后添加如下代码

// 生成jwt
String token = JwtUtils.geneJsonWebToken(member);

//存入cookie
//CookieUtils.setCookie(request, response, "guli_jwt_token", token);
//因为端口号不同存在跨域问题,cookie不能跨域,所以这里使用url重写
return "redirect:http://localhost:3000?token=" + token;

4、前端打印token(前端采用的是NUXT服务器端渲染技术)

在layout/defaullt.vue中打印获取的token值

export default {
  created() {
    console.log(this.$route.query.token)
  }
}

5.前端首页面获取路由token值,调用接口,根据token得到token里面用户信息,返回进行显示

(1)编写接口,根据token字符串获取用户信息

@PostMapping("getUserInfoToken/{token}")
@ApiOperation(value = "根据token获取token里面的用户信息")
public R getUserInfoToken(@PathVariable String token) {
    Claims claims = JwtUtils.checkJWT(token);
    String id = (String)claims.get("id");
    String nickname = (String)claims.get("nickname");
    String avatar = (String)claims.get("avatar");
    UcenterMember member = new UcenterMember();
    member.setId(id);
    member.setNickname(nickname);
    member.setAvatar(avatar);
    return R.ok().data("member",member);
}

总结流程:

image-20230219163321179

举报

相关推荐

0 条评论