springboot 集成 spring security + jsp 登录失败返回异常消息到前台的实现方式以及源码的分析

一. 运行环境
springboot+mybatis+mysql+security+jsp

 

二. 前台登录页面代码

<form class="form form-horizontal" id="loginForm" action="login" method="post">
  <div class="row cl">
    <label class="form-label col-md-7 ">用户登录</label>
  </div>
  <div class="row cl">
    <label class="form-label col-md-2"><i class="Hui-iconfont">&#xe60d;</i></label>
    <div class="formControls col-md-9">
      <input id="username" name="username" type="text" placeholder="请输入用户名" class="input-text size-L" style="width: 350px;">
        <c:if test="${not empty param.error}">
          <span style="color: red">用户名或密码错误,请重新输入</span>
        </c:if>
        <c:if test="${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message!=null}">
          <span style="color: red">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}</span>
        </c:if>
    </div>
  </div>
.....代码省略.....

 

三. 后台代码配置
1.spring security配置文件configuration,继承WebSecurityConfigurerAdapter,重写

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 创建PasswordEncoder密码解析对象
* @return
*/
@Bean
public PasswordEncoder getPasswordEncoder(){
  return new BCryptPasswordEncoder();
}


@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf防护
http.csrf().disable();
//防止iframe
http.headers().frameOptions().disable();
//表单认证
http.formLogin()
  .loginPage("/index.jsp")
  .successForwardUrl("/menu/main")
  .loginProcessingUrl("/login")
  .failureUrl("/index?error=true");

//退出登录
http.logout().logoutSuccessUrl("/index")
  .logoutUrl("/logout");
}
}

 

2. 实现UserDetailsService接口,实现loadUserByUsername方法,也可以继承WebSecurityConfigurerAdapter类,因为WebSecurityConfigurerAdapter也实现了loadUserByUsername方法

public class UserServiceImpl implements UserService, UserDetailsService {
//注入userMapper对象
@Autowired
private UserMapper userMapper;


/**
* 自定义逻辑,用户效验
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  //通过用户名从数据库查询用户信息
  User user = userMapper.selectUserByUsername(username);
  if (user==null){
    //抛出异常信息
    throw new BadCredentialsException("用户名不正确");
  }

  //校验用户信息
  UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities("ROLE_admin").build();
    return userDetails;
  }
}

 

四. 源码分析

其核心就是一组过滤器链 SpringSecurityFilterChain ,项目启动后将会自动配置。最核心的就是 BasicAuthenticationFilter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式

 

 

 

 

UsernamePasswordAuthenticationFilter:基于用户表单登录的验证

BasicAuthenticationFilter:基于HttpBasic的验证

ExceptionTranslationFilter: 认证出现异常处理的过滤器

FilterSecurityInterceptor:总的拦截器

 

认证流程

 

 

 

 

1.用户登录提交用户名,密码,过滤器 AbstractAuthenticationProcessingFilter.doFilter() 调用 UsernamePasswordAuthenticationFilter 中的 attempAuthentication 方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
.....此处代码省略.....
try {   authResult =attemptAuthentication(request, response);

  .....此处代码省略.....

catch (AuthenticationException failed) {
// Authentication failed 
unsuccessfulAuthentication(request, response, failed);
  return;
}
.....此处代码省略.....
successfulAuthentication(request, response, chain, authResult);
}

 

2. 通过调用 UsernamePasswordAuthenticationFilter 中的 attempAuthentication 方法

public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
.....此处代码省略.....
// Allow subclasses to set the "details" property setDetails(request, authRequest); 

  return this.getAuthenticationManager().authenticate(authRequest);
}


3.通过 authenticate方法( authenticate 方法为 AuthenticationManager (认证管理器)接口中的方法)委托认证, DaoAuthenticationProvider 类中 retreveUser 方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  
this.prepareTimingAttackProtection();   try {     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);   } }

 

4.,通过调用 loadUserByUsername(username) 方法来获取返回 UserDetails ,我们可以通过实现实现 UserDetailsService 接口,实现 loadUserByUsername 方法,也可以继承 WebSecurityConfigurerAdapter 类,因为 WebSecurityConfigurerAdapter 也实现了 loadUserByUsername 方法来自定义用户认证

public class UserServiceImpl implements UserService, UserDetailsService {
//注入userMapper对象
@Autowired
private UserMapper userMapper;
/**
* 自定义逻辑,用户效验
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过用户名从数据库查询用户信息
User user = userMapper.selectUserByUsername(username);
if (user==null){
  //抛出异常信息
  throw new BadCredentialsException("用户名不正确");
}
//校验用户信息
UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities("ROLE_admin").build();
return userDetails;
}
}

 

5. BadCredentialsException 异常抛出后,会将异常消息保存为 message

public BadCredentialsException(String msg) {
  super(msg);
}

 

6.异常抛出后,异常会往上抛出,直到在过滤器 AbstractAuthenticationProcessingFilter.doFilter() 方法中处理该异常,调用 unsuccessfulAuthentication 方法处理异常

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {

  .........此处省略代码..........
  }
  catch (InternalAuthenticationServiceException failed) {
    logger.error("An internal error occurred while trying to authenticate the user.",failed);
    unsuccessfulAuthentication(request, response, failed);
    return;
  }
  .........此处省略代码..........

}

 

7.然后继续处理异常,通过调用 onAuthenticationFailure 方法继续处理

protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException {
  SecurityContextHolder.clearContext();

  .........此处省略代码..........

  failureHandler.onAuthenticationFailure(request, response, failed);
}

 

8.SimpleUrlAuthenticationFailureHandler 类中的 onAuthenticationFailure 方法通过调用 saveException 方法保存异常信息

public void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {

  .........此处省略代码..........

  else {
    saveException(request, exception);

    .........此处省略代码..........
  }
}

 

9.SimpleUrlAuthenticationFailureHandler 类中 WebAttributes.AUTHENTICATION_EXCEPTION

protected final void saveException(HttpServletRequest request,AuthenticationException exception) {
  if (forwardToDestination) {
    request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
  }
  .........此处省略代码..........
}

 

10.WebAttributes 类中 AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION"

public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";

 

11. 所以前台页面可以通过 ${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message} 获取到自定义返回的异常消息,以下提供了两种方法来在前台实现登录失败的消息显示

注意:如果用param.error判断则需要在failureUrl("/login?error=true")进行设置
<c:if test="${not empty param.error}"> <span style="color: red">用户名或密码错误,请重新输入</span> </c:if> <c:if test="${not empty sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}"> <span style="color: red">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}</span> </c:if>

 

以上就是我对spring security这部分流程的理解,理解的还不够,有问题的地方还望大佬提出

posted on 2020-04-18 11:28  arsn  阅读(2266)  评论(0编辑  收藏  举报

导航