在实际小程序开发中,大部分的前后台交互业务操作与前后端分离的webapp没什么区别,都是调用后台接口,使用JSON数据传递,只有在一些特殊的环节才会感觉到微信的存在,比如微信授权登录、微信支付、微信模板消息推送等场景;
这些场景如果理解了原理,其实也都不复杂,本节博文主要讲解,如何使用springboot后台处理微信授权登录:
在处理微信小程序授权登录之前,最后可以先了解一下Oauth2.0授权协议,如果确实不清楚,那么也没事,直接看微信小程序开发官方文档相关章节:小程序登录时序图

说明:
调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器;
调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key;
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份;
注意:
会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥;
临时登录凭证 code 只能使用一次;

上面把流程理清了,那么下面就开始用代码实现吧:springboot部分:为了减少博文的篇幅,我就不将所有的代码都贴在这里了,只挑核心的贴:
需要用到的核心技术,HttpClient和jwtToken,需要在原有依赖的基础上增加这两个maven依赖:
<!-- httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!-- jwt-token --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
为HttpClient和jwtToken封装工具类:
HttpClientUtil.java
package com.wx.video.utils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
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.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
public class HttpClientUtil {
public static String doGet(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doGet(String url) {
return doGet(url, null);
}
public static String doPost(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<BasicNameValuePair> paramList = new ArrayList<>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doPost(String url) {
return doPost(url, null);
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
}
UserClaims.java
package com.wx.video.utils;
import java.util.Date;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.RequiredTypeException;
import io.jsonwebtoken.impl.JwtMap;
public class UserClaims extends JwtMap implements Claims {
private String grantType = "password";
private Integer uid;
private String openid;
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
setValue("uid", uid);
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
setValue("openid", openid);
}
public String getGrantType() {
return grantType;
}
public void setGrantType(String grantType) {
this.grantType = grantType;
setValue("grantType", this.grantType);
}
@Override
public String getIssuer() {
return getString(ISSUER);
}
@Override
public Claims setIssuer(String iss) {
setValue(ISSUER, iss);
return this;
}
@Override
public String getSubject() {
return getString(SUBJECT);
}
@Override
public Claims setSubject(String sub) {
setValue(SUBJECT, sub);
return this;
}
@Override
public String getAudience() {
return getString(AUDIENCE);
}
@Override
public Claims setAudience(String aud) {
setValue(AUDIENCE, aud);
return this;
}
@Override
public Date getExpiration() {
return get(Claims.EXPIRATION, Date.class);
}
@Override
public Claims setExpiration(Date exp) {
setDate(Claims.EXPIRATION, exp);
return this;
}
@Override
public Date getNotBefore() {
return get(Claims.NOT_BEFORE, Date.class);
}
@Override
public Claims setNotBefore(Date nbf) {
setDate(Claims.NOT_BEFORE, nbf);
return this;
}
@Override
public Date getIssuedAt() {
return get(Claims.ISSUED_AT, Date.class);
}
@Override
public Claims setIssuedAt(Date iat) {
setDate(Claims.ISSUED_AT, iat);
return this;
}
@Override
public String getId() {
return getString(ID);
}
@Override
public Claims setId(String jti) {
setValue(Claims.ID, jti);
return this;
}
@Override
public <T> T get(String claimName, Class<T> requiredType) {
Object value = get(claimName);
if (value == null) {
return null;
}
if (Claims.EXPIRATION.equals(claimName)
|| Claims.ISSUED_AT.equals(claimName)
|| Claims.NOT_BEFORE.equals(claimName)) {
value = getDate(claimName);
}
if (requiredType == Date.class && value instanceof Long) {
value = new Date((Long) value);
}
if (!requiredType.isInstance(value)) {
throw new RequiredTypeException("Expected value to be of type: "
+ requiredType + ", but was " + value.getClass());
}
return requiredType.cast(value);
}
}
JwtTokenProvider.java
package com.wx.video.utils;
import javax.crypto.spec.SecretKeySpec;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class JwtTokenProvider {
private SecretKeySpec key;
public JwtTokenProvider(String key) {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS512.getJcaName());
this.key = secretKeySpec;
}
public String createToken(Claims claims) {
String compactJws = Jwts.builder().setPayload(JSONObject.toJSONString(claims))
.compressWith(CompressionCodecs.DEFLATE).signWith(SignatureAlgorithm.HS512, key).compact();
return compactJws;
}
public Claims parseToken(String token) {
try {
return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
JwtUtils.java
package com.wx.video.utils;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
@Component
public class JwtUtils {
@Value("${auth.jwtKey}")
private String jwtKey;
/**
** 根据UserClaims创建JwtToken
* @param userClaims
* @return
*/
public String createToken(UserClaims userClaims) {
JwtTokenProvider tokenProvider = new JwtTokenProvider(jwtKey);
return tokenProvider.createToken(userClaims);
}
/**
** 根据token解析出Claim
* @param token
* @return
*/
public Claims parseToken(String token) {
JwtTokenProvider tokenProvider = new JwtTokenProvider(jwtKey);
return tokenProvider.parseToken(token);
}
/**
** 根据request请求直接解析出Claim
* @param request
* @return
*/
public Claims getUserClaim(HttpServletRequest request) {
String token = request.getHeader("Authorization");
JwtTokenProvider tokenProvider = new JwtTokenProvider(jwtKey);
return tokenProvider.parseToken(token);
}
}
有了上面的工具类,正在的授权登录代码就很简单啦,代码如下(为了方便直接阅读,少贴一些代码,里面需要用到的常量,我就不提取不封装啦,实际工作工程中,可以专门做一个类,存放所有的常量,如appid,secret等,统一管理):
@Controller
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private VorderService vorderService;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private RedisOperator redis;
@PostMapping("/wxlogin")
@ResponseBody
public JsonResult wxlogin(@RequestBody Map<String, Object> map){
// 配置请求参数
Map<String, String> param = new HashMap<>();
param.put("appid", "wx0fb11123456897544");
param.put("secret", "5c5e5efae856768878yb53976b");
param.put("js_code", map.get("code").toString());
param.put("grant_type", "authorization_code");
// 发送请求
String url = "https://api.weixin.qq.com/sns/jscode2session";
String wxResult = HttpClientUtil.doGet(url, param);
JSONObject jsonObject = JSONObject.parseObject(wxResult);
// 获取参数返回的
String sessionkey = jsonObject.get("session_key").toString();
String openid = jsonObject.get("openid").toString();
// 根据返回的user实体类,判断用户是否是新用户,不是的话,更新最新登录时间,是的话,将用户信息存到数据库
User user = userService.selectByOpenId(openid);
System.out.println("查询用户结果"+user);
if(user != null){
user.setUname(map.get("uname").toString());
user.setUavatar(map.get("uavatar").toString());
user.setUgender(map.get("ugender").toString());
user.setUaddress(map.get("address").toString());
user.setSessionkey(sessionkey); //修改sessionKey
user.setUpdateTime(new Date()); //修改更新时间
//更新数据库
userService.update(user);
}else{
User newUser = new User();
newUser.setOpenid(openid);
newUser.setSessionkey(sessionkey);
newUser.setUname(map.get("uname").toString());
newUser.setUavatar(map.get("uavatar").toString());
newUser.setUgender(map.get("ugender").toString());
newUser.setUaddress(map.get("address").toString());
newUser.setCreateTime(new Date());
// 添加到数据库
int count = userService.insert(newUser);
if(count < 0){
return JsonResult.error("插入数据失败");
}
}
//获取到当前用户的数据库uid
User user1 = userService.selectByOpenId(openid);
//生成JWTtoken
UserClaims userClaims = new UserClaims();
userClaims.setUid(user1.getUid());
userClaims.setOpenid(openid);
String jwttoken = jwtUtils.createToken(userClaims);
System.out.println(jwttoken);
//将token存入Redis
redis.set(jwttoken, sessionkey);
// 封装返回小程序
Map<String, String> result = new HashMap<>();
result.put("token", jwttoken);
return JsonResult.successs(result);
}
}
注意:
1、上文中用到的appid和secret都是我写的假值,需要替换为自己的;
2、统一返回结果数据结构,这种实现方式很多,就是我里面出现的JsonResult类;
3、需要用到的Redis工具类也自己封装一下,这个实现方法也不麻烦,不影响我们的授权登录流程;
上面的代码就足以完成微信小程序授权登陆啦,小程序获取到token后,记得将token存入本地storage,后面进行登录态校验的请求,在Header头中设置Authorization值为token;后台使用封装好的JwtUtils工具类中的getUserClaim方法,就可以从request中直接解析出Claims对象,里面存放着我们封装进去的uid和openid,然后开始处理我们的正常业务啦;
此致敬礼




1 Comment
深入浅出,受益匪浅