1.接口文档
项目对接任何三方接口,最重要的参考永远都是官方给提供的接口文档,接下来以微信小程序登录为例,链接如下:
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
这个文档中比较关键的部分就是:
功能描述,查阅是否符合我们业务对接的需求
调用方式,三方接口一般都是https居多,更加安全,包含了请求方式和请求地址
请求参数,接口的入参,特别注意的是,必须要传的参数
返回参数,查看返回的数据结构和内容,是否符合自己的返回预期
错误码,当接口调用失败的时候,可以查阅以方便修正接口调用失败原因
2.API客户端
1.通常的http请求,都是前端到后端发起请求,如下图:
Client客户端一般都是指浏览器发起了请求
Server是指服务端,Java的后端服务
2.如果从后端调用三方接口,一样也是用的http请求,只是这次请求是从后端到另外一个后端服务,我们需要借助工具才能发起请求:
以下统一用糊涂工具包发起请求,糊涂工具包使用说明地址:https://doc.hutool.cn/pages/http/
3.微信登录
在任意需要登录才能显示信息的页面点击时,判断当前用户是否登录,如未登录,则进入登录页面
点击微信账号登录,弹出弹窗,需要获取用户手机号进行登录
微信登录业务逻辑规则:
我们的主要实现思路可以根据微信小程序开放者平台给提供的实现思路,链接和流程如下:
微信小程序登录,官方文档:
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
微信官方推荐登录流程:
注意点:
-
前端在小程序中集成微信相关依赖,当用户请求登录的同时,调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
-
后端服务器调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key
-
开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
4.登录接口说明
-
接口路径(已固定):
/customer/user/login
-
请求方式(已固定):
POST
-
请求参数:(已固定)
{
"code": "0e36jkGa1ercRF0Fu4Ia1V3fPD06jkGW", //临时登录凭证code
"nickName": "微信用户",
"phoneCode": "13fe315872a4fb9ed3deee1e5909d5af60dfce7911013436fddcfe13f55ecad3"
}
以上三个参数,都是前端开发人员调用wx.login()方法返回的数据
code:临时登录凭证code
nickName:微信用户昵称(现在微信统一返回为:微信用户)
phoneCode:详细用户信息code,后台根据此code可以获取用户手机号
-
响应示例
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlpb3mn7_lvIDoirE4OTE1IiwiZXhwIjoxNDY1MjI3MTMyOCwidXNlcmlkIjoxfQ.nB6ElZbUywh-yiHDNMJS8WqUpcLWCszVdvAMfySFxIM",
"nickName": "好柿开花8915"
},
"operationTime": null
}
5.微信登录后端接口实现
5.1.实现思路说明
基于微信官方提供的流程图,结合我们的业务,最终的实现思路,如下图:
5.2.接口定义
新增类CustomerUserController
/**
* <p>
* 用户管理
*/
@Slf4j
@Api(tags = "客户管理")
@RestController
@RequestMapping("/customer/user")
public class CustomerUserController {
@Autowired
private MemberService memberService;
/**
* C端用户登录--微信登录
* @param userLoginRequestDto 用户登录信息
* @return 登录结果
*/
@PostMapping("/login")
@ApiOperation("小程序端登录")
public ResponseResult<LoginVo> login(@RequestBody UserLoginRequestDto userLoginRequestDto){
LoginVo loginVo = memberService.login(userLoginRequestDto);
return ResponseResult.success(loginVo);
}
}
接收参数类型UserLoginRequestDto
/**
* C端用户登录
*/
@Data
public class UserLoginRequestDto {
@ApiModelProperty("昵称")
private String nickName;
@ApiModelProperty("登录临时凭证")
private String code;
@ApiModelProperty("手机号临时凭证")
private String phoneCode;
}
返回类型:LoginVo
@Data
@ApiModel(value = "登录对象")
public class LoginVo {
@ApiModelProperty(value = "JWT token")
private String token;
@ApiModelProperty(value = "昵称")
private String nickName;
}
5.3.Mapper
在整个登录的过程中,会涉及到用户的新增、更新、查询,所以我们需要定义三个方法,代码如下:
/**
* @author sjqn
*/
@Mapper
public interface MemberMapper {
@Select("SELECT * FROM member WHERE open_id = #{openId}")
Member getByOpenId(String openId);
void save(Member member);
void update(Member member);
}
MemberMapper.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.zzyl.mapper.MemberMapper">
<resultMap id="BaseResultMap" type="com.zzyl.entity.Member">
<id column="id" property="id" />
<result column="phone" property="phone" />
<result column="name" property="name" />
<result column="avatar" property="avatar" />
<result column="open_id" property="openId" />
<result column="gender" property="gender" />
<result column="create_by" property="createBy"/>
<result column="update_by" property="updateBy"/>
<result column="remark" property="remark"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<insert id="save" parameterType="com.zzyl.entity.Member" keyProperty="id" useGeneratedKeys="true">
INSERT INTO member ( phone, name, avatar, open_id, gender, create_by, create_time)
VALUES ( #{phone}, #{name}, #{avatar}, #{openId}, #{gender}, #{createBy}, #{createTime})
</insert>
<update id="update" parameterType="com.zzyl.entity.Member">
UPDATE member SET
phone = #{phone},
name = #{name},
avatar = #{avatar},
open_id = #{openId},
gender = #{gender},
update_by = #{updateBy},
update_time = #{updateTime}
WHERE id = #{id}
</update>
</mapper>
5.4.业务层
我们从整个流程可以看出来,在后台实现的过程中,我们需要调用两次微信开发者平台的接口来获取数据,一般像这种第三方接口的调用,我们通常都会封装一个单独的业务代码,使其更通用
我们可以先分析微信开发者平台的接口,接口地址:
-
获取用户openId
-
获取手机号
-
获取token:获取接口调用凭据 | 微信开放文档
-
5.4.1.微信接口调用-单独封装
新增WechatService
/**
* @author sjqn
*/
public interface WechatService {
/**
* 获取openid
* @param code 登录凭证
* @return
* @throws IOException
*/
public String getOpenid(String code) ;
/**
* 获取手机号
* @param code 手机号凭证
* @return
* @throws IOException
*/
public String getPhone(String code);
}
WechatService实现类
/**
* @author sjqn
*/
@Service
public class WechatServiceImpl implements WechatService {
// 登录
private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code";
// 获取token
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential";
// 获取手机号
private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
@Value("${zzyl.wechat.appId}")
private String appId;
@Value("${zzyl.wechat.appSecret}")
private String secret;
/**
* 获取openid
*
* @param code 登录凭证
* @return
* @throws IOException
*/
@Override
public String getOpenid(String code) throws IOException {
//封装参数
Map<String,Object> requestUrlParam = getAppConfig();
requestUrlParam.put("js_code",code);
String result = HttpUtil.get(REQUEST_URL, requestUrlParam);
JSONObject jsonObject = JSONUtil.parseObj(result);
// 若code不正确,则获取不到openid,响应失败
if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getStr("openid");
}
/**
* 封装公共参数
* @return
*/
private Map<String, Object> getAppConfig() {
Map<String, Object> requestUrlParam = new HashMap<>();
requestUrlParam.put("appid",appId);
requestUrlParam.put("secret",secret);
return requestUrlParam;
}
/**
* 获取手机号
*
* @param code 手机号凭证
* @return
* @throws IOException
*/
@Override
public String getPhone(String code) throws IOException {
//获取access_token
String token = getToken();
//拼接请求路径
String url = PHONE_REQUEST_URL + token;
Map<String,Object> param = new HashMap<>();
param.put("code",code);
String result = HttpUtil.post(url, JSONUtil.toJsonStr(param));
JSONObject jsonObject = JSONUtil.parseObj(result);
if (jsonObject.getInt("errcode") != 0) {
//若code不正确,则获取不到phone,响应失败
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getJSONObject("phone_info").getStr("purePhoneNumber");
}
public String getToken(){
Map<String, Object> requestUrlParam = getAppConfig();
String result = HttpUtil.get(TOKEN_URL, requestUrlParam);
//解析
JSONObject jsonObject = JSONUtil.parseObj(result);
//如果code不正确,则失败
if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getStr("access_token");
}
}
由于上述代码需要读取配置来获取微信小程序的appid和appSecret ,所以需要在application.yml添加对应配置,这里大家注意,这个要跟微信开发者平台设置的相同,就是自己申请的测试小程序的appid和appSecret
大家注意格式:zzyl.wechat.appid
zzyl:
wechat:
appId: wxa54e0997e8b6649e
appSecret: 9e4b693aa8cee71a9077e15dcac30b52
注意,使用自己申请的appid和secret,不然小程序无法登录
5.4.2.微信登录业务开发
定义:MemberService
/**
* @author sjqn
*/
public interface MemberService {
/**
* 小程序端登录
* @param userLoginRequestDto
* @return
*/
LoginVo login(UserLoginRequestDto userLoginRequestDto);
}
定义:MemberService实现类MemberServiceImpl
/**
* @author sjqn
* @date 2023/10/27
*/
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private WechatService wechatService;
@Autowired
private MemberMapper memberMapper;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
static ArrayList DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
"生活更美好",
"大桔大利",
"日富一日",
"好柿开花",
"柿柿如意",
"一椰暴富",
"大柚所为",
"杨梅吐气",
"天生荔枝"
);
/**
* 小程序端登录
*
* @param userLoginRequestDto
* @return
*/
@Override
public LoginVo login(UserLoginRequestDto userLoginRequestDto){
//1.调用微信api,根据code获取openId
String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
//2.根据openId查询用户
Member member = memberMapper.getByOpenId(openId);
//3.如果用户为空,则新增
if (ObjectUtil.isEmpty(member)) {
member = Member.builder().openId(openId).build();
}
//4.调用微信api获取用户绑定的手机号
String phone = wechatService.getPhone(userLoginRequestDto.getPhoneCode());
//5.保存或修改用户
saveOrUpdate(member, phone);
//6.将用户id存入token,返回
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.JWT_USERID, member.getId());
claims.put(Constants.JWT_USERNAME, member.getName());
String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), claims);
LoginVo loginVo = new LoginVo();
loginVo.setToken(token);
loginVo.setNickName(member.getName());
return loginVo;
}
/**
* 保存或修改客户
*
* @param member
* @param phone
*/
private void saveOrUpdate(Member member, String phone) {
//1.判断取到的手机号与数据库中保存的手机号是否一样
if(ObjectUtil.notEqual(phone, member.getPhone())){
//设置手机号
member.setPhone(phone);
}
//2.判断id存在
if (ObjectUtil.isNotEmpty(member.getId())) {
memberMapper.update(member);
return;
}
//3.保存新的用户
//随机组装昵称,词组+手机号后四位
String nickName = DEFAULT_NICKNAME_PREFIX.get((int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size()))
+ StringUtils.substring(member.getPhone(), 7);
member.setName(nickName);
memberMapper.save(member);
}
}
其中jwt相关的配置,我们已经在application.yml文件中定义,主要有两个属性,一个是签名,一个是过期时间
zzyl:
framework:
jwt:
base64-encoded-secret-key: $2a$10$PVtHnkj86mJgf6li/yron.LRx/cQAlaiZkBJ9BeogCNTryXJRT1YC
ttl: 3600000
读取配置文件的配置类
/**
* jw配置文件
*/
@Setter
@Getter
@NoArgsConstructor
@ToString
@Configuration
@ConfigurationProperties(prefix = "zzyl.framework.jwt")
public class JwtTokenManagerProperties implements Serializable {
/**
* 签名密码
*/
private String base64EncodedSecretKey;
/**
* 有效时间
*/
private Integer ttl;
}
6.校验token
6.1.思路分析
用户登录成功以后,会返回前端一个token,这个token就是来验证用户信息的,当用户点击了小程序中的其他操作(需要登录),则会把token携带到请求头(header)中,方便后台去验证并获取用户信息,简易流程如下:
如果想要验证用户的token,我们可以使用自定义的拦截器实现,整体的流程如下:
6.2.功能实现
在common模块中定义拦截器,代码如下:
@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果不是映射到方法就放行,比如跨域验证请求、静态资源等不需要身份校验的请求
if(!(handler instanceof HandlerMethod)){
return true;
}
//获取header的参数
String token = request.getHeader(Constants.USER_TOKEN);
log.info("开始解析 customer user token:{}",token);
if(ObjectUtil.isEmpty(token)){
//token失效
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
Map<String,Object> claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), token);
if (ObjectUtil.isEmpty(claims)) {
//token失效
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
//获取用户ID
Long userId = MapUtil.get(claims, Constants.JWT_USERID, Long.class);
if (ObjectUtil.isEmpty(userId)) {
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
//存入当前请求的线程中
UserThreadLocal.set(userId);
//以上检查都没问题,一定返回true,这个请求才能继续
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//响应结束,需清理ThreadLocal中的数据,防止内存泄漏
UserThreadLocal.remove();
}
}
在自定义常量类Constants中新增常量,如下:
public static final String USER_TOKEN ="authorization";
使拦截器生效
/**
* @ClassName WebMvcConfig.java
* @Description webMvc高级配置
*/
@Configuration
@ComponentScan("springfox.documentation.swagger.web")
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
//拦截的时候过滤掉swagger相关路径和登录相关接口
private static final String[] EXCLUDE_PATH_PATTERNS = new String[]{"/swagger-ui.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs",
// 登录接口
"/customer/user/login"};
/**
* @Description 拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 小程序端接口鉴权拦截器
registry.addInterceptor(userInterceptor)
.excludePathPatterns(EXCLUDE_PATH_PATTERNS)
.addPathPatterns("/customer/**");
}
}