在实际小程序开发中,大部分的前后台交互业务操作与前后端分离的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
深入浅出,受益匪浅