SpringBoot后端实现微信小程序登录(JWT校验)
(本篇文章采用Mybatis和Maven构建)
在开始之前我们需要引入一些依赖,其中包括mysql、mybatis、lombok、jjwt、fastjson、wechatpay等等
首先我们需要为JWT和微信小程序做一些配置
我创建了两个配置类JwtProperties和WeChatProperties:
(记得将他们交给IOC容器管理)
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "gmsj.jwt")
@Data
public class JwtProperties {
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "gmsj.wechat")
@Data
public class WeChatProperties {
private String appid; //小程序的appid
private String secret; //小程序的秘钥
}
并且在application配置文件中给出对应的配置信息:
这里的数据库配置和小程序配置要填写自己的
server:
port: 8080
spring:
datasource:
#配置数据库连接信息
url: jdbc:mysql://localhost:3306/wechat-login
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
gmsj:
jwt:
# 设置jwt签名加密时使用的秘钥
user-secret-key: gaomengsuanjia
# 设置jwt过期时间
user-ttl: 7200000
# 设置前端传递过来的令牌名称
user-token-name: token
wechat:
#小程序的appid
appid: wx1234567890523
#小程序的秘钥
secret: b0c46549847984561432165463ee6
接着我们来编写程序将要用到的一些pojo类和工具类
由于我们要用前端传来的微信授权码去请求微信接口服务器来获得openId:
具体可参考微信小程序官网
所以我们要使用HttpClient来发送Http请求,还要使用JWT来生成和解析令牌,这里创建了两个工具类:
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
/**
* JWT工具类
*/
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的body
JwtBuilder 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) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
紧接着我们来创建DTO和VO对象,接收到前端请求时,我们要获取到授权码code,然后将Token、openId等等一些必要信息响应给前端:
DTO对象:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginDTO implements Serializable {
//微信授权码
private String code;
}
VO对象:
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;
}
这里我采用了统一的响应格式:
import lombok.Data;
import java.io.Serializable;
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
现在我们要创建并注册拦截器,拦截动态请求,进行登录校验
在这之前,我们需要定义一个BaseContext类,使用ThreadLocal来存储当前线程的ID,这里将用来存储当前登录用户的userId,并且在当前线程的任何地方可以随时获取该userId:
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
然后创建Jwt令牌校验拦截器,这里先从请求头中提取令牌,使用先前编写的JwtUtil类进行解析令牌,判断是否为登录状态(此时我们还未编写登录模块,后面在拦截器注册时也会放行login请求,以便能够发放令牌):
/**
* Jwt令牌校验拦截器
*/
@Component
@Slf4j
public class JwtTokenInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception{
//判断当前拦截到的是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("userId").toString());
log.info("当前用户id:{}",userId);
BaseContext.setCurrentId(userId);
//3、放行
return true;
}catch (Exception ex){
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
接着我们创建一个配置类WebMvcConfiguration,并继承WebMvcConfigurationSupport类,用来注册我们的自定义拦截器:
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenInterceptor jwtTokenInterceptor;
/**
* 注册自定义拦截器
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry){
log.info("开始注册自定义拦截器.....");
registry.addInterceptor(jwtTokenInterceptor) //添加要注册的自定义拦截器
.addPathPatterns("/user/**") //要进行拦截的资源
.excludePathPatterns("/user/user/login"); //需要放行的资源
}
}
此时我们就可以开始编写服务模块的程序代码了
本人习惯从Controller层开始往下写
创建UserController类,在类中接收前端请求获取请求参数并封装成DTO对象,将其交给业务层进行处理,这里为Post请求方式
@RestController
@RequestMapping("/user/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
log.info("微信登录:{}",userLoginDTO);
//获取登录user对象
User user = userService.login(userLoginDTO);
//封装当前用户信息
Map<String, Object> claims = new HashMap<>();
claims.put("userId",user.getId());
//生成JWT令牌
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
//封装VO对象
UserLoginVO userLoginVO = new UserLoginVO();
userLoginVO.setId(user.getId());
userLoginVO.setOpenid(user.getOpenId());
userLoginVO.setToken(token);
return Result.success(userLoginVO);
}
}
接着我们要在service层中做逻辑业务处理,向微信接口服务器发送请求,获取该用户的openId并判断是否为新用户登录,并实现自动注册
public interface UserService {
/**
* 微信登录
* @param userLoginDTO
* @return
*/
User login(UserLoginDTO userLoginDTO);
}
@Service
public class UserServiceImpl implements UserService {
public static final String WxLoginUrl = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@Override
public User login(UserLoginDTO userLoginDTO) {
//请求微信服务器获取openid
Map<String, String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code",userLoginDTO.getCode());
map.put("grant_type","authorization_code");
String json = HttpClientUtil.doGet(WxLoginUrl, map);
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
//检查openid是否为空
if (openid == null){
throw new LoginFailedException("登录失败");
}
//判断是否为新用户(自动注册)
User user = userMapper.getUserByOpenId(openid);
if (user == null){
user = new User();
user.setOpenId(openid);
user.setCreatTime(LocalDateTime.now());
userMapper.insert(user);
}
//返回用户对象
return user;
}
}
到这里,你会发现其中判断openid为空时会抛出异常,这里需要用到全局异常处理器,遇到的异常我们会上抛给Servlet容器,将错误信息返回给前端(所有的信息字符串都建议封装为常量类,我这里偷懒了):
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
String message = ex.getMessage();
if (message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username+ "账号已存在";
return Result.error(msg);
}
else {
return Result.error("未知错误");
}
}
}
/**
* 业务异常
*/
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String msg) {
super(msg);
}
}
/**
* 登录失败
*/
public class LoginFailedException extends BaseException{
public LoginFailedException(String msg){
super(msg);
}
}
然后就可以编写持久层代码了,这里需要两个方法:
- 根据openId查询用户 :
getUserByOpenId(String openid)
- 插入用户 :
insert(User user)
@Mapper
public interface UserMapper {
/**
* 根据openid查询用户
* @param openid
* @return
*/
@Select("select * from user where open_id = #{openid}")
User getUserByOpenId(String openid);
/**
* 自动注册
* @param user
*/
void insert(User user);
}
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.gaomengsuanjia.westlake.server.mapper.UserMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user(open_id,username,phone,sex,avatar,creat_time) values (#{openId},#{username},#{phone},#{sex},#{avatar},#{creatTime})
</insert>
</mapper>
到这里我们就实现了微信登陆的模块,可以与你的前端程序联合调试
(排版顺序不太熟练,若发现有代码错误尽管指出)