(目录)
项目整合微信登录功能
一、准备工作
1.准备工作
appid appsecret
需要去微信开发平台注册 https://open.weixin.qq.com/
- 注册
- 完善开发者资料
- 开发者资质认证
- 创建网站应用
- 熟悉微信扫码登录流程
参考文档: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时序图
二、后端开发
(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官网
②注册账号,依据个人情况申请开通隧道。
③填写隧道相关配置
④申请成功后下载Ngrok客户端,并启动Ngrok
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. 测试结果(生成二维码)
测试结果如下:
(2)扫描后,回调URL
1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
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中得到的数据为:
userInfoResult中得到的数据为:
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);
}