详解 Spring Security 基于表单登录的认证模式

本文思维导图

图1 思维导图
原理探讨
当我们在项目中引入 Spring Security 的相关依赖后,默认的就是表单登录形式;俗话说:“听人劝,吃饱饭”,既然 Spring Security 已经给我们安排的明明白白了,我们就从表单登录开始吧。
在开始之前,我们可以站在 Spring Security 的角度上思考:如果我自己来实现表单登录的功能,那么我需要做哪些工作呢?
就我个人而言,我可能会考虑以下几点:
配置用户信息,存储如账号、密码等;密码不能以明文传输,需要加密功能
执行校验
认证成功或者失败的处理方案
可以简单的制作成如下流程图:

图1-1 表单登录简单流程图
上方属于我们自己设想的实现方案,属于"低配版"模式,下面我们来看看 Spring Security 是怎么做的。Spring Security的思路和我们大同小异,优点在于其提供了很好的封装,提高了框架本身的可扩展性。
Spring Security 的实现步骤如下:
UsernamePasswordAuthenticationFilter拦截器拦截前端传递的表单登录请求,将登录信息(username、password)封装成 UsernamePasswordAuthenticationToken,传递给 AuthenticationManager认证管理器
AuthenticationManager认证管理器根据Token的类型遍历获取对应的Provider,也即是 DaoAuthenticationProvider,执行认证流程
DaoAuthenticationProvider 依靠 PasswordEncoder 和 UserDetailsService对登录请求进行验证
验证通过,由AuthenticationSuccessHandler 认证成功处理器进行处理
验证失败,由AuthenticationFailureHandler 认证失败处理器进行处理
制作成流程图如示:

图1-2 Spring Security表单登录认证流程图
这时你可能会一脸懵逼:这咋和刚刚我们自己设想的完全不一样呀~ 又是Manager又是Provider的;莫慌,且听我慢慢道来。
上面出现了很多新的概念,我们目前不需要十分细致的了解它们是怎么发挥作用的,只需要大概知道它们有什么用的即可;具体的介绍会在下篇《认证(二):表单登录认证流程源码解析》娓娓道来。
UsernamePasswordAuthenticationFilter 表单登录拦截器,用以捕获前端传递的登录信息(username、password),并将登录信息封装成某些Token。
AuthenticationManager 认证管理器,可简单的理解为分配工作的领导。DaoAuthenticationProvider DAO认证处理器,相当于被安排干活的童鞋;从名字DAO也可以简单的推测出:它与数据库中的用户信息密不可分。
PasswordEncoder 密码加密器,密码不能明文传输,需要加密。UserDetailsService 用户信息Service层,这个也很好理解,前端传递的登录信息肯定是有对应的数据库实体存储。
AuthenticationSuccessHandler 认证成功处理器 AuthenticationFailureHandler 认证失败处理器。
经过上述的原理探讨,我们大体上能弄懂了整个表单登录有哪几个模块需要处理;可简单的总结为3个模块:
登录前置处理:用户信息的封装、密码加密器的设置
登录中处理:登录的校验
登录后置处理:登录失败、登录成功的处理方案
小试牛刀
俗话说:“光说不练假把式”,那么就让我们来实战一番吧。
登录前置处理
作为一个Java Web项目,第一步当然是引入相关依赖;直接引入Spring Boot封装好的starter即可。

org.springframework.boot
spring-boot-starter-security

Step-1 配置用户信息
Spring Security 提供了UserDetails接口,用于获取用户的基本信息(账号密码、权限集合、是否锁定等等),我们只需要根据自身的业务场景,实现该接口即可。
Spring Security提供的UserDetails.class接口
public interface UserDetails extends Serializable {

/**
 * 用户权限集,默认需要添加ROLE_作为前缀
 */
Collection<? extends GrantedAuthority> getAuthorities();

/**
 * 获取用户密码
 */
String getPassword();

/**
 * UserDetails的接口
 * 获取用户名
 */
String getUsername();

 /**
 * 账户是否未过期  --true则为未过期
 */
boolean isAccountNonExpired();

/**
 * 账户是否未被锁定
 */
boolean isAccountNonLocked();

/**
 * 账户凭证是否未过期
 */
boolean isCredentialsNonExpired();

/**
 * 账户是否可用
 */
boolean isEnabled();

}
自定义业务相关的用户信息类,业务定义的UserInfo.class必须带有username和password相关的信息,用于做用户验证;项目根据自身需求来判断是否需要使用下面的几个boolean方法,如果无相关需求则直接返回true即可。
@Setter
public class UserInfo implements UserDetails {

private String username;

private String password;

/**
 * UserDetails的接口
 * 用户权限集,默认需要添加ROLE_作为前缀
 */
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(1);
    simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
    return simpleGrantedAuthorities;
}

/**
 * 获取用户密码
 */
@Override
public String getPassword() {
    return this.password;
}

/**
 * 获取用户名
 */
@Override
public String getUsername() {
    return this.username;
}

/**
 * 账户是否未过期  --true则为未过期
 */
@Override
public boolean isAccountNonExpired() {
    return true;
}

/**
 * 账户是否未被锁定
 */
@Override
public boolean isAccountNonLocked() {
    return true;
}

/**
 * 账户凭证是否未过期
 */
@Override
public boolean isCredentialsNonExpired() {
    return true;
}

/**
 * 账户是否可用
 */
@Override
public boolean isEnabled() {
    return true;
}

}
在定义完用户实体UserInfo后,我们同时也需要提供对应的Service层的API方法,用以进行一些基本的操作,诸如:新增用户、删除用户等。
Spring Security 也提供了对应的Service层接口,UserDetailsService,接口只有一个方法:UserDetails loadUserByUsername(String username);根据用户名加载用户信息.
UserDetailsService.class
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
因此我们可以自定义业务相关的UserInfoServiceImpl类,实现Spring Security提供的 UserDetailsService接口
UserInfoServiceImpl.class
/**

  • 用户信息service模块

  • UserDetailsService接口为SpringSecurity内置接口,内部有方法:

  • UserDetails loadUserByUsername(String username):如名所得 根据用户名加载用户

  • 该方法主要是在:DaoAuthenticationProvider中被调用,获取用户的信息

  • @author 小奇
    */
    @Slf4j
    @Service
    public class UserInfoServiceImpl implements UserDetailsService, UserInfoService {

    private final UserInfoDAO userInfoDAO;

    @Autowired
    public UserInfoServiceImpl(UserInfoDAO userInfoDAO) {
    this.userInfoDAO = userInfoDAO;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional userInfoOpt = Optional.ofNullable(userInfoDAO.loadUserByUsername(username));
    UserInfo user = userInfoOpt.orElseThrow(() -> new UsernameNotFoundException(“can’t not load user by username”));
    log.info(“根据用户名:{}查询用户成功”, user.getUsername());
    return user;
    }
    }
    Step-2 配置密码加密器
    众所周知,密码是不能以明文的方式存储的,贴心的Spring Security自然不会忘记提供加密的功能。PasswordEncoder接口,主要提供2个方法;String encode(CharSequence rawPassword)方法用于加密,由我们在注册用户的时候调用;boolean matches(CharSequence rawPassword, String encodedPassword) 方法用于匹配,登录验证时由Spring Security框架调用。
    PasswordEncoder.class
    public interface PasswordEncoder {

    /**

    • 加密,注册用户或者修改密码的时候调用
      */
      String encode(CharSequence rawPassword);

    /**

    • 匹配,登录验证时,security框架调用
      */
      boolean matches(CharSequence rawPassword, String encodedPassword);

    default boolean upgradeEncoding(String encodedPassword) {
    return false;
    }
    }
    如果项目有自己的加解密方式,只需要实现该接口即可,如果没有可以尝试使用Spring提供的BCryptPasswordEncoder密码加密器。
    登录中处理
    在这一块上,我们可以自定义与自身业务有关的登录逻辑判断,目前没有这种需求就使用Spring Security提供的默认实现即可。
    登录后置处理
    登录的后置处理分两种情况,第一种是登录成功的处理,一种是登录失败的处理。
    Step-03 配置登录成功处理器
    Spring Security提供了认证成功处理器接口AuthenticationSuccessHandler,当我们有一些自定义的业务逻辑,诸如:用户登录成功后赠送积分,或者登录成功后自动跳转……就可以通过提供该接口的自定义实现。
    AuthenticationSuccessHandler.class
    public interface AuthenticationSuccessHandler {

    /**

    • 默认方法
      */
      default void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authentication)
      throws IOException, ServletException{
      onAuthenticationSuccess(request, response, authentication);
      chain.doFilter(request, response);
      }

    /**

    • 成功后会被调用
      /
      void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response, Authentication authentication)
      throws IOException, ServletException;
      }
      自定义成功处理器 WebAuthenticationSuccessHandler.class
      /
      *
  • 自定义验证成功处理器

  • @author 小奇
    */
    @Slf4j
    @Component
    public class WebAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
    throws IOException, ServletException {
    log.info(“登录成功~~”);
    // 返回json 可添加自身业务逻辑 如:登录成功后添加用户积分等……
    response.setContentType(“application/json;charset=UTF-8”);
    response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
    }
    Step-04 配置登录失败处理器
    AuthenticationFailureHandler失败处理器和成功处理器类似,不做过多的解析,上代码。
    public interface AuthenticationFailureHandler {

    /**

    • 失败后调用
      /
      void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception)
      throws IOException, ServletException;
      }
      自定义失败处理器WebAuthenticationFailureHandler.class
      /
      *
  • 自定义验证失败处理器

  • @author 小奇
    */
    @Slf4j
    @Component
    public class WebAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
    throws IOException, ServletException {
    log.error(“登录失败”);
    // 把exception返回给前台
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    response.setContentType(“application/json;charset=UTF-8”);
    response.getWriter().write(objectMapper.writeValueAsString(exception));

     // 可做其他业务逻辑,诸如限制每天登录失败的次数
    

    }
    }
    Step-05 配置SecurityConfig
    还记得之前我们提过的Spring Security为人广为诟病的繁琐配置吗?自从搭上Spring Boot的列车之后,有了翻天覆地的改变。
    下面就来简单配置一下我们在上面自定义的一些模块吧。
    /**

  • @author kylin
    */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private WebAuthenticationSuccessHandler successHandler;

    @Autowired
    private WebAuthenticationFailureHandler failureHandler;

    /**

    • 密码加密器,使用spring提供的BCryptPasswordEncoder
      */
      @Bean
      public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
      }

    /**

    • http请求安全配置
    • @param http
      */
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http
      .authorizeRequests()
      .antMatchers("/resources/", "/css/", “/about”, “/test”).permitAll()
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .loginPage("/login.html")
      .successHandler(successHandler)
      .failureHandler(failureHandler)
      .permitAll()
      .and()
      .csrf().disable();
      }

}
整个配置就基本完成了,也比较简单易懂;对一些配置进行基础的讲解
.antMatchers("/resources/", "/css/", “/about”, “/test”).permitAll()是指对于这些正则路径进行放行
loginPage("/login.html")指的是自定义了一个前端的登录页面,当然也可以使用默认的页面(只是相对比较简陋了些)
最后的csrf记得关闭,这一块后面会专门介绍。
总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值