写在前面:如果想自己开发微信小程序,需要先到微信小程序官方平台注册账号,地址为:https://mp.weixin.qq.com/wxopen/waregister?action=step1.
登录流程
其中,开发者服务器就是我们的后端服务器,微信接口服务就是微信提供的服务。openid是微信用户身份的唯一标识。开发者服务器中所谓的自定义登录状态,就是记录当前用户的相关信息,比如存储用户的openid到数据库、生成token等。当小程序获取到开发者服务器返回的自定义登录态(token)后,小程序可以记录下该值,用它作为与开发者服务器业务交互时的令牌。
官方指南:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
实现
前端实现:
通过小程序发送请求,获取code
官方教程https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
var jsCode = '';
uni.login({provider: 'weixin',success: function success(loginRes) {if (loginRes.errMsg === 'login:ok') {console.log('-=-=-=-=loginRes-=-=-=', loginRes);jsCode = loginRes.code;}} });
后端实现:
1:在实现代码前,还是需要添加相关的配置文件,以方便不同环境下代码的运行:
添加配置属性类:
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {private String appid; //小程序的appidprivate String secret; //小程序的秘钥
}
令牌配置:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {/*** 用户端微信用户生成jwt令牌相关配置*/private String userSecretKey;private long userTtl;private String userTokenName;}
application-dev.yml 这里的appid和secret是注册小程序的相关信息
sky:wechat:appid: xxxsecret: yyyy
applicatio.yml中的相应设置
spring:profiles:active: dev
sky:jwt:# 配置用户端令牌user-secret-key: itcastuser-ttl: 7200000user-token-name: authentication# 微信相关配置wechat:appid: ${sky.wechat.appid}secret: ${sky.wechat.secret}
2:根据接口设置相应的DTO,VO,Entity:
DTO:表示服务层需要接收的数据和返回的数据;
将从前端页面受到的JSON数据封装成UserDTO对象:
import lombok.Data;import java.io.Serializable;/*** C端用户登录*/
@Data
public class UserLoginDTO implements Serializable {private String code;}
VO:表示展示层需要展示的数据;
将返回给前端的数据封装成UserVO对象:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {private Long id;private String openid;private String token;}
Entity:该类每一个变量对应于数据库中的数据表的一个字段,也是常说的PO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {private static final long serialVersionUID = 1L;private Long id;//微信用户唯一标识private String openid;//姓名private String name;//手机号private String phone;//性别 0 女 1 男private String sex;//身份证号private String idNumber;//头像private String avatar;//注册时间private LocalDateTime createTime;
}
3:添加登录接口
由上至下:controller–> service --> mapper
UserController
import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.UserService;
import com.sky.utils.JwtUtil;
import com.sky.vo.UserLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user/user")
@Api(tags = "用户登录相关接口")
@Slf4j
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate JwtProperties jwtProperties;/*** 微信登录* @param userLoginDTO* @return*/@PostMapping("/login")@ApiOperation("微信登录")public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){log.info("微信用户登录:{}",userLoginDTO.getCode());// 微信登录User user = userService.wxLogin(userLoginDTO);// 为微信用户生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put(JwtClaimsConstant.USER_ID,user.getId());// JwtProperties与application中的配置相关联,然后通过注入的方式间接的拿到配置文件中添加的属性值String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);UserLoginVO userLoginVO = UserLoginVO.builder().id(user.getId()).openid(user.getOpenid()).token(token).build();return Result.success(userLoginVO);}
}
Userservice
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;public interface UserService {/*** 微信登录* @param userLoginDTO* @return*/User wxLogin(UserLoginDTO userLoginDTO);
}
UserserviceImpl
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.constant.MessageConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.exception.LoginFailedException;
import com.sky.mapper.UserMapper;
import com.sky.properties.WeChatProperties;
import com.sky.service.UserService;
import com.sky.utils.HttpClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;@Service
@Slf4j
public class UserServiceImpl implements UserService {// 微信服务接口地址public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";@Autowiredprivate WeChatProperties weChatProperties;@Autowiredprivate UserMapper userMapper;/*** 用户微信登录* @param userLoginDTO* @return*/@Overridepublic User wxLogin(UserLoginDTO userLoginDTO) {//获取openidString openid = getOpenid(userLoginDTO.getCode());// 判断openid是否为空,如果为空则表示登录失败,抛出业务异常if(openid == null){throw new LoginFailedException(MessageConstant.LOGIN_FAILED);}// 判断当前用户是否是该系统的新用户User user = userMapper.getByOpenid(openid);// 如果是新用户,则自动完成注册if(user == null){user = User.builder().openid(openid).createTime(LocalDateTime.now()).build();userMapper.insert(user);}// 返回用户对象return user;}/*** 调用微信接口服务,获得当前微信用户的openid* @param code* @return*/private String getOpenid(String code){// 调用微信接口服务,获得当前微信用户的openidMap<String, String> map = new HashMap<>();map.put("appid",weChatProperties.getAppid());map.put("secret",weChatProperties.getSecret());map.put("js_code",code);map.put("grant_type","authorization_code");String json = HttpClientUtil.doGet(WX_LOGIN, map); // 得到微信端服务器返回的json数据JSONObject jsonObject = JSON.parseObject(json);String openid = jsonObject.getString("openid"); // 解析json数据,获取openidreturn openid;}
}
UserMapper
import com.sky.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;@Mapper
public interface UserMapper {/*** 根据openid查询用户* @param openid* @return*/@Select("select * from user where openid = #{openid}")User getByOpenid(String openid);/*** 创建新用户并返回主键值* @param user*/void insert(User user);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper"><insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into user (openid, name, phone, sex, id_number, avatar, create_time)values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})</insert></mapper>
补充
生成jwt令牌:(token)
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘钥** @param secretKey jwt秘钥* @param ttlMillis jwt过期时间(毫秒)* @param claims 设置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定签名的时候使用的签名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的时间long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 设置过期时间.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个* @param token 加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}
jwt校验:
1:添加拦截器:
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** jwt令牌校验的拦截器*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校验jwt** @param request* @param response* @param handler* @return* @throws Exception*/public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 输出当前线程的IDSystem.out.println("当前线程的ID:"+Thread.currentThread().getId());//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法,直接放行return true;}//1、从请求头中获取令牌String token = request.getHeader(jwtProperties.getUserTokenName());//2、校验令牌try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());// 将empId存储在当前ThreadLocal中(线程局部变量存储空间,具有线程隔离的效果)BaseContext.setCurrentId(userId);log.info("当前用户id:", userId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}
在配置文件中注册该拦截器:
import com.sky.interceptor.JwtTokenUserInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;/*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenUserInterceptor jwtTokenUserInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtTokenUserInterceptor).addPathPatterns("user/**").excludePathPatterns("/user/user/login").excludePathPatterns("/user/shop/status");}}
HttpClient
上面的代码在请求微信端的时候,使用的HttpClient发送的请求,其相关的代码见HttpClient基础。
本次的微信小程序登录操作实际也是一个第三方登录,若是想实现其他平台的登录,也可以按照这套流程走~今天又进步了一点点,nice!