项目日记day0224
一、通过用户登录失败的异常信息来给用户进行提示
登陆失败后,我们一般会通过不同的失败信息来提示用户,但目前如果登陆失败,无论是账户名错误或者密码错误都只会提示登陆失败。
SpringScurity默认将登陆失败的异常封装到session对象中。
我们知道,session对象是以键值对的方式存在的,现在我们来探究一下,登陆失败后的session中的全部的键。
我们发现登陆失败后,session对象中存在有一个以SPRING_SECURITY_LAST_EXCEPTION
为key的键值对。
现在我们测试一下,通过不同的登陆失败的方式,SPRING_SECURITY_LAST_EXCEPTION
中会存些什么异常。
经过测试,我们发现:
- 用户名错误:org.springframework.security.authentication.BadCredentialsException: Bad credentials
- 密码错误:org.springframework.security.authentication.BadCredentialsException: Bad credentials
- 用户名密码均为空:org.springframework.security.authentication.BadCredentialsException: Bad credentials
- 用户名为空:org.springframework.security.authentication.BadCredentialsException: Bad credentials
结果发现,登陆失败的各种情况都是出现了BadCredentialsException的异常,但我们明明手动设置了如果用户找不到抛出UsernameNotFoundException异常,这显然不符合我们的预期。
我们通过跟程序,发现在 AbstractUserDetailsAuthenticationProvider 类中将hideUserNotFoundExceptions设置为true,也就是说,SpringScurity默认隐藏UsernameNotFoundException异常。
通过查阅资料发现,我们有两种方式来处理提示信息,第一种就是用户名不存在时,抛出BadCredentialsException异常,在其中存入提示信息,然而我们更喜欢用不同的异常来判断出错,现在我们修改一下,当用户名不存在时,抛出UsernameNotFoundException异常。
SpringScurity默认使用的认证处理提供者为DaoAuthenticationProvider,将UsernameNotFoundException异常给隐藏了,如果想使用UsernameNotFoundException则需要单独处理。
设置方式:
修改auth的认证提供者为自定义的认证提供者对象,在自定义的认证提供者对象中设置hideUserNotFoundExceptions为false,服务层认证处理程序以及密码编码器。
/**
* 设置认证处理者对象
* @return
*/
private AuthenticationProvider authenticationProvider() {
//创建认证处理者对象
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//设置隐藏UserNotFoundExceptions为false(默认为true)
authenticationProvider.setHideUserNotFoundExceptions(false);
//设置认证的服务层处理对象
authenticationProvider.setUserDetailsService(userDetailsService);
//设置密码编码器对象
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
登陆失败测试:
- 用户名错误:org.springframework.security.core.userdetails.UsernameNotFoundException: 账户名不存在
- 密码错误:org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误
- 用户名和密码均为空:org.springframework.security.core.userdetails.UsernameNotFoundException: 账户名不存在
测试成功!
二、设置账户状态并通过异常来提示用户
之前我们在认证的服务层处理方法中,只设置了通过用户名和密码的形式来获取凭证,但我们的数据库中有一个account_states
的字段,表示用户的状态,用户有三种状态“0表示正常,1表示冻结,-1表示离职”
现在我们通过UserDetails实现类对象User的另一种构造方法来实现利用用户状态获取凭证。
另一种构造方法:
/**
* Construct the <code>User</code> with the details required by
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider}.
*
* @param username the username presented to the
* <code>DaoAuthenticationProvider</code>
* @param password the password that should be presented to the
* <code>DaoAuthenticationProvider</code>
* @param enabled set to <code>true</code> if the user is enabled
* @param accountNonExpired set to <code>true</code> if the account has not expired
* @param credentialsNonExpired set to <code>true</code> if the credentials have not
* expired
* @param accountNonLocked set to <code>true</code> if the account is not locked
* @param authorities the authorities that should be granted to the caller if they
* presented the correct username and password and the user is enabled. Not null.
*
* @throws IllegalArgumentException if a <code>null</code> value was passed either as
* a parameter or as an element in the <code>GrantedAuthority</code> collection
*/
public User(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException(
"Cannot pass null or empty values to constructor");
}
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
实现方法:
/**
* 根据用户名获得用户信息
*
* @param username 用户名
* @return 用户信息
* @throws UsernameNotFoundException 用户名不存在异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountDao.queryAccountByAccountName(username);
if (account == null){
throw new UsernameNotFoundException("账户名不存在");
}
//创建一个用于存储用户认证权限的集合
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority("USER"));
/**
* 创建UserDetails实现类对象User,将认证信息传给User对象进行认证
* 参数1:要认证用户的用户名
* 参数2:要认证用户的密码
* 参数3:账户是否可以
* 参数4:账户是否已经过期
* 参数5:凭证是否已经过期
* 参数6:账户是否已经被锁定
* 参数7:要认证用户所拥有的权限集合
*/
User userAuth = new User(
account.getAccount_name(),
account.getAccount_password(),
account.getAccount_status()==0?true:account.getAccount_status()==-1?false:true,
true,true,
account.getAccount_status()==0?true:account.getAccount_status()==1?false:true,
grantedAuthorities
);
return userAuth;
}
测试:
- 冻结账户:org.springframework.security.authentication.LockedException: 用户帐号已被锁定
- 离职账户:org.springframework.security.authentication.DisabledException: 用户已失效
测试成功!
接下来实现通过异常信息来提示用户登陆失败。
首先,先编写一个Result类,来存放登录的结果。
package com.jiazhong.office.commons;
/**
* @ClassName: Result
* @Description: TODO 结果类
* @Author: JiaShiXi
* @Date: 2021/2/24 12:39
* @Version: 1.0
**/
public class Result {
private boolean success;
private String message;
private Object data;
private Result(boolean success, String message, Object data) {
this.success = success;
this.message = message;
this.data = data;
}
public static Result success(){
return new Result(true,null,null);
}
public static Result success(String message){
return new Result(true,message,null);
}
public static Result success(String message,Object data){
return new Result(true,message,data);
}
public static Result fail(){
return new Result(false,null,null);
}
public static Result fail(String message){
return new Result(false,message,null);
}
public static Result fail(String message,Object data){
return new Result(false,message,data);
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
其次,修改登录控制类(LoginHandler),SpringScurity下的所有有关于认证的异常都是继承了AuthenticationException
异常类,所以我们可以统一用此类来接受有关于认证的异常。
package com.jiazhong.office.controller.rbac;
import com.jiazhong.office.commons.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.Enumeration;
/**
* @ClassName: LoginHandler 登录控制器
* @Description: TODO 定义一些登陆后的操作
* @Author: JiaShiXi
* @Date: 2021/2/23 18:37
* @Version: 1.0
**/
@RestController
@RequestMapping("/login")
public class LoginHandler {
/**
* 登陆成功后的操作
* @return
*/
@RequestMapping("/result/loginSuccess")
public Result loginSuccess() {
return Result.success("登陆成功");
}
/**
* 登录失败后的操作
* @return
*/
@RequestMapping("/result/loginFail")
public Result loginFail(HttpSession session){
//获取session中错误异常
AuthenticationException exception = (AuthenticationException) session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
//自定义错误信息
if (exception instanceof UsernameNotFoundException){
return Result.fail("用户名不存在");
}else if (exception instanceof BadCredentialsException){
return Result.fail("用户名或密码错误");
}else if (exception instanceof LockedException){
return Result.fail("用户账号已冻结,请联系公司人事部!");
}else if (exception instanceof DisabledException){
return Result.fail("用户已离职,请联系公司人事部!");
}
return Result.fail("系统升级中,请稍后进行访问...");
}
}
最后,修改前端显示效果。
this.$axios.post("/login",qs.stringify(this.account))
.then(response=>{
loading.close();//关闭加载条
setTimeout(()=>{
let result = response.data;
if(result.success){
this.$swal.fire({
icon: 'success',
title: result.message,
showConfirmButton: false,
timer: 1500
})
}else{
this.$swal.fire({
icon: 'error',
title: result.message,
showConfirmButton: false,
timer: 1500
})
};
},350);
})
.catch(err=>{
console.log(err)
})
测试,显示成功!
三、登陆前后视图跳转的问题
前端编写首页测试的视图:
<template>
<div>
首页
<button @click="test">测试</button>
</div>
</template>
<script>
export default {
data(){
return{
}
},
methods:{
test(){
this.$axios.get("/test")
.then(response=>{
alert(response.data)
})
.catch(err=>{
alert(err)
})
}
}
}
</script>
提示框关闭后,进行首页的跳转:
我们进行测试:
登陆后:正常显示
未登录:显示未获取凭证错误
注:我们为什么可以在没有凭证的情况下访问到上面的这个页面呢?
因为只有访问后端需要认证的资源(有人也叫做"受控资源")才会进行凭证控制,上面这个页面只是前端的静态资源,不会跳转到首页,当我们点击按钮的时候,访问后端的控制器"/test",这才算受控资源。
因为在前端请求返回的都是ajax请求,所以返回的也会是ajax形式的响应,所以这时不会直接跳转到登陆页面。
此时,我们可以尝试将登陆表单设置为一个ajax返回的json数据,通过该json数据进行登录处理。
我们在LoginHandler类中添加一个用户未登陆的Controller,设置http.loginPage("/login/unLogin"),使用户未获得凭证时,访问后端资源让其跳转至"/login/unLogin"进行处理。
/**
* 用户未登录
* @return
*/
@RequestMapping("/unLogin")
public String unLogin(){
return "unLogin";
}
此时要注意的是,用户未获得凭证,默认情况下,是不能访问所有后端资源的,所以我们要将"/login/unLogin"设置为不需要凭证即可访问的资源。
http.antMatchers("/login/result/loginFail","/login/unLogin").permitAll()
但是呢,此处有一个坑:
当我们在configure(HttpSecurity http)方法中加入"super.configure(http);“时,会自动调用”.anyRequest().authenticated()“将所有的后端资源变为需要认证的资源,虽然我们用http.antMatchers().permitAll()
将”/login/unLogin"放行了,但是有时候就会出现一些bug,所以我们最好不要使用父类的configure方法。我们可以这样自己手动设置:
设置好之后,前端进行修改:
测试发现,当点击按钮时,就会跳转到首页了。
这种做法的原理如下:
这样做法的坏处是,每个前端的组件如果想要请求后端的资源的话,都要加上"unlogin"的字符串判断,前端组件非常多,这样加下去,会是一件及其麻烦的事情。