关于spring:万字长文详解Spring-Security基于表单登录的认证模式

6次阅读

共计 8491 个字符,预计需要花费 22 分钟才能阅读完成。

本文思维导图

图 1 思维导图

原理探讨

当咱们在我的项目中引入 Spring Security 的相干依赖后,默认的就是表单登录模式;俗话说:“听人劝,吃饱饭”,既然 Spring Security 曾经给咱们安顿的明明白白了,咱们就从表单登录开始吧。

在开始之前,咱们能够站在 Spring Security 的角度上思考:如果我本人来实现表单登录的性能,那么我须要做哪些工作呢?

就我集体而言,我可能会思考以下几点:

  • 配置用户信息,存储如账号、明码等;明码不能以明文传输,须要加密性能
  • 执行校验
  • 认证胜利或者失败的解决计划

能够简略的制作成如下流程图:

图 1 -1 表单登录简略流程图

上方属于咱们本人构想的实现计划,属于 ” 低配版 ” 模式,上面咱们来看看 Spring Security 是怎么做的。Spring Security 的思路和咱们大同小异,长处在于其提供了很好的封装,进步了框架自身的可扩展性。

Spring Security 的实现步骤如下:

  1. UsernamePasswordAuthenticationFilter 拦截器拦挡前端传递的表单登录申请,将登录信息(username、password)封装成 UsernamePasswordAuthenticationToken,传递给 AuthenticationManager 认证管理器
  2. AuthenticationManager 认证管理器依据 Token 的类型遍历获取对应的 Provider,也即是 DaoAuthenticationProvider,执行认证流程
  3. DaoAuthenticationProvider 依附 PasswordEncoder 和 UserDetailsService 对登录申请进行验证
  4. 验证通过,由 AuthenticationSuccessHandler 认证胜利处理器进行解决
  5. 验证失败,由 AuthenticationFailureHandler 认证失败处理器进行解决

制作成流程图如示:

图 1 -2 Spring Security 表单登录认证流程图

这时你可能会一脸懵逼:这咋和刚刚咱们本人构想的齐全不一样呀~ 又是 Manager 又是 Provider 的;莫慌,且听我缓缓道来。

下面呈现了很多新的概念,咱们目前不须要非常粗疏的理解它们是怎么发挥作用的,只须要大略晓得它们有什么用的即可;具体的介绍会在下篇《认证(二):表单登录认证流程源码解析》娓娓道来。

  • UsernamePasswordAuthenticationFilter 表单登录拦截器,用以捕捉前端传递的登录信息(username、password),并将登录信息封装成某些 Token。
  • AuthenticationManager 认证管理器,可简略的了解为调配工作的领导。DaoAuthenticationProvider DAO 认证处理器,相当于被安顿干活的童鞋;从名字 DAO 也能够简略的揣测出:它与数据库中的用户信息密不可分。
  • PasswordEncoder 明码加密器,明码不能明文传输,须要加密。UserDetailsService 用户信息 Service 层,这个也很好了解,前端传递的登录信息必定是有对应的数据库实体存储。
  • AuthenticationSuccessHandler 认证胜利处理器 AuthenticationFailureHandler 认证失败处理器。

通过上述的原理探讨,咱们大体上能弄懂了整个表单登录有哪几个模块须要解决;可简略的总结为 3 个模块:

  1. 登录前置解决:用户信息的封装、明码加密器的设置
  2. 登录中解决:登录的校验
  3. 登录后置解决:登录失败、登录胜利的解决计划

小试牛刀

俗话说:“光说不练假把式”,那么就让咱们来实战一番吧。

登录前置解决

作为一个 Java Web 我的项目,第一步当然是引入相干依赖;间接引入 Spring Boot 封装好的 starter 即可。

Step-1 配置用户信息

Spring Security 提供了 UserDetails 接口,用于获取用户的根本信息(账号密码、权限汇合、是否锁定等等),咱们只须要依据本身的业务场景,实现该接口即可。

Spring Security 提供的 UserDetails.class 接口

自定义业务相干的用户信息类,业务定义的 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<UserInfo> 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

如果我的项目有本人的加解密形式,只须要实现该接口即可,如果没有能够尝试应用 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();
    }
}

整个配置就根本实现了,也比较简单易懂;对一些配置进行根底的解说

  1. .antMatchers(“/resources/“, “/css/“, “/about”, “/test”).permitAll()是指对于这些正则门路进行放行
  2. loginPage(“/login.html”)指的是自定义了一个前端的登录页面,当然也能够应用默认的页面(只是绝对比拟简陋了些)
  3. 最初的 csrf 记得敞开,这一块前面会专门介绍。

总结

以上内容则为本文的全内容,文章通过原理探讨、入手尝试逐个开展。如有谬误之处,请多多斧正

正文完
 0