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"></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这部分流程的理解,理解的还不够,有问题的地方还望大佬提出