用户表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作用:
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请求。
实现步骤:
- 创建HttpClient对象
- 创建请求对象
- 发送请求,接受响应结果
- 解析结果
- 关闭资源
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 {
@Test public void testGET() throws Exception{ 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请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。
实现步骤:
- 创建HttpClient对象
- 创建请求对象
- 发送请求,接收响应结果
- 解析响应结果
- 关闭资源
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
|
@Test public void testPOST() throws Exception{ 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
步骤分析:
- 小程序端,调用wx.login()获取code,就是授权码。
- 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
- 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
- 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
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: admin-secret-key: itcast 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;
@PostMapping("/login") @ApiOperation("微信登录") public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){ log.info("微信用户登录:{}",userLoginDTO.getCode());
User user = userService.wxLogin(userLoginDTO);
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 {
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;
public User wxLogin(UserLoginDTO userLoginDTO) { String openid = getOpenid(userLoginDTO.getCode());
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; }
private String getOpenid(String code){ 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 {
@Select("select * from user where openid = #{openid}") User getByOpenid(String openid);
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
|
@Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired private JwtProperties jwtProperties;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; }
String token = request.getHeader(jwtProperties.getUserTokenName());
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); return true; } catch (Exception ex) { 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 { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { log.info("放行OPTIONS预检请求: {}", request.getRequestURI()); return true; }
log.info("=== 拦截器开始处理: {} {} ===", request.getMethod(), request.getRequestURI());
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());
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; } }
return true; }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 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;
protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/user/login") .excludePathPatterns("/user/shop/status"); }
|
4.商品浏览功能
与前几天类似,不再赘述