一. axios拦截器
1. axios携带token
1 2 3 4 5 6 7 8 9 10 11 12
|
axios.interceptors.request.use(res=>{ let token = localStorage.getItem("token"); if(token){ res.headers["token"] = token; } return res; },error => { Promise.reject(error) })
|
2. 后端拦截器
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
| @Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate stringRedisTemplate;
@Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { String token = req.getHeader("token"); if (token != null) { String loginInfo = stringRedisTemplate.opsForValue().get(Constants.LOGIN_TOKEN + token); if (loginInfo != null) { stringRedisTemplate.opsForValue().set(token, loginInfo, 30, TimeUnit.MINUTES); return true; } }
resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().println("{\"success\":false,\"message\":\"noLogin\"}"); return false; } }
|
3. 处理拦截器响应
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
axios.interceptors.response.use(res => { if (false === res.data.success && "noLogin" === res.data.message) { localStorage.removeItem("token"); localStorage.removeItem("logininfo"); router.push({path: '/login'}); } return res; },error => { Promise.reject(error) })
|
4. 路由拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
router.beforeEach((to, from, next) => { if (to.path == '/login' || to.path == "/register") { next(); }else{ let logininfo = localStorage.getItem('logininfo'); if (logininfo) { next(); } else { next({path: '/login'}); } } })
|
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
| handleSubmit2(ev) { this.$refs.ruleForm2.validate((valid) => { if (valid) { this.logining = true; this.$http.post("/login/account", this.ruleForm2).then(res => { if (res.data.success) { this.$message({ message: "登录成功", type: 'success' }); let {token, loginInfo} = res.data.data localStorage.setItem("token", token) localStorage.setItem("logininfo", JSON.stringify(logininfo)) console.log(res.data); this.$router.push({path: '/echarts'}); } else { this.$message.error(res.data.msg); } this.logining = false; }).catch(res => { this.$message.error("系统繁忙,请稍后重试!!!【400,404】") this.logining = false; }) } else { console.log('表单校验失败!!!'); return false; } }); }
|
二. 第三方登录概述
1. 什么是第三方登录
三方登录指的是基于用户在主流平台【微信,支付宝,QQ】上已有的账号来快速完成己方应用的登录或者注册的功能。而这里的主流平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。 第三方登录的目的是使用用户在其他平台上频繁使用的账号,来快速登内录己方产品,也可以实现不注册就能登录,好处就是登录比较快捷,无需注册。
2. 优缺点
- 优点:这些系统有很大的用户群体,可以扩大客户群,引流。不需要记录账号密码,不担心忘记,直接扫描登录,体验度高。简单快捷,无需注册就可以直接登录
三. 三方登录协议
1. OAuth2.0
OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。OAuth是Open Authorization的简写,目前的版本是2.0版.
https://oauth.net/2/
1
| 例如:使用微信登录,并不会获取到微信的账号密码,只需要同意授权即可。如果我们的项目被攻破了,那就知道了用户的微信账号,然后用户的微信就危险了。小平台或小公司的系统很容易被攻击甚至被攻破
|
2. 运行流程
- 获取用户授权
- 得到用户授权获取令牌
- 使用令牌访问受限资源
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
上述六个步骤中,B是关键,即用户怎样才能给客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭借令牌获取资源
四. 微信登录概述
- 开发网址:https://open.weixin.qq.com/
- 自己的网站可以接入网站应用开发,为用户提供了微信登录功能,降低了注册门槛,并可在用户授权后,获取用户基本信息,包括头像、昵称、性别、地区。出于安全考虑,网站应用的微信登录,需通过微信扫描二维码来实现
- 注册账号:要想接入微信的登录功能,首先需要在微信开发平台进行用户的注册,同时需要认证为开发者,再创建网站应用,等待微信审批,审批过后,就可以使用相关功能
- 开发者认证:认证一次300人民币。 以后要做第三方登录
- 微信登陆功能官网教程网址:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
五. 微信登录实现
1. 三个请求
- 微信登录一共发送三个请求:
- 授权请求 - a标签链接过去就OK,获取code
- 使用授权码code和appid和SECRET获取令牌token,返回token和openid - 后端使用Httpclient发送请求
- 如果微信用户没有绑定三方程序user,需要发送请求获取微信用户信息:token和openid
2. 配置回调域名
问:当扫码成功之后要跳转到哪个页面呢?
即使你在当前项目中定义一个页面,外网微信开发平台无法访问本地应用127.0.0.1
如果上线了:配置真实域名,但是测试阶段使用本地域名
3. 配置本地域名(注:域名换成自己购买的域名)
1 2 3
| 文件位置:C:\Windows\System32\drivers\etc\hosts Host文件配置:127.0.0.1 bugtracker.itsource.cn 注意:bugtracker.itsource.cn是真实有效的
|
六. 微信授权流程
1.用户点击微信登录,发送第一个请求,弹出二维码,请求地址:
1
| 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
|
2.用户使用手机扫码之后,点击同意授权,返回回调地址和code
回调地址:上一步redirect_uri设置的地址,该地址必须是一个可以访问的域名地址,localhost不行。
可以使用bugtracker.itsource.cn做测试
code:授权码
3.配置回调域名:hosts文件
准备callback.html回调页面,一个空页面,仅仅用来处理数据,并发送微信登录请求
注意:前端拦截器已经拦截了callback.html,而且bugtracker.itsource.cn域名要访问后端服务器要配置跨域
callback.html
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
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>回调</title>
<script src="js/plugins/vue/dist/vue.js"></script> <script src="js/plugins/axios/dist/axios.js"></script> <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 params = {"code": paramObj.code}; let code = paramObj.code this.$http.get("/login/wechat/" + code) .then(result => { result = result.data; console.log(result); if (result.success) { alert("登录成功!") let {token, loginInfo} = result.data; localStorage.setItem("token", token); localStorage.setItem("loginInfo", JSON.stringify(loginInfo)); location.href = "index.html"; } else { let binderUrl = "http://bugtracker.itsource.cn/binder.html" + "?accessToken=" + result.data.accessToken + "&openId=" + result.data.openId; location.href = binderUrl; } }) .catch(result => { alert("系统错误"); console.log(result); }) } }); </script> </body> </html>
|
common.js的全局配置文件
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
| axios.defaults.baseURL = 'http://localhost:8080';
Vue.prototype.$http = axios
axios.interceptors.request.use(res => { let token = localStorage.getItem("token"); if (token) { res.headers["token"] = token; } return res; }, error => { Promise.reject(error) })
axios.interceptors.response.use(res => { if (false === res.data.success && "noLogin" === res.data.message) { localStorage.removeItem("token"); localStorage.removeItem("loginInfo"); location.href = 'login.html' } return res; }, error => { Promise.reject(error) })
let url = location.href;
if (url.indexOf('login.html') == -1 && url.indexOf('register.html') == -1 && url.indexOf('binder.html') == -1 && url.indexOf('callback.html') == -1) { let logininfo = localStorage.getItem("loginInfo") if (!logininfo) { location.href = 'login.html' } }
function parseUrlParams2Obj(url) { let paramStr = url.substring(url.indexOf("?") + 1); let paramArr = paramStr.split("&"); let paramObj = {}; for (let i = 0; i < paramArr.length; i++) { let paramTemp = paramArr[i]; let paramName = paramTemp.split("=")[0]; let paramValue = paramTemp.split("=")[1]; paramObj[paramName] = paramValue; } return paramObj; }
|
七. 微信登录流程
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
| 1.在callback.html页面中解析地址栏中的code,并发送微信异步登录请求,传递code 2.后端处理微信登录请求,service业务中 2.1.获得授权码code,和appid,SECRET一起发送获取access_token的请求 请求地址2:https: 2.2:建议配置常量比较方便些,也比较好维护 2.3:返回值是一个json字符串【参考官网】,想办法将其转成json对象,才能获取里面的数据【fastJsoon】 { "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE", "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" } 3.根据openid去查询微信用户信息 a.如果有并且和user绑定了【有user_id】,说明绑定过,直接获取Logininfo对象实现免密登录 解释1:如果没有wxuser,说明第一次登录 解释2:如果用户以前登录过,后面注销了user信息,wxuser就关联不上了,获取级联清空了wxuser中的user_id 疑问:为啥要将wxuser与user绑定:以后不管是微信登录还是账号登录都是同一个账户,都是自己的信息 return true; b.如果没有wxuser信息并且也没有和user_id绑定 就通过Result将access_token和openid响应给前端,发送微信用户绑定请求 return false; 请求地址3:https: 测试:注意,微信只能扫一次,第二次扫的时候accessToken为null
|
7.1 微信登录常量
1 2 3 4 5 6 7
| public interface WxConstants { String APPID = "wxd853562a0548a7d0"; String GET_ACK_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code"; String SECRET = "4a5d5615f93f24bdba2ba8534642dbb6"; String GET_USER_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID"; }
|
7.2 微信登录业务代码
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
| @Override public Result wechatLogin(String code) {
final String url = WxConstants.GET_ACK_URL.replace("APPID", WxConstants.APPID) .replace("SECRET", WxConstants.SECRET).replace("CODE", code);
final String jsonStr = HttpUtil.httpGet(url); final JSONObject jsonObject = JSONObject.parseObject(jsonStr); final String access_token = jsonObject.getString("access_token"); final String openid = jsonObject.getString("openid"); final WxUser wxUser = wxUserMapper.selectOne(new LambdaQueryWrapper<WxUser>().eq(WxUser::getOpenid, openid)); if (wxUser != null) { final User user = userMapper.selectById(wxUser.getUserId()); if (user != null) { final LoginInfo loginInfo = loginInfoMapper.selectById(user.getLogininfoId()); final String token = UUID.randomUUID().toString(); stringRedisTemplate.opsForValue().set(Constants.LOGIN_TOKEN + token, JSON.toJSONString(loginInfo), 30, TimeUnit.MINUTES); Map<String, Object> map = new HashMap<>(); map.put("token", token); map.put("loginInfo", loginInfo); return Result.success(map); } else { return Result.fail("用户未注册"); } } else { Map<String, Object> map = new HashMap<>(); map.put("accessToken", access_token); map.put("openId", openid); return Result.fail(map); } }
|
八. 微信绑定流程
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
| 1.callback.html发送异步请求的else中获取后端响应的access_token和openid 2.将其拼接到binder.html后跳转到binder.html页面 binder.html?accessToken=access_token&openId=openid 3.binder.html页面,输入手机号码,获取验证码 4.后端处理获取验证码请求 5.前端binder.html页面收到验证码之后,填写验证码 5.1.页面一加载要解析url获取access_token和openid,复制给模型数据 phoneUserForm:{ phone:"13330964748", verifyCode:null, accessToken:null, openId:null } 6.发送绑定微信用户请求 7.后端处理微信用户绑定请求 8.后端处理微信用户绑定请求 8.1.校验验证码和验证码过期时间 8.2.发送第三个请求获取wxuser信息 8.3.将wxuser信息转成WxUser对象:WxUser wxUser = wxUserStr2WxUser(wxUserStr); 8.4.根据电话从头t_user中获取用户信息,进行判断 8.5.如过user==null,通过手机号构建一个User对象,密码随机的6位,并同步Logininfo信息,保存到数据库 8.6.如果user!=null,直接就用这个user对象 8.7.将wxUser与User绑定 = wxUser的user_id关联起来 8.8.添加用户到t_wxUser 8.9.免密登录
|
8.1 发送微信绑定请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| binder() { this.$http.post("/login/wechat/binder", this.phoneUserForm).then(result => { console.log(this.phoneUserForm) result = result.data; if (result.success) { alert("绑定成功!") let {token, loginInfo} = result.data; localStorage.setItem("token", token); localStorage.setItem("loginInfo", JSON.stringify(loginInfo)); location.href = "/index.html"; } else { alert(result.msg) } }).catch(result => { alert("系统错误!"); }) },
|
8.2 binder.html页面初始化数据
1 2 3 4 5 6 7
| mounted() { let paramObj = parseUrlParams2Obj(location.href); if (paramObj) { this.phoneUserForm.accessToken = paramObj.accessToken; this.phoneUserForm.openId = paramObj.openId; } }
|
8.3 绑定时获取验证码
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
| @Override public void binderSmsCode(PhoneCodeDTO codeDTO) { final String phone = codeDTO.getPhone(); if (StrUtil.isBlank(phone)) { throw new BusinessException("手机号码不能为空"); } final User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)); if (user != null) { final WxUser wxUser = wxUserMapper.selectOne(new LambdaQueryWrapper<WxUser>().eq(WxUser::getUserId, user.getId())); if (wxUser != null) { throw new BusinessException("手机号已经绑定其它微信账号,请直接登录..."); } } final String value = stringRedisTemplate.opsForValue().get(VerifyCodeConstants.PHONE_CODE + phone); String code = null; if (value != null) { final long oldTime = Long.parseLong((value.split(":")[1])); if ((System.currentTimeMillis() - oldTime) <= 60) { throw new BusinessException("操作频繁,请稍后再试!"); } else { code = value.split(":")[0]; } } else { code = RandomUtil.randomNumbers(6); } stringRedisTemplate.opsForValue().set(VerifyCodeConstants.PHONE_CODE + phone, code, 3L, TimeUnit.MINUTES); log.info("授权短信验证码:{}", code); }
|
8.4 微信绑定业务代码
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
| @Override public Result wechatBinder(BinderDTO binderDTO) { final String phone = binderDTO.getPhone(); final String verifyCode = binderDTO.getVerifyCode(); if (StrUtil.isBlank(phone) || StrUtil.isBlank(verifyCode)) { throw new BusinessException("数据不能为空"); } final String value = stringRedisTemplate.opsForValue().get(VerifyCodeConstants.PHONE_CODE + phone); if (value == null) { throw new BusinessException("验证码已经过期!!!"); } if (!verifyCode.equals(value.split(":")[0])) { throw new BusinessException("验证码错误!!!"); }
String url = WxConstants.GET_USER_URL.replace("ACCESS_TOKEN", binderDTO.getAccessToken()) .replace("OPENID", binderDTO.getOpenId()); final String jsonStr = HttpUtil.httpGet(url); WxUser wxUser = jsonStr2WxUser(jsonStr); User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)); if (user == null) { user = phone2User(phone); LoginInfo loginInfo = user2LoginInfo(user); loginInfoMapper.insert(loginInfo); user.setLogininfoId(loginInfo.getId()); userMapper.insert(user); } wxUser.setUserId(user.getId()); wxUserMapper.insert(wxUser); final LoginInfo loginInfo = loginInfoMapper.selectById(user.getLogininfoId()); loginInfo.setPassword(null); loginInfo.setSalt(null); final String token = UUID.randomUUID().toString(); stringRedisTemplate.opsForValue().set(Constants.LOGIN_TOKEN + token, JSON.toJSONString(loginInfo), 30, TimeUnit.MINUTES); final HashMap<String, Object> map = new HashMap<>(); map.put("token", token); map.put("loginInfo", loginInfo); return Result.success(map); }
private LoginInfo user2LoginInfo(User user) { final LoginInfo loginInfo = BeanUtil.copyProperties(user, LoginInfo.class); loginInfo.setDisable(true); return loginInfo; }
private User phone2User(String phone) { final User user = new User(); user.setPhone(phone); user.setUsername(phone); user.setState(1); final String salt = RandomUtil.randomString(32); final String pwd = RandomUtil.randomNumbers(6); user.setSalt(salt); user.setPassword(DigestUtil.md5Hex(salt + pwd)); return user; }
private WxUser jsonStr2WxUser(String jsonStr) { final JSONObject res = JSON.parseObject(jsonStr); final WxUser wxUser = new WxUser(); wxUser.setNickname(res.getString("nickname")); wxUser.setSex(res.getInteger("sex")); wxUser.setOpenid(res.getString("openid")); wxUser.setHeadimgurl(res.getString("headimgurl")); wxUser.setUnionid(res.getString("unionid")); wxUser.setAddress(res.getString("country") + res.getString("province") + res.getString("city")); log.info("nickname:{}", res.getString("nickname")); log.info("city:{}", res.getString("city")); log.info("province:{}", res.getString("province")); log.info("headimgurl:{}", res.getString("headimgurl")); return wxUser; }
|
九. 后端发送Http请求工具类
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| package io.coderyeah.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.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.Map;
public class HttpUtil {
public static String post(String requestUrl, String accessToken, String params) throws Exception { String contentType = "application/x-www-form-urlencoded"; return HttpUtil.post(requestUrl, accessToken, contentType, params); }
public static String post(String requestUrl, String accessToken, String contentType, String params) throws Exception { String encoding = "UTF-8"; if (requestUrl.contains("nlp")) { encoding = "GBK"; } return HttpUtil.post(requestUrl, accessToken, contentType, params, encoding); }
public static String post(String requestUrl, String accessToken, String contentType, String params, String encoding) throws Exception { String url = requestUrl + "?access_token=" + accessToken; return HttpUtil.postGeneralUrl(url, contentType, params, encoding); }
public static String postGeneralUrl(String generalUrl, String contentType, String params, String encoding) throws Exception { URL url = new URL(generalUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", contentType); connection.setRequestProperty("Connection", "Keep-Alive"); connection.setUseCaches(false); connection.setDoOutput(true); connection.setDoInput(true);
DataOutputStream out = new DataOutputStream(connection.getOutputStream()); out.write(params.getBytes(encoding)); out.flush(); out.close();
connection.connect(); Map<String, List<String>> headers = connection.getHeaderFields(); for (String key : headers.keySet()) { System.err.println(key + "--->" + headers.get(key)); } BufferedReader in = null; in = new BufferedReader( new InputStreamReader(connection.getInputStream(), encoding)); String result = ""; String getLine; while ((getLine = in.readLine()) != null) { result += getLine; } in.close(); System.err.println("result:" + result); return result; }
public static String httpGet(String url) { try { HttpClient client = new HttpClient(); GetMethod getMethod = new GetMethod(url); getMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "utf8"); client.executeMethod(getMethod); String result = new String(getMethod.getResponseBodyAsString().getBytes("utf8")); return result; } catch (IOException e) { e.printStackTrace(); } return null; } }
|