流程分析
1.在登录页准备微信登录的按钮 2.当用户点击微信扫码登录,页面向微信发起获取授权的请求 ① http://xxxxxx.xxx.cn/callback.html === 3.微信直接展示扫描二维码给用户(询问用户要不要给我们项目授权) 4.用户扫码,确认授权页面可以获取微信用户信息 5.微信收到确认,生成code(授权码),通过回调域(pethome-web)拼接code返回 http:/xxxxxx.xxx.cn/callback.html?code=asdfaosur464f6asdfasdfadf 6.我们项目在callback.html页面上就可以获取授权码了 一:微信登录流程 1.在callback.html页面中,我们定义钩子函数。 从请求栏中获取code,并且触发调用后端的微信登录接口,将code传送到后端 2.后端接口收到请求,交给service处理 3.service业务流 4.code不能为空 5.根据code从微信获取token 使用httpClient ② 6.拿到token+openId 7.判断openId是已经存在(查询t_wxUser), 7.1.如果已经有了并且userid不为空,直接免密登录 7.2 如果为空,需要让用户绑定个人用户信息 返回token+openId 前端帮我们跳转到绑定页面 二:微信绑定流程 1.在callback.html页面的钩子函数中 2.接收微信登录流程的返回值: AjaxResult {success:false,resultObj:"?token=asdfaf$openId=136462315546"} 3.跳转到binder.html页面 location.href="binder.html"+resultObj; 4.binder.html页面解析地址栏参数并且接收用户输入的参数 5.发起微信绑定流程 phone verifyCode token openId 6.后端接收参数交给service处理 7.service业务流 一:校验 1.空校验 2.判断验证码 二:判断phone是否被注册 user 1.如果注册了,那么我们可以直接绑定微信用户了 2.如果没有注册过,生成t_user + t_loginInfo 三:通过 token+openId 查询微信信息 wxuser ③ 生成t_wxuser 四:绑定user和wxuser 五:免密登录
具体实现流程
打开微信开发平台(https://open.weixin.qq.com/ ):
注册完成后,需要进行开发者的认证:
登录后,点击登录名,进入:
大致需要个人身份证等真实信息,还需要企业签字盖章等流程,认证一次300人民币。
认证成功后,创建网站应用,也需要企业签字盖章,还需要备案的域名 ,作为微信的回调
创建完成后,获取到appid和appsecret,配置好回调域名。
一切参照官方文档:
上线:
开发阶段:
路由解析原理
6.4 配置电脑HOST文件
Host文件配置
127.0.0.1 xxxxxxx.xxxxx.xx(此处为微信开发平台注册申请的域名)
通过原理分析,要发三个请求
授权请求-a标签链接过去就OK 获取AccessToken等信息请求- Httpclient
在我们项目中以后这样用代码来发送请求的情况有可能很多,所以抽取一个工具类,以后专门发请求。
流程分析
工具类的封装
package cn.itsource.basic.util; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.params.HttpMethodParams; import java.io.IOException; /** * 使用 httpclient 组件发送 http 请求 * get :现在只用到 get * post */ public class HttpClientUtils { /** * 发送 get 请求 * @param url 请求地址 * @return 返回内容 json */ public static String httpGet(String url){ // 1 创建发起请求客户端 try { HttpClient client = new HttpClient(); // 2 创建要发起请求 -tet GetMethod getMethod = new GetMethod(url); // getMethod.addRequestHeader("Content-Type", // "application/x-www-form-urlencoded;charset=UTF-8"); getMethod.getParams().setParameter(HttpMethodParams. HTTP_CONTENT_CHARSET , "utf8" ); // 3 通过客户端传入请求就可以发起请求 , 获取响应对象 client.executeMethod(getMethod); // 4 提取响应 json 字符串返回 String result = new String(getMethod.getResponseBodyAsString().getBytes( "utf8" )); return result; } catch (IOException e) { e.printStackTrace(); } return null ; } }
常量封装
package cn.itsource.user.constant; // 微信登录相关常量 public class WxConstants { public static final String APPID = "wxd853562a0548a7d0" ; public static final String SECRET = "4a5d5615f93f24bdba2ba8534642dbb6" ; public static final String GET_ACK_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code" ; public static final String GET_USER_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID" ; }
检查配置回调域名hosts配置
核心代码 实现
<!--处理json-->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
跳转授权界面
Login.html
< script type= "text/javascript" > //创建vue是需要给他传递一个对象 new Vue ({ el : "#loginMain" , //id在hmtl模板中要对应 data : { //数据模型 loginForm : { username : '' , password : '' }, wxAuthUrl : ' https://open.weixin.qq.com/connect/qrconnect?appid=wxd853562a0548a7d0' + '&redirect_uri=http://bugtracker.itsource.cn/callback.html&response_type=code&scope=snsapi_login&state=1#wechat_redirect' },
回调页面
<!DOCTYPE html > < html lang= "en" > < head > < meta charset= "UTF-8" > < title > 回调 </ title > <!-- 集成 vue 和 axios ,还要 axios 全局配置(导入 common.js ) --> <!--script src 方式引入 vue 和 axios--> < script src= "js/plugins/vue/dist/vue.js" ></ script > < script src= "js/plugins/axios/dist/axios.js" ></ script > <!-- 全局配置,以后只要用 vue+axios 的页面都引入 common.js--> < script src= "js/common.js" ></ script > </ head > < body > < div id= "myDiv" > </ div > < script type= "text/javascript" > new Vue({ el : "#myDiv" , mounted (){ // 解析参数对象 let url = location .href; let paramObj = parseUrlParams2Obj ( url ); // 获取发送请求参数 let binderUrl = "http://bugtracker.itsource.cn/binder.html" let params = { "code" : paramObj . code , "binderUrl" : binderUrl }; // 发起微信登录请求 this . $http . post ( "/login/wechat" , params ) . then (result=>{ result = result.data; if (result.success){ // 已经关联了 // 做登录 // 提示 alert ( " 登录成功! " ) // 把 token 和 loginInfo 存放到 localStorage let { token , loginInfo } = result.resultObj; localStorage . setItem ( "token" , token ); // 把对象转换为 json 字符串存放 localStorage . setItem ( "loginInfo" , JSON . stringify ( loginInfo )); // 跳转主页 location .href = "/index.html" ; } else { // 没有关联跳转关联页面 let url = result.resultObj; location .href = url ; } }) . catch (result=>{ alert ( " 系统错误 " ); console . log (result); }) } }); </ script > </ body > </ html >
跳转后台微信登录
LoginInfocontroller 判断是否已经绑定,如果绑定就免密登录,否则返回一个未绑定错误,配合前端跳转到绑定页面
@PostMapping ( "/wechat" ) public AjaxResult loginWechat( @RequestBody Map<String,String> params){ try { return loginInfoService .loginWechat(params); } catch (Exception e) { e.printStackTrace(); return AjaxResult. me ().setMessage( " 系统错误! " +e.getMessage()); } }
LoginInfoServiceImpl
@Override public AjaxResult loginWechat(Map<String, String> params) { //1 获取 code String code = params.get( "code" ); String binderUrl = params.get( "binderUrl" ); //2 获取 accessToken String getAckUrl = WxConstants. GET_ACK_URL .replace( "APPID" , WxConstants. APPID ) .replace( "SECRET" , WxConstants. SECRET ) .replace( "CODE" , code); String jsonStr = HttpClientUtils. httpGet (getAckUrl); JSONObject jsonObject = JSONObject. parseObject (jsonStr); String accessToken = jsonObject.getString( "access_token" ); String openid = jsonObject.getString( "openid" ); // 就相当于微信号 //3 判断是否已经关联了 WxUser wxUser = wxUserMapper .loadByOpenId(openid); if (wxUser!= null && wxUser.getUser_id()!= null ){ // 查询 Logininfo LoginInfo loginInfo = loginInfoMapper .loadByUserId(wxUser.getUser_id()); //3.1 如果关联了实现免密登录 String token = UUID. randomUUID ().toString(); redisTemplate .opsForValue().set(token,loginInfo, 30 , TimeUnit. MINUTES ); Map<String,Object> result = new HashMap<>(); result.put( "token" ,token); loginInfo.setSalt( null ); loginInfo.setPassword( null ); result.put( "loginInfo" ,loginInfo); return AjaxResult. me ().setResultObj(result); } else { //3.2 否则跳转到绑定页面 binderUrl = binderUrl+ "?accessToken=" +accessToken+ "&openId=" +openid; return AjaxResult. me ().setSuccess( false ).setResultObj(binderUrl); } }
绑定页面
<!DOCTYPE html > < html > < head lang= "en" > < meta charset= "UTF-8" > < title > 注册 </ title > < meta http-equiv= "X-UA-Compatible" content= "IE=edge" > < meta name= "viewport" content= "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" > < meta name= "format-detection" content= "telephone=no" > < meta name= "renderer" content= "webkit" > < meta http-equiv= "Cache-Control" content= "no-siteapp" /> < link rel= "stylesheet" href= "./AmazeUI-2.4.2/assets/css/amazeui.min.css" /> < link href= "./css/dlstyle.css" rel= "stylesheet" type= "text/css" > < script src= "./AmazeUI-2.4.2/assets/js/jquery.min.js" ></ script > < script src= "./AmazeUI-2.4.2/assets/js/amazeui.min.js" ></ script > <!--script src 方式引入 vue 和 axios--> < script src= "js/plugins/vue/dist/vue.js" ></ script > < script src= "js/plugins/axios/dist/axios.js" ></ script > <!-- 全局配置,以后只要用 vue+axios 的页面都引入 common.js--> < script src= "js/common.js" ></ script > <!-- 一个一个页面配置,搞一个公共 common.js ,以后只需要引入它就 ok--> <!--<script type="text/javascript">--> <!--// 配置 axios 的全局基本路径 --> <!--axios.defaults.baseURL='http://localhost:8080/'--> <!--// axios.defaults.baseURL='/api' // 前端跨域配置 --> <!--// 全局属性配置,在任意组件内可以使用 this.$http 获取 axios 对象 --> <!--Vue.prototype.$http = axios--> <!--</script>--> </ head > < body > < div class= "login-boxtitle" > < a href= "home/demo.html" >< img alt= "" src= "./images/logobig.png" /></ a > </ div > < div class= "res-banner" > < div class= "res-main" > < div class= "login-banner-bg" >< span ></ span >< img src= "./images/big.jpg" /></ div > < div class= "login-box" > < div class= "am-tabs" id= "doc-my-tabs" > < div class= "am-tabs-bd" > < div class= "am-tab-panel am-active" id= "myDiv" > < form method= "post" > < div class= "user-phone" > < label for= "phone" >< i class= "am-icon-mobile-phone am-icon-md" ></ i ></ label > < input type= "tel" name= "" id= "phone" v-model= " phoneUserForm . phone " placeholder= " 请输入手机号 " > </ div > < div class= "verification" > < label for= "code" >< i class= "am-icon-code-fork" ></ i ></ label > < input type= "tel" name= "" id= "code" v-model= " phoneUserForm . verifyCode " placeholder= " 请输入验证码 " > <!--<a class="btn" href="javascript:void(0);" οnclick="sendMobileCode();" id="sendMobileCode">--> <!--<span id="dyMobileButton"> 获取 </span></a>--> < button type= "button" @click= "sendMobileCode" > 获取 </ button > </ div > </ form > < div class= "login-links" > < label for= "reader-me" > < input id= "reader-me" type= "checkbox" > 点击表示您同意商城《服务协议》 </ label > </ div > < div class= "am-cf" > < input type= "button" @click= "binder" name= "" value= " 绑定授权 " class= "am-btn am-btn-primary am-btn-sm am-fl" > </ div > < hr > </ div > < script > $ ( function () { $ ( '#doc-my-tabs' ). tabs (); }) </ script > </ div > </ div > </ div > </ div > < div class= "footer " > < div class= "footer-hd " > < p > < a href= "# " > 恒望科技 </ a > < b > | </ b > < a href= "# " > 商城首页 </ a > < b > | </ b > < a href= "# " > 支付宝 </ a > < b > | </ b > < a href= "# " > 物流 </ a > </ p > </ div > < div class= "footer-bd " > < p > < a href= "# " > 关于恒望 </ a > < a href= "# " > 合作伙伴 </ a > < a href= "# " > 联系我们 </ a > < a href= "# " > 网站地图 </ a > < em > © 2015-2025 Hengwang.com 版权所有 . 更多模板 < a href= "http://www.cssmoban.com/" target= "_blank" title= " 模板之家 " > 模板之家 </ a > - Collect from < a href= "http://www.cssmoban.com/" title= " 网页模板 " target= "_blank" > 网页模板 </ a ></ em > </ p > </ div > </ div > </ body > < script type= "text/javascript" > new Vue({ "el" : "#myDiv" , data :{ phoneUserForm :{ phone : "13330964748" , verifyCode : "" , accessToken : null , openId : null } }, methods :{ binder (){ this . $http . post ( "/login/binder/wechat" , this . phoneUserForm ) . then (result=>{ result = result.data; // 提示 alert ( " 登录成功! " ) // 把 token 和 loginInfo 存放到 localStorage let { token , loginInfo } = result.resultObj; localStorage . setItem ( "token" , token ); // 把对象转换为 json 字符串存放 localStorage . setItem ( "loginInfo" , JSON . stringify ( loginInfo )); console . log (result, "fjfjjfjfjfjfjjfjf" ) // 跳转主页 location .href = "/index.html" ; }) . catch (result=>{ console . log (result, "jjjjj" ) alert ( " 系统错误! " ); }) }, sendMobileCode (){ //1. 判断手机号不为空 if (! this . phoneUserForm . phone ){ alert ( " 手机号不能为空 " ); return ; } //2. 获取按钮,禁用按钮 发送时灰化不能使用,发送成功倒计时 60 才能使用,如果发送失败立即可以发送 var sendBtn = $ ( event . target ); sendBtn . attr ( "disabled" , true ); this . $http . post ( '/verifycode/smsCode' , { "phone" : this . phoneUserForm . phone , "type" : "binder" }). then ((res) => { console . log (res); var ajaxResult = res.data; if ( ajaxResult . success ){ alert ( " 手机验证码已经发送到您的手机,请在 3 分钟内使用 " ); //3.1. 发送成:倒计时 var time = 60 ; var interval = window . setInterval ( function () { // 每一条倒计时减一 time = time - 1 ; // 把倒计时时间搞到按钮上 sendBtn . html ( time ); //3.2. 倒计时完成恢复按钮 if ( time <= 0 ){ sendBtn . html ( " 重发 " ); sendBtn . attr ( "disabled" , false ); // 清除定时器 window . clearInterval ( interval ); } }, 1000 ); } else { //3.3. 发送失败:提示,恢复按钮 sendBtn . attr ( "disabled" , false ); alert ( " 发送失败 :" + ajaxResult . message ); } }); } }, mounted (){ let paramObj = parseUrlParams2Obj ( location .href); if ( paramObj ){ this . phoneUserForm . accessToken = paramObj . accessToken ; this . phoneUserForm . openId = paramObj . openId ; } } }) </ script > </ html >
改造发送短信验证接口
@RestController @RequestMapping ( "/verifycode" ) public class VerifyCodeController { @Autowired private IVerifyCodeService verifyCodeService ; // 一定情况下 Map 能够代替类使用 @PostMapping ( "/smsCode" ) // 注册验证码 public AjaxResult sendSmsCode( @RequestBody Map<String,String> params){ String phone = params.get( "phone" ); String type = params.get( "type" ); //register binder login try { verifyCodeService .sendSmsCode(params); return AjaxResult. me (); } catch (BusinessException e){ e.printStackTrace(); return AjaxResult. me ().setMessage( " 发送失败 !" +e.getMessage()); } catch (Exception e) { return AjaxResult. me ().setMessage( " 系统错误 !" +e.getMessage()); } } }
VerifyCodeServiceImpl
@Override public void sendSmsCode(Map<String,String> params) { String phone = params.get( "phone" ); String type = params.get( "type" ); //1 校验 //1.1 手机号不能为 null if (!StringUtils. hasLength (phone)) throw new BusinessException( " 请输入手机号! " ); if ( "register" .equals(type)){ // 注册 //1.2 不能被注册 User user = userMapper .loadByPhone(phone); if (user!= null ) throw new BusinessException( " 用户已经被注册! " ); String businessKey = UserConstants. REGISTER_CODE_PREFIX + phone; sendSmsCode(businessKey); } else if ( "binder" .equals(type)){ // 绑定 String businessKey = UserConstants. BINDER_CODE_PREFIX + phone; sendSmsCode(businessKey); } } private void sendSmsCode(String businessKey){ //2 判断原来的是否有效 Object codeObj = redisTemplate .opsForValue().get(businessKey); //code:time String code = "" ; //2.1 如果有效 if (codeObj!= null ){ String codeStr = (String) codeObj; //2.1.1 判断是否已过重发时间 String time = codeStr.split( ":" )[ 1 ]; //114555558888 long intervalTime = System. currentTimeMillis () - Long. valueOf (time); if (intervalTime<= 1 * 60 * 1000 ){ //2.1.1.1 如果没有过提示错误 throw new BusinessException( " 请勿重复发送短信验证码! " ); } //2.1.1.2 如果过了,就使用原来验证码 code = codeStr.split( ":" )[ 0 ]; } else { //2.2 如果没有 //2.2.1 重新生成验证码 code = StrUtils. getComplexRandomString ( 4 ); } //3 保存验证码到 redis redisTemplate .opsForValue().set(businessKey ,code+ ":" +System. currentTimeMillis () , 3 , TimeUnit. MINUTES ); //4 调用短信接口发送短信 // SmsUtil.sendMsg(phone," 您的验证码为: "+code+", 请在 3 分钟之内使用! "); System. out .println( " 您的验证码为: " +code+ ", 请在 3 分钟之内使用! " ); }
绑定实现
LoginController
@PostMapping ( "/binder/wechat" ) public AjaxResult binderWechat( @RequestBody Map<String,String> params){ try { return loginInfoService .binderWechat(params); } catch (Exception e) { e.printStackTrace(); return AjaxResult. me ().setMessage( " 系统错误! " +e.getMessage()); } }
LoginInfoService
// 前台输入手机号是否有账号,如果有创建 wxUser 帮上就 ok ,如果没有创建账号在绑定 @Override public AjaxResult binderWechat(Map<String, String> params) { // 参数 String phone = params.get( "phone" ); String verifyCode = params.get( "verifyCode" ); String accessToken = params.get( "accessToken" ); String openId = params.get( "openId" ); //0 验证码比对 Object codeObj = redisTemplate .opsForValue().get(UserConstants. BINDER_CODE_PREFIX + phone); if (codeObj== null ){ return AjaxResult. me ().setMessage( " 请重新获取验证码后再操作! " ); } else { String codeStr = (String) codeObj; String code = codeStr.split( ":" )[ 0 ]; //code : time if (!verifyCode.equalsIgnoreCase(code)){ return AjaxResult. me ().setMessage( " 请输入正确验证码后再操作! " ); } } //1 获取微信用户信息 String url = WxConstants. GET_USER_URL .replace( "ACCESS_TOKEN" , accessToken) .replace( "OPENID" , openId); String jsonStr = HttpClientUtils. httpGet (url); //2 通过电话和 type 获取用户登录信息 LoginDto loginDto = new LoginDto(); loginDto.setUsername(phone); loginDto.setLoginType( 1 ); LoginInfo info = loginInfoMapper .loadByPhone(loginDto); //3 如果用户登录信息不存在 User user = null ; if (info== null ){ user = wxUser2User(phone); info = user2LoginInfo(user); //3.1 创建 loginInfo loginInfoMapper .save(info); //3.2 创建 User user.setInfo(info); userMapper .save(user); } else { //4 用户存在 查询用户 user = userMapper .loadByPhone(phone); } //5 把用户和 wxUser 进行绑定 WxUser wxUser = wxUserJsonStr2WxUser(jsonStr); wxUser.setUser_id(user.getId()); wxUserMapper .save(wxUser); //6 做免密登录 //3.1 如果关联了实现免密登录 String token = UUID. randomUUID ().toString(); redisTemplate .opsForValue().set(token,info, 30 , TimeUnit. MINUTES ); Map<String,Object> result = new HashMap<>(); result.put( "token" ,token); info.setSalt( null ); info.setPassword( null ); result.put( "loginInfo" ,info); return AjaxResult. me ().setResultObj(result); } private LoginInfo user2LoginInfo(User user) { LoginInfo info = new LoginInfo(); BeanUtils. copyProperties (user,info); // 按照同名原则拷贝属性 return info; } private User wxUser2User(String phone) { User user = new User(); user.setUsername(phone); user.setPhone(phone); user.setEmail( null ); // 给一个随机密码 String salt = UUID. randomUUID ().toString(); String password = MD5Utils. encrypByMd5 (StrUtils. getComplexRandomString ( 6 )+salt); user.setPassword(password); user.setSalt(salt); user.setState( 1 ); user.setCreatetime( new Date()); return user; } private WxUser wxUserJsonStr2WxUser(String jsonStr) { JSONObject jsonObject = JSONObject. parseObject (jsonStr); WxUser wxUser = new WxUser(); wxUser.setOpenid(jsonObject.getString( "openid" )); wxUser.setNickname(jsonObject.getString( "nickname" )); wxUser.setSex(jsonObject.getInteger( "sex" )); wxUser.setAddress( null ); wxUser.setHeadimgurl(jsonObject.getString( "headimgurl" )); wxUser.setUnionid(jsonObject.getString( "unionid" )); return wxUser; }
后台获取登录用户
package cn.itsource.basic.util; import cn.itsource.user.domain.LoginInfo; import org.springframework.beans.factory.annotation. Autowired ; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype. Component ; import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import javax.servlet.http.HttpServletRequest; /** * 登录的上下文 * 登录用户的一些信息放到这里面,以后直接获取就 ok 了 * 1 获取登录用户 * 2 获取登录用户的角色或权限 * .... * ========== 只是一个工具类不需要交给 Spring 管理 ================= * * ?? 一个不受 spirng 管理的 bean ,要获取受 spring 管理的 bean */ public class LoginContext { /** * 获取当前登录用户信息 * @param request * @return */ public static LoginInfo getLoginInfo(HttpServletRequest request){ // 从请求头中获取 token String token = request.getHeader( "token" ); // 使用 token 从 redis 中获取登录信息 if (!StringUtils. isEmpty (token)){ //1 获取 spring 容器 WebApplicationContext context = WebApplicationContextUtils . getWebApplicationContext (request.getServletContext()); //2 通过容器获取 bean RedisTemplate redisTemplate = (RedisTemplate) context .getBean( "redisTemplate" ); //3 获取登录信息 Object loginInfoObj = redisTemplate.opsForValue().get(token); if (loginInfoObj!= null ) return (LoginInfo) loginInfoObj; } return null ; } }