day06微信登录

  1. 1. 用户表user:
  2. 2. 1.HttpClient
    1. 2.1. 1.1 介绍
    2. 2.2. 1.2 入门案例
      1. 2.2.1. 1.2.1 GET方式请求
      2. 2.2.2. 1.2.2 POST方式请求
  3. 3. 2.微信小程序开发
  4. 4. 3.微信登录
    1. 4.1. 3.1 微信登录流程
    2. 4.2. 3.2 代码开发
      1. 4.2.1. 3.2.1 定义相关配置
      2. 4.2.2. 3.2.2 Controller层
      3. 4.2.3. 3.2.3 Service层接口
      4. 4.2.4. 3.2.4 Service层实现类
      5. 4.2.5. 3.2.5 Mapper层
      6. 4.2.6. 3.2.6 编写拦截器
  5. 5. 4.商品浏览功能

用户表user:

字段名 数据类型 说明 备注
id bigint 主键 自增
openid varchar(45) 微信用户的唯一标识
name varchar(32) 用户姓名
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
avatar varchar(500) 微信用户头像路径
create_time datetime 注册时间

1.HttpClient

1.1 介绍

HttpClient作用:

  • 发送HTTP请求
  • 接收响应数据

HttpClient应用场景:

当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时

其实,应用程序本身并未实现这些功能,都是在应用程序里访问提供这些功能的服务,访问这些服务需要发送HTTP请求,并且接收响应数据,可通过HttpClient来实现。

HttpClient的maven坐标:

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>

HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
  • HttpClients:可认为是构建器,可创建HttpClient对象。
  • CloseableHttpClient:实现类,实现了HttpClient接口。
  • HttpGet:Get方式请求类型。
  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求

1.2 入门案例

对HttpClient编程工具包有了一定了解后,那么,我们使用HttpClient在Java程序当中来构造Http的请求,并且把请求发送出去,接下来,就通过入门案例分别发送GET请求POST请求,具体来学习一下它的使用方法。

正常来说,首先,应该导入HttpClient相关的坐标,但在项目中,就算不导入,也可以使用相关的API。

因为在项目中已经引入了aliyun-sdk-oss坐标:

1
2
3
4
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>

上述依赖的底层已经包含了HttpClient相关依赖。

1.2.1 GET方式请求

进入到sky-server模块,编写测试代码,发送GET请求。

实现步骤:

  1. 创建HttpClient对象
  2. 创建请求对象
  3. 发送请求,接受响应结果
  4. 解析结果
  5. 关闭资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@SpringBootTest
public class HttpClientTest {

/**
* 测试通过httpclient发送GET方式的请求
*/
@Test
public void testGET() throws Exception{
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

//发送请求,接受响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);

//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码为:" + statusCode);

HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:" + body);

//关闭资源
response.close();
httpClient.close();
}
}

1.2.2 POST方式请求

在HttpClientTest中添加POST方式请求方法,相比GET请求来说,POST请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。

实现步骤:

  1. 创建HttpClient对象
  2. 创建请求对象
  3. 发送请求,接收响应结果
  4. 解析响应结果
  5. 关闭资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 测试通过httpclient发送POST方式的请求
*/
@Test
public void testPOST() throws Exception{
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");

StringEntity entity = new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("utf-8");
//数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);

//发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);

//解析返回结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:" + statusCode);

HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为:" + body);

//关闭资源
response.close();
httpClient.close();
}

2.微信小程序开发

要留存AppID、AppSecret,设置不校验合法域名。

3.微信登录

3.1 微信登录流程

微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

步骤分析:

  1. 小程序端,调用wx.login()获取code,就是授权码。
  2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
  3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
  4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
  5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
  6. 小程序端,收到自定义登录态,存储storage。
  7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
  8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
  9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

3.2 代码开发

3.2.1 定义相关配置

配置微信登录所需配置项:

application-dev.yml

1
2
3
4
sky:
wechat:
appid: 获取自己小程序的
secret: 获取自己小程序的

application.yml

1
2
3
4
sky:
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}

配置为微信用户生成jwt令牌时使用的配置项:

application.yml

1
2
3
4
5
6
7
8
9
10
11
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication

3.2.2 Controller层

根据接口定义创建UserController的login方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {

@Autowired
private UserService userService;
@Autowired
private 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());
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);
}
}

其中,JwtClaimsConstant.USER_ID常量已定义。

3.2.3 Service层接口

创建UserService接口:

1
2
3
4
5
6
7
8
9
public interface UserService {

/**
* 微信登录
* @param userLoginDTO
* @return
*/
User wxLogin(UserLoginDTO userLoginDTO);
}

3.2.4 Service层实现类

创建UserServiceImpl实现类:实现获取微信用户的openid和微信登录功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Service
@Slf4j
public class UserServiceImpl implements UserService {

//微信服务接口地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;

/**
* 微信登录
* @param userLoginDTO
* @return
*/
public User wxLogin(UserLoginDTO userLoginDTO) {
String 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){
//调用微信接口服务,获得当前微信用户的openid
Map<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);

JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
}

3.2.5 Mapper层

创建UserMapper接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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映射文件:

1
2
3
4
5
6
7
8
9
10
11
<?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>

3.2.6 编写拦截器

编写拦截器JwtTokenUserInterceptor: 统一拦截用户端发送的请求并进行jwt校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
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(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户的id:", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

可用的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 处理OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
log.info("放行OPTIONS预检请求: {}", request.getRequestURI());
return true;
}

log.info("=== 拦截器开始处理: {} {} ===", request.getMethod(), request.getRequestURI());

// 对于所有/user路径的请求都进行token检查,不管是不是Controller方法
if (request.getRequestURI().startsWith("/user/")) {
// 排除登录和店铺状态接口
if (request.getRequestURI().equals("/user/user/login") ||
request.getRequestURI().equals("/user/shop/status")) {
return true;
}

log.info("对路径 {} 进行JWT校验", request.getRequestURI());

// 支持多个token字段名
String token = request.getHeader("authentication");
log.info("从authentication字段获取token: {}", token);

if (token == null || token.isEmpty()) {
token = request.getHeader(jwtProperties.getUserTokenName());
log.info("从配置字段{}获取token: {}", jwtProperties.getUserTokenName(), token);
}

if (token == null || token.isEmpty()) {
token = request.getHeader("token");
log.info("从token字段获取token: {}", token);
}

log.info("最终使用的token: {}", token);

if (token == null || token.isEmpty()) {
log.error("所有token字段都为空,认证失败");
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":0,\"msg\":\"用户未登录\"}");
return false;
}

// 校验令牌
try {
log.info("开始JWT校验...");
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("JWT校验成功,当前用户id: {}", userId);
BaseContext.setCurrentId(userId);
log.info("ThreadLocal设置用户ID: {}", BaseContext.getCurrentId());
log.info("=== JWT校验通过 ===");
return true;
} catch (Exception ex) {
log.error("JWT校验失败: {}", ex.getMessage());
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":0,\"msg\":\"Token无效或已过期\"}");
return false;
}
}

// 非/user路径直接放行
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后清理ThreadLocal
if (request.getRequestURI().startsWith("/user/")) {
BaseContext.removeCurrentId();
log.info("清理ThreadLocal用户ID - 请求: {}", request.getRequestURI());
}
}
}

在WebMvcConfiguration配置类中注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
//.........

registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}

4.商品浏览功能

与前几天类似,不再赘述