前言
今天带来的其实也没啥,就是简简单单的校验,去校验token,然后就好了,但是区别是啥呢,咱们这边有个大冤种就是这个 GateWay。此外这边的全部代码都是在WhiteHolev0.7里面的,可见的。
由于这个玩意,咱们不好再像以前直接去在拦截器里面去搞事情。而且说实话,请求那么多,如果全部都在GateWay去做的话,我是真的懒得去写那些啥配置了,到时候放行哪些接口都会搞乱。
所以问题背景就是在分布式微服务的场景下,如何去更好地校验token。并且通过我们的token我们可以做到单点登录。
那么这个时候我们就不得不提到我们上篇博文提到的内容了:
当然重点是登录模块。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
- 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 视频教程:https://doc.iocoder.cn/video/
token存储
既然我们要校验,那么我们要做的就是拿到这个token,那么首先要做的就是生成token,然后存储token,咱们上一篇博文已经说的很清楚了,甚至还给出了对应的工具类。我们的流程是这样的:
那么在这里的话,和先前不一样的是,由于咱们的这个其实是一个多端的,所以的话咱们不仅仅有PC端还有移动端(当然移动端的作者也是我这个大冤种)所以token的话也是要做到多端的。那么这样的话,我们就要对上次做一点改动。
这里的话,和上次不一样的地方有两个。
Token 存储实体
这里新建了一个token的实体,用来存储到redis里面。
@Data
@AllArgsConstructor
@NoArgsConstructor
publicclassLoginToken{
//这个是我们的存储Redis里面的Token
privateStringPcLoginToken;
privateStringMobileLoginToken;
privateStringLoginIP;
}
login 业务代码
之后就是我们修改后的代码了。这个也就是和先前做了一点改动,主要是做多端的token嘛。
@Service
publicclassloginServiceImplimplementsLoginService{
@Autowired
UserServiceuserService;
@Autowired
RedisUtilsredisUtils;
//为安全期间这里也做一个20防刷
@Override
publicRLogin(LoginEntityentity){
Stringusername=entity.getUsername();
Stringpassword=entity.getPassword();
password=password.replaceAll("","");
if(redisUtils.hasKey(RedisTransKey.getLoginKey(username))){
returnR.error(BizCodeEnum.OVER_REQUESTS.getCode(),BizCodeEnum.OVER_REQUESTS.getMsg());
}
redisUtils.set(RedisTransKey.setLoginKey(username),1,20);
UserEntityUser=userService.getOne(
newQueryWrapper().eq("username",username)
);
if(User!=null){
if(SecurityUtils.matchesPassword(password,User.getPassword())){
//登录成功,签发token,按照平台类型去签发不同的Token
Stringtoken=JwtTokenUtil.generateToken(User);
//登录成功后,将userid--->token存redis,便于做登录验证
StringipAddr=GetIPAddrUtils.GetIPAddr();
if(entity.getType().equals(LoginType.PcType)){
LoginTokenloginToken=newLoginToken(token,null,ipAddr);
redisUtils.set(RedisTransKey.setTokenKey(User.getUserid()+":"+LoginType.PcType)
,loginToken,7,TimeUnit.DAYS
);
returnObjects.requireNonNull(R.ok(BizCodeEnum.SUCCESSFUL.getMsg())
.put(LoginType.PcLoginToken,token))
.put("userid",User.getUserid());
}elseif(entity.getType().equals(LoginType.MobileType)){
LoginTokenloginToken=newLoginToken(null,token,ipAddr);
redisUtils.set(RedisTransKey.setTokenKey(User.getUserid()+":"+LoginType.MobileType)
,loginToken,7,TimeUnit.DAYS
);
returnObjects.requireNonNull(R.ok(BizCodeEnum.SUCCESSFUL.getMsg())
.put(LoginType.PcLoginToken,token))
.put("userid",User.getUserid());
}else{
returnR.error(BizCodeEnum.NUNKNOW_LGINTYPE.getCode(),BizCodeEnum.NUNKNOW_LGINTYPE.getMsg());
}
}else{
returnR.error(BizCodeEnum.BAD_PUTDATA.getCode(),BizCodeEnum.BAD_PUTDATA.getMsg());
}
}else{
returnR.error(BizCodeEnum.NO_SUCHUSER.getCode(),BizCodeEnum.NO_SUCHUSER.getMsg());
}
}
}
枚举类修改
同样的这里和先前的枚举类有一点不一样,主要是多了一点东西。
publicenumBizCodeEnum{
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
HAS_USERNAME(10002,"已存在该用户"),
OVER_REQUESTS(10003,"访问频次过多"),
OVER_TIME(10004,"操作超时"),
BAD_DOING(10005,"疑似恶意操作"),
BAD_EMAILCODE_VERIFY(10007,"邮箱验证码错误"),
REPARATION_GO(10008,"请重新操作"),
NO_SUCHUSER(10009,"该用户不存在"),
BAD_PUTDATA(10010,"信息提交错误,请重新检查"),
NOT_LOGIN(10011,"用户未登录"),
BAD_LOGIN_PARAMS(10012,"请求异常!触发5次以上账号将保护性封禁"),
NUNKNOW_LGINTYPE(10013,"平台识别异常"),
BAD_TOKEN(10014,"token校验失败"),
SUCCESSFUL(200,"successful");
privateintcode;
privateStringmsg;
BizCodeEnum(intcode,Stringmsg){
this.code=code;
this.msg=msg;
}
publicintgetCode(){
returncode;
}
publicStringgetMsg(){
returnmsg;
}
}
当然同样的,多的东西还有几个异常类,这个其实就是继承了Exception。
/**
*校验用户登录时,参数不对的情况,此时可能是恶意爬虫
**/
publicclassBadLoginParamsExceptionextendsException{
publicBadLoginParamsException(){}
publicBadLoginParamsException(Stringmessage){
super(message);
}
}
publicclassBadLoginTokenExceptionextendsException{
publicBadLoginTokenException(){}
publicBadLoginTokenException(Stringmessage){
super(message);
}
}
publicclassNotLoginExceptionextendsException{
publicNotLoginException(){}
publicNotLoginException(Stringmessage){
super(message);
}
}
其他的倒还是和先前的保持一致。
存储效果
那么到此我们在登录部分完成了对token的存储,但是这个是在服务端,现在这个玩意已经存到了咱们的redis里面:
客户端存储
现在我们服务端已经存储好了,那么接下来就是要在客户端进行存储。这个也好办,我们直接来看到完整的用户登录代码就知道了。
<template>
<div>
<el-form:model="formLogin":rules="rules"ref="ruleForm"label-width="0px">
<el-form-itemprop="username">
<el-inputv-model="formLogin.username"placeholder="账号">
<islot="prepend"class="el-icon-s-custom"/>
el-input>
el-form-item>
<el-form-itemprop="password">
<el-inputtype="password"placeholder="密码"v-model="formLogin.password">
<islot="prepend"class="el-icon-lock"/>
el-input>
el-form-item>
<el-form-itemprop="code">
<el-row:span="24">
<el-col:span="12">
<el-inputv-model="formLogin.code"auto-complete="off"placeholder="请输入验证码"size="">el-input>
el-col>
<el-col:span="12">
<divclass="login-code"@click="refreshCode">
<s-identify:identifyCode="identifyCode">s-identify>
div>
el-col>
el-row>
el-form-item>
<el-form-item>
<divclass="login-btn">
<el-buttontype="primary"@click="submitForm()"style="margin-left:auto;width:35%">登录el-button>
<el-buttontype="primary"@click="goRegister"style="margin-left:27%;width:35%">注册el-button>
div>
el-form-item>
el-form>
div>
template>
<script>
importSIdentifyfrom"../../components/SIdentify/SIdentify";
exportdefault{
name:"loginbyUserName",
components:{SIdentify},
data(){
return{
formLogin:{
username:"",
password:"",
code:""
},
identifyCodes:'1234567890abcdefjhijklinopqrsduvwxyz',//随机串内容
identifyCode:'',
//校验
rules:{
username:
[
{required:true,message:"请输入用户名",trigger:"blur"}
],
password:[
{required:true,message:"请输入密码(区分大小写)",trigger:"blur"}
],
code:[
{required:true,message:"请输入验证码",trigger:"blur"}
]
}
}
},
mounted(){
//初始化验证码
this.identifyCode=''
this.makeCode(this.identifyCodes,4)
},
methods:{
refreshCode(){
this.identifyCode=''
this.makeCode(this.identifyCodes,4)
},
makeCode(o,l){
for(leti=0;i< l; i++) {
this.identifyCode+=this.identifyCodes[this.randomNum(0,this.identifyCodes.length)]
}
},
randomNum(min,max){
returnMath.floor(Math.random()*(max-min)+min)
},
submitForm(){
if(this.formLogin.code.toLowerCase()!==this.identifyCode.toLowerCase()){
this.$message.error('请填写正确验证码')
this.refreshCode()
}
else{
//这边后面做一个提交,服务器验证,通过之后获得token
this.axios({
url:"/user/user/login",
method:'post',
data:{
"username":this.formLogin.username,
"password":this.formLogin.password,
"type":"PcType",
}
}).then((res)=>{
res=res.data
if(res.code===10001){
alert("请将对应信息填写完整!")
}elseif(res.code===0){
alert("登录成功")
localStorage.setExpire("LoginToken",res.PcLoginToken,this.OverTime)
localStorage.setExpire("userid",res.userid,this.OverTime)
this.$router.push({path:'/userinfo',query:{'userid':res.userid}});
}else{
alert(res.msg);
}
})
}
},
goRegister(){
this.$router.push("/register")
}
},
}
script>
<stylescoped>
style>
这里的话,咱们对localStorage
做了一点优化:
这个代码是在main.js直接搞的。
Storage.prototype.setExpire=(key,value,expire)=>{
letobj={
data:value,
time:Date.now(),
expire:expire
};
localStorage.setItem(key,JSON.stringify(obj));
}
//Storage优化
Storage.prototype.getExpire=key=>{
letval=localStorage.getItem(key);
if(!val){
returnval;
}
val=JSON.parse(val);
if(Date.now()-val.time>val.expire){
localStorage.removeItem(key);
returnnull;
}
returnval.data;
}
这个this.OverTime 就是一个全局变量,就是7天过期的意思。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
token验证
前面咱们说完了这个存储,那么现在的话咱们就是验证服务了。首先我们来看到什么地方需要验证。
我们拿这个为例子:
主页的话,都是get请求,没啥技术含量,不过我不介意再水一篇博客~。那么就是咱们这个页面需要。
那么在这里的话我先说一下执行流程,这样的话咱们完整的案例就起来了:
前端提交
那么现在咱们来看看前端的代码: