在使用security 做登录授权的时候,有一个接口UserDetailsService,通过实现该接口可以重写loadUserByUsername做用户校验,但是有个问题就是,当用户输入的账号不存在,怎么提示信息给前端,貌似只有输入用户密码错误框架才会返回:
{
"error": "invalid_grant",
"error_description": "用户名或密码错误"
}
如果
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 首先判断username是否存在
// 通过传进来的 username 去数据库查找用户信息,这里模拟
// User userInfo = userService.findUserByUserName(username);
if(userInfo == null){
return null; // 返回null 直接报500错误,怎么才能返回"用户名或密码错误"的提示,
}
// String userPasswordDb = userInfo.getPassword();
//Integer userId = userInfo.getId();
// 线上环境应该通过用户名查询数据库获取用户加密后的密码
String userPasswordDb = passwordEncoder.encode("123456");
// 根据用户名查询出来的用户角色权限列表
String permissions = "sys_user_edit,sys_user_del,sys_user_add";
CpmsUser userDetails = new CpmsUser(userId,username,userPasswordDb,AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
return userDetails;
}
###
首先,在不为security加入自定义过滤器的前提下,只能给出:用户名或密码错误
的提示,没有办法细化到:用户名错了
或是 密码错了
的提示。
其次,如果只是想使loadUserByUsername
返回类似用户名或密码错误
的提示,我认同楼上的方法,直接抛出异常即可:
User userInfo = userService.findUserByUserName(username);
if(userInfo == null){
throw new UsernameNotFoundException();
}
此时,前台将得到了一个401错误。表示:用户名或密码错误。
我简单说下为什么返回null会发生500,而抛出UsernameNotFoundException
则会返回401.
在AbstractUserDetailsAuthenticationProvider
中有这么几行核心代码:
try {
// 获取当前登录用户,该方法将调用我们自定义的loadUserByUsername方法。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
if (hideUserNotFoundExceptions) {
// 由于hideUserNotFoundExceptions的默认值为true
// 所以若接收到UsernameNotFoundException则抛出该异常,则抛出BadCredentialsException的异常。
// 这正是会提示401的原因
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
然后另一个核心文件:DaoAuthenticationProvider
中有这么几行核心代码:
try {
// 调用我们的loadUserByUsername方法
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
// 这是关键,如果loadUserByUsername方法返回null,则将抛出InternalAuthenticationServiceException异常
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
// 该异常直接向上抛出InternalAuthenticationServiceException
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
最后,在BasicAuthenticationFilter
又有这么几行代码:
try {
// 省略部分代码
if (authenticationIsRequired(username)) {
// 以下语句完成认证:
// 1. 未发生异常,说明认证成功。继续向下执行
// 2. 发生BadCredentialsException异常,不能够catch接收。异常向上抛出,最终被Sring获取,返回401
// 3. 发生InternalAuthenticationServiceException,该异常继承于AuthenticationException。将被catch接收。
// catch中清空了用户登录信息后直接返回,(猜想,未跑demo)近而在后续的语句执行中发生了500异常。
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
// 未发生异常,用户名密码认证成功
}
}
catch (AuthenticationException failed) {
// 清空登录信息后直接return
return;
}
最后再说一下为什么我们无法分辨出是:用户名不存在或密码错误。
在DaoAuthenticationProvider
有以下几行是验证密码是否正确的:
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
如上所示:如果密码未匹配成功,同样会抛出BadCredentialsException异常。既然用户名不存在与密码错误同样抛出的是BadCredentialsException异常,而Spring Security处理BadCredentialsException的方法又是统一的,所以得到401实际上只能代表接收到了BadCredentialsException异常,而该异常是由用户名错误引起的,或是由密码错误引起的,就无从得知了。
总结:Spring Security是个相对复杂的工程,水平有限,相信也没能讲明白,取其精华去其糟粕吧。
###throw new UsernameNotFoundException();
原因.不抛出springsecurity系列的异常,比如UsernameNotFoundException,BadCredentialsException,CredentialsException等异常。否则会在springsecurity里统一成“认证失败"的异常信息,就无法区分用户不存在,密码错误,还是用户状态不对的情况。
解决办法:
1.我抛的是空指针异常,上层捕获,返回前端用户不存在,然后密码错误是在DaoAuthenticationProvider的自定义子类方法中抛出new BadCredentialsException("密码不正确!")。
2.你当然可以抛出自定义一系列异常,也能达到目的。