业务难点:
1.项目采用全后端分离开发
2.实现单点登录(即一处登录,其他模块也直接登录)
3.清楚JWT+Token的原理
首先登录问题并不是这个业务的难点,重点在于如何实现单点登录呢?
实现单点登录目前所学到的有三种方法:
① session广播机制(session复制到每个服务器)
② 使用cookie+redis实现:
在项目任何一个模块进行登录后,把数据放在redis中,key保存唯一随机值(ip或者用户id等),value保存用户数据,再把redis生成的key值放到cookie中
这样每次访问项目中任意模块,发送请求都会带着cookie到redis中进行查询
③ 使用token实现
在项目任何一个模块进行登录后,按照一定规则生成字符串返回给前端(可以通过cookie也可以通过地址栏)
每次再访问其他模块时,每次访问都带着特定字符串,根据字符串获取用户信息,如果获取到了就是登陆
JWT原理
代码具体实现过程:
为了方便我们对前端传来的数据进行操作,所以先封装一个Vo类
@Data
@ApiModel(value="登录对象", description="登录对象")
public class LoginVo {
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
}
首先编写后端服务器Controller接口
public class UcenterMemberController {
@Autowired
private UcenterMemberService memberService;
//登录
@PostMapping("/login")
public R loginUser(@RequestBody LoginVo loginVo){
String token = memberService.login(loginVo);
return R.ok().data("token",token);
}
//根据token获取用户信息
@GetMapping("/getMemberInfo")
public R getMemberInfo(HttpServletRequest request){
//调用jwt工具类的方法,根据request对象获取头信息,返回用户id
String memberId = JwtUtils.getMemberIdByJwtToken(request);
UcenterMember member = memberService.getById(memberId);
return R.ok().data("userInfo",member);
}
}
接下来调Service层方法:
public String login(LoginVo loginVo) {
//获取登录的手机号和密码
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
//判断手机和密码是否非空
if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
throw new GuliException(20001, "登陆失败");
}
//判断手机号是否正确
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("mobile", mobile);
UcenterMember mobileMember = this.baseMapper.selectOne(wrapper);
if (mobileMember == null) {
throw new GuliException(20001, "登陆失败");
}
//判断密码是否正确
//因为存储到数据库的密码是加密的,需要加密后比较
if (!MD5.encrypt(password).equals(mobileMember.getPassword())) {
throw new GuliException(20001, "登陆失败");
}
//判断用户是否禁用
if (mobileMember.getIsDisabled()) {
throw new GuliException(20001, "登陆失败");
}
//登录成功
String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
return jwtToken;
}
Ps: 判断类似非空这样的过滤检查,可以在前端直接过滤,无需在后端再进行检查
由于我们在将密码存放到数据库,为了保证我们数据的安全性,采用了MD5加密,由于MD5是不可逆的,所以我们将用户传来的密码进行加密后对比,即可检查。
重点是,如果登陆成功,我们需要用JWT生成token返回给前端页面。
接下来就是前端页面进行页面请求以及渲染:
先用Axios封装Ajax请求,这样方便我们对后端接口进行请求:
import request from '@/utils/request'
export default {
//登录
submitLoginUser(userInfo) {
return request({
url: `/eduucenter/member/login`,
method: 'post',
data: userInfo
})
},
//获取用户信息
getLoginUserInfo() {
return request({
url: `/eduucenter/member/getMemberInfo`,
method: 'get'
})
}
}
登录页面完整代码:
<template>
<div class="main">
<div class="title">
<a class="active" href="/login">登录</a>
<span>·</span>
<a href="/register">注册</a>
</div>
<div class="sign-up-container">
<el-form ref="userForm" :model="user">
<el-form-item
class="input-prepend restyle"
prop="mobile"
:rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]"
>
<div>
<el-input type="text" placeholder="手机号" v-model="user.mobile" />
<i class="iconfont icon-phone" />
</div>
</el-form-item>
<el-form-item
class="input-prepend"
prop="password"
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
>
<div>
<el-input type="password" placeholder="密码" v-model="user.password" />
<i class="iconfont icon-password" />
</div>
</el-form-item>
<div class="btn">
<input type="button" class="sign-in-button" value="登录" @click="submitLogin()" />
</div>
</el-form>
<!-- 更多登录方式 -->
<div class="more-sign">
<h6>社交帐号登录</h6>
<ul>
<li>
<a
id="weixin"
class="weixin"
target="_blank"
href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login"
>
<i class="iconfont icon-weixin" />
</a>
</li>
<li>
<a id="qq" class="qq" target="_blank" href="#">
<i class="iconfont icon-qq" />
</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import "~/assets/css/sign.css";
import "~/assets/css/iconfont.css";
import cookie from "js-cookie";
import loginApi from "@/api/login";
export default {
layout: "sign",
data() {
return {
user: {
mobile: "",
password: ""
},
loginInfo: {}
};
},
methods: {
//登录
submitLogin() {
//调用接口返回token
loginApi.submitLoginUser(this.user).then(response => {
//获取token字符串放到cookie中
var token = response.data.data.token;
//第一个参数是cookie中值的名称,第二个参数是值,第三个参数是作用范围(在哪些地址进行传递这个cookie)
cookie.set("guli_token", token, { domain: "localhost" });
//第四步 调用获取用户信息的接口
loginApi.getLoginUserInfo().then(response => {
this.loginInfo = response.data.data.userInfo;
//获取返回的用户信息,放到cookie中
cookie.set("guli_ucenter", JSON.stringify(this.loginInfo), { domain: "localhost" });
//跳转页面
window.location.href = "/";
});
});
},
checkPhone(rule, value, callback) {
//debugger
if (!/^1[34578]\d{9}$/.test(value)) {
return callback(new Error("手机号码格式不正确"));
}
return callback();
}
}
};
</script>
<style>
.el-form-item__error {
z-index: 9999999;
}
</style>
注意:这里我们先调用登录接口,如果成功登陆了再带着返回来的token值去根据token查询用户信息的接口,获得用户信息,再在页面进行返回,之后再返回首页。
其次,我们为了其他页面实现单点登录,我们需要将用户信息也放入到token值,所以,这样我们在别的页面在初次渲染的时候,调用created方法,先尝试获取我们的用户信息,下面以首页代码为例:
export default {
data() {
return {
token: "",
loginInfo: {
id: "",
age: "",
avatar: "",
mobile: "",
nickname: "",
sex: ""
}
};
},
created() {
this.showInfo();
},
methods: {
//创建方法,从cookie中获取用户信息
showInfo() {
//从cookie获取用户信息
var userStr = cookie.get("guli_ucenter");
// 把字符串转换为json对象
if (userStr) {
this.loginInfo = JSON.parse(userStr);
}
}
}
};