最近开发个人博客,shiro配置后,需要进行登录验证,如要输入用户名或密码不正确,需要进行异常拦截并将结果返回给前端,在此过程有一些坑,在此记录一下:
1. 前端登录界面
该界面是用vue开发的,登录的el-form:
<!-- 登录表单区域 -->
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginFormRules"
label-width="0px"
class="login_form"
>
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
prefix-icon="iconfont icon-3702mima"
type="password"
></el-input>
</el-form-item>
<!-- 按钮区域 -->
<el-form-item class="btns">
<el-button type="primary" @click="login">登录</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
</el-form>
涉及到主要的函数:
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm() {
// console.log(this);
this.$refs.loginFormRef.resetFields()
},
login() {
let result
this.$refs.loginFormRef.validate(async (valid) => {
if (!valid) return
// 发送ajax请求登陆
result = await reqPwdLogin(this.loginForm)
console.log(result)
if (result.status !== 200) return this.$message.error('登录失败!')
this.$message.success('登录成功')
// 1. 将登录成功之后的 token,保存到客户端的 sessionStorage 中
// 1.1 项目中出了登录之外的其他API接口,必须在登录之后才能访问
// 1.2 token 只应在当前网站打开期间生效,所以将 token 保存在 sessionStorage 中
window.sessionStorage.setItem('token', result.data.token)
// 2. 通过编程式导航跳转到后台主页,路由地址是 /home
this.$router.push('/home')
})
},
},
对应的axios函数:
//登录操作,注意发达的post请求是json格式
export function reqPwdLogin(loginForm){
return axios({
method: 'post',
url: `/login`,
data: {
username: loginForm.username,
password: loginForm.password,
},
})
}
2.后端访问
2.1 controller层
//登录
@PostMapping("/api/login")
public Object toLogin(@RequestBody JSONObject param) {
//vue传过来的一般是json数据,注意用@RequestBody接收
//取出content
String username = param.getString("username");
String password=param.getString("password");
//从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();
//在认证提交前准备token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//自定义返加的token,登录后自动分配一个线程Id
String token_res = subject.getSession().getId().toString();
Map<String,Object> map=new HashMap<>();
map.put("token",token_res);
//执行认证登陆,抛出的异常在GlobalExceptionHandler中处理
subject.login(token);
if (subject.isAuthenticated()) {
return ApiResult.succ(map);
} else {
token.clear();
return new ResponseEntity<>("登录失败", HttpStatus.NOT_FOUND);
}
}
2.2 CustomRealm中的验证操作
“subject.login(token)”为登录操作,为此需要进入自定义的CustomRealm中进行验证,具体shiro配置之前博文已写,这里不再赘述,CustomRealm验证函数如下:
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=====身份认证======");
//通过令牌拿到userName和userPwd
String userName = (String) authenticationToken.getPrincipal();
String userPwd = new String((char[]) authenticationToken.getCredentials());
if (StringUtils.isEmpty(userName)) {
//定义UnknownAccountException异常,不过这里一般由前端验证,我没有对这个异常拦截
throw new UnknownAccountException("用户名为空");
}
//根据用户名从数据库获取密码,加密方法见自定义的SimpleCredentialsMatcher子类,可以看我shiro配置博客。
//查询数据库,判断该用户是否存在,userService具体定义很简单,这里不说了,可以自己实现
User user = userService.queryByName(userName);
if (user==null) {
//定义AccountException异常
throw new AccountException("用户名不存在");
}else{
String password = user.getPassword();
//SimpleAuthenticationInfo自动调用doCredentialsMatch进行加密匹配,可加断点追踪看加密后的值
return new SimpleAuthenticationInfo(userName, password, getName());
}
}
}
这里我试过抛出自己定义的异常,但是CustomRealm中抛出的异常都属于AuthenticationException,自定义异常也会最终被识别为AuthenticationException,所以我自定义的异常拦截都不起作用,没有办法,只能用AuthenticationException的子类进行定义,AuthenticationException的子类有:
2.3 自定义异常拦截
//======================处理shiro的Exception start======================================================
/**
* 处理 IncorrectCredentialsException
*/
@ExceptionHandler(value = IncorrectCredentialsException.class)
public ResponseEntity<Object> incorrectCredentialsException(IncorrectCredentialsException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>("密码错误", HttpStatus.NETWORK_AUTHENTICATION_REQUIRED);
}
/**
* 处理 AuthenticationException 总异常
*/
@ExceptionHandler(value = AuthenticationException.class)
public ResponseEntity<String> authenticationException(AuthenticationException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>("登录验证失败", HttpStatus.NOT_FOUND);
}
/**
* 处理 AccountException 用户名不存在
*/
@ExceptionHandler(value = AccountException.class)
public ResponseEntity<String> userNameNotFoundException(AccountException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>(e.getMessage(), NOT_FOUND);
}
//======================处理shiro请求的Exception end======================================================
3.前端response拦载
根据异常返回的response定义不同的消息框:
//response拦截器
axios.interceptors.response.use(
//如果返回的status为200左右,则认为不是error
response => {
console.log("=====返回成功=====")
return response.data
},
error => {
//一般返回的status为300以上,则认为是error
let status=error.response.status;
console.log(status)
//对应自定义的IncorrectCredentialsException
//HttpStatus.NETWORK_AUTHENTICATION_REQUIRED对应status为511
if(status == 511){
Message({
type: 'warning',
showClose: true,
message: '密码错误'
})
//对应自定义的AccountException
//HttpStatus.NOT_FOUND对应的status为404
}else if(status==404){
Message({
type: 'warning',
showClose: true,
message: '用户名不存在'
})
}else{
Message({
type: 'warning',
showClose: true,
message: '连接超时'
})
}
return Promise.reject('error')
})
这里要说明一下,我把HttpStatus中大部分的value值都试了一下,一般new ResponseEntity封装的value值,也就是status为201,202,…都会认为是正常的response,如果为300几,400几,500几就会被认为是error。
4.用postman测一下
数据库中的用户值:
注意password都是加密后的值,原密码都是“123456”
先测一下正确的:
注意发送json格式的请求,按上图红框内配置,可以看到操作成功,并返回token。
再试试用户名不存在的:
再试试用户名存在,密码错误的:
成功!vue前端渲染显示我就不测了,只是用 Message组件封装并友好的回显而以。
5. 小结
我之前一直搞不懂ResponseEntity封装的response是如何被vue识别为error或者正常的,原来主要看respones中的status的数值进行判断。后面继续前进,如何对vue中的一些菜单操作进行授权和验证。如果对其他小伙伴有帮助,请记得给我点赞!谢谢