1.SpringSecurity动态认证实现
先分析DaoAuthenticationProvider认证管理器的方法retrieveUser()
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//根据当前登录用户名查询用户对象--
//此时执行的是InMemoryUserDetailsManager内存中的用户
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}其中.getUserDetailsService() -->是一个接口
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}我们得出结论,如果我们自己定义类实现UserDetailsService接口,就可执行我们自己的认证处理了
编写UserServiceImpl实现UserDetailsService接口
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户名查询用户UserDetails
* @param username
* @return
* @throws UsernameNotFoundException
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//访问数据库,根据用户查询用户对象
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
if(user==null){
throw new UsernameNotFoundException("用户名不存在!");
}
//封装用户的权限集
List<GrantedAuthority> authorities = new ArrayList<>();
//封装数据库存询的用户信息
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
user.getUsername(),user.getPassword(),authorities);
return userDetails;
}
}添加配置类相关代码
/**
* Spring Securtiy配置类
*/
@Configuration //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserServiceImpl userServiceImpl;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
}
}2.异常处理
(1) 常见异常:
- `UsernameNotFoundException`(用户不存在)
- `DisabledException`(用户已被禁用)
- `BadCredentialsException`(坏的凭据)
- `LockedException`(账户锁定)
- `AccountExpiredException` (账户过期)
- `CredentialsExpiredException`(证书过期)
-...AuthenticationException lastException --> 以上异常类都是作为AuthenticationException子类
(2) security底层源码分析
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
this.logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
} else {
//保存异常 SPRING_SECURITY_LAST_EXCEPTION --异常对象
this.saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
} else {
this.logger.debug("Redirecting to " + this.defaultFailureUrl);
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}至此 Spring security 完成了异常处理,总结一下流程:
–> AbstractAuthenticationProcessingFilter`.doFilter()`
–> AbstractAuthenticationProcessingFilter.`unsuccessfulAuthentication()`
–> SimpleUrlAuthenticationFailureHandler.`onAuthenticationFailure()`
–> SimpleUrlAuthenticationFailureHandler.`saveException()`(3) 实现异常处理
step1: 在配置类中配置异常的处理的url
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启自定义表单模式登录认证
//需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
.defaultSuccessUrl("/home")
//登录认证失败后请求URL ,要放行
.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
}注意:此处不能指定/error ,因为这个 /error是security内置的请求处理
step2: 编写erro1处理器
@GetMapping("/error1")
public String error(HttpServletRequest request, HttpServletResponse response){
return "error";
}step3: 创建error.html模板页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
出错了: <p th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></p>
</body>
</html>(4) 用户名找不到异常处理
由于spring security框架底层默认将UsernameNotFoundException设置为隐藏,而显示的是BadCredential异常,可以通过下面的方式配置实现
/**
* Spring Securtiy配置类
*/
@Configuration //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserServiceImpl userServiceImpl;
//重新定制DaoAuthenticationProvider
@Bean
public DaoAuthenticationProvider getDaoAuthenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置用户未找到异常不隐藏
provider.setHideUserNotFoundExceptions(false);
//设置认证管理器使用UserDetaisService对象--此处使用的是我们自定义的业务实现类
provider.setUserDetailsService(userServiceImpl);
//设置认证管理器使用的密码检验器对象
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//指定认证管理器
auth.authenticationProvider(getDaoAuthenticationProvider());
}
...
}3.前后端分离认证成败的处理

(1) 认证成功的处理
登录成功之后我们是跳转到/home控制器的,也就是跳转到home.html
但是在前后端分离的情况下,页面的跳转是交给前端去控制的,后端的控制器就不生效了,那我们应该如何实现让前端去跳转页面呢?
我们发现在认证成功后,执行的是AuthenticationSuccessHandler接口实现类:
默认为SimpleUrlAuthenticationSuccessHandler,如果定制自定义处理器,只需要实现该接口
/**
* 自定义认证成功的处理器Handler
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ResponseResult<Void> result = ResponseResult.ok();
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}然后配置成功的处理器:
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启自定义表单模式登录认证
//需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
http.authorizeRequests().antMatchers("/login", "/login.html")
.permitAll().anyRequest().authenticated()
.and().
// 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
formLogin().loginPage("/login.html").loginProcessingUrl("/login")
//登录表单form中密码输入框input的name名,不修改的话默认是password
.usernameParameter("username").passwordParameter("password")
//登录认证成功后默认转跳的路径
//.defaultSuccessUrl("/home")
// 前后端分离认证成功的处理器 -输出json
.successHandler(myAuthenticationSuccessHandler)
.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
}(2) 认证失败的处理
同样的,有登录成功的处理器就有登录失败的处理器,但是登录失败的情况比较多,所以需要经过很多的判断,登录失败处理器主要用来对登录失败的场景(密码错误、账号锁定等…)做统一处理并返回给前台统一的json返回体
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException e)
throws IOException, ServletException {
//定义响应的结果对象
ResponseResult<String> result = null;
if(e instanceof UsernameNotFoundException){
result = ResponseResult.error(ResultCode.USER_ACCOUNT_NOT_EXIST);
}else if (e instanceof AccountExpiredException) {
//账号过期
result = ResponseResult.error(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//凭证不对 错误
result = ResponseResult.error(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = ResponseResult.error(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = ResponseResult.error(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = ResponseResult.error(ResultCode.USER_ACCOUNT_LOCKED);
} else{
result = ResponseResult.error(ResultCode.COMMON_FAIL);
}
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}添加配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
....
.successHandler(myAuthenticationSuccessHandler)
// 前后端分离认证失败的处理器 -输出json
.failureHandler(myAuthenticationFailureHandler);
//关闭CSRF跨域攻击防御
http.csrf().disable();
}4.前后端分离用户未登录的处理
而在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽重定向的登录页面,并返回统一的json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)
//自定义用户未登录的处理器
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
//重新定义未登录的处理
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
ResponseResult<Void> result = ResponseResult.error(ResultCode.USER_NOT_LOGIN);
response.setContentType("application/json;charset=utf-8");
PrintWriter out= response.getWriter();
out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
out.flush();
out.close();
}
}配置用户未登录的处理
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// 前后端分离认证成功的处理器 -输出json
.successHandler(myAuthenticationSuccessHandler)
// 前后端分离认证失败的处理器 -输出json
.failureHandler(myAuthenticationFailureHandler)
.and()
// 前后端分离处理未登录请求
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint);
//关闭CSRF跨域攻击防御
http.csrf().disable();
}
3729

被折叠的 条评论
为什么被折叠?



