spring security各种认证配置及异常处理

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.前后端分离认证成败的处理

image-20220705162442737

(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();
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值