Spring Security——基于表单登录认证原理及实现

基于表单的登录认证

一、默认安全验证

当我们项目里添加spring security依赖它就已经起作用了,启动项目访问时,会出现弹出框。spring security默认
采用basic模式认证。浏览器发送http报文请求一个受保护的资源,浏览器会弹出对话框让输入用户名和密码。并以用
户名:密码的形式base64加密,加入Http报文头部的Authorization(默认用户名为user,密码则是会在启动程序时后
台console里输出,每次都不一样)。后台获取Http报文头部相关认证信息,认证成功返回相应内容,失败则继续认证。
下面会详细介绍具体认证流程。
复制代码

二、基于表单的认证原理

基本认证流程:
                     SecurityContextPersistenceFilter
                                   ↓
                   AbstractAuthenticationProcessingFilter
                                   ↓
                    UsernamePasswordAuthenticationFilter
                                   ↓
                         AuthenticationManager
                                   ↓
                         AuthenticationProvider
                                   ↓
                          userDetailsService
                                   ↓
                              userDetails
                                   ↓
                                认证通过
                                   ↓
                            SecurityContext
                                   ↓
                          SecurityContextHolder
                                   ↓
                           RememberMeServices
                                   ↓
                      AuthenticationSuccessHandler
SecurityContextPersistenceFilter会校验请求中session是否有SecurityContext,有放SecurityContextHolder
中,返回时校验SecurityContextHolder中是否有securityContext,有放session,从而实现认证信息在多个请求中共
享。
AbstractAuthenticationProcessingFilter中会调自身attemptAuthentication抽象方法,
UsernamePasswordAuthenticationFilter是其一个实现类。
复制代码

它从请求中获取账号密码后,构造了一个token,此时没有认证,随后会调用AuthenticationManager接口的
authenticate方法
复制代码

下面是其构造函数,我们可看到这个token没有认证
复制代码

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }
复制代码

ProviderManager实现了AuthenticationManager接口,它获取所有provider后遍历调用其supports方法,去匹配哪
个provider支持之前申明的token,找到provider后调用authenticate方法。

到provider才开始认证用户信息,表单登录的是DaoAuthenticationProvider其父类进行以下操作:
①调用userDetailsService接口的loadUserByUsername方法获取UserDetails用户信息
②检查UserDetails用户是否可用、是否账户没有过期、是否账户没有被锁定
③检查UserDetails密码是否正确
④检查UserDetails密码是否没有过期
⑤都通过会重新申明一个token,不过多了权限集合参数如下
复制代码
  
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
    Collection authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
复制代码
此时用户才被认证通过,上述步骤有一个错了会抛出AuthenticationException异常的子类,下面是其继承图:
复制代码

认证结果返回给AbstractAuthenticationProcessingFilter,将结果放到SecurityContextHolder的
SecurityContext中,若添加了记住我功能又会调用rememberMeServices来实现,最后调用
AuthenticationSuccessHandler接口成功处理。
复制代码

三、表单登录实现

在第二部分介绍具体原理,接下来实现自定义登录验证。首先编写一个config类继承WebSecurityConfigurerAdapter
类重写configure方法填写配置
复制代码
   
 protected void configure(HttpSecurity http) throws Exception {
        http
                /**
                 * 表单登录配置
                 */
                .formLogin() //表单登录
                .loginPage("/authentication/login.html") //自定义登录页面
                .loginProcessingUrl("/authentication/form") //与自定义登录页面处理路径一致
//        http.httpBasic()   basic模式弹出框登录
                .successHandler(myAuthenticationSuccessHandler)  //自定义认证成功处理
                .failureHandler(myAuthenticationFailureHandler)  //自定义认证失败处理
                .and()
                /**
                 * 需要认证的请求配置,
                 * 注:最为具体的请求路径放在前面,而最不具体的路径(如anyRequest())放在最后面。
                 * 如果不这样做的话,那不具体的路径配置将会覆盖掉更为具体的路径配置
                 */
                .authorizeRequests()
                //允许这样请求通过,需要将登录所需路径配好,不然会一直重定向
                .antMatchers("/authentication/*").permitAll() 
                .anyRequest().authenticated()//任何请求都需要认证
                .and()
                .csrf().disable(); //关闭csrf防御机制
    }
    复制代码
自定义用户实现UserDetailsService接口重写loadUserByUsername方法
复制代码

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User(username, "123456", true, true,
                true, true, new ArrayList());
    }
复制代码
这里的User是security自己的,写死了密码为123456,正常应该根据用户名从数据库查出用户信息。当然密码也不可
能为明文,这里推荐使用BCryptPasswordEncoder加密,因为其每次加密都会生成随机盐加入字符串中。需要在之前申
明的config类添加即可
复制代码

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
复制代码
    这四个boolean值分别表示是否可用、是否账户没有过期、是否密码没有过期、是否账户没有被锁定。构造函数
最后一个参数为该用户用哪些接口权限,同样也从数据库查,但是这个权限只会在登录时初始化一次,若我登录后修
改权限,则无法同步,后续介绍解决办法。
    接下来就是两个登录的自定义登录处理,成功的实现AuthenticationSuccessHandler接口,失败的则实现
AuthenticationFailureHandler接口
复制代码

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{
        response.setContentType("application/json;UTF-8");
        response.getWriter().println("{\"success\":true,\"result\":\"" + "登录成功" + "\"}");
    }
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;UTF-8");
        response.getWriter().println("{\"error\":true,\"message\":\"登录名或者密码错误\"}");
    }
复制代码
    注意成功和失败最后的方法参数是不同的,一个是成功的认证信息,另一个失败抛出的认证异常json串的key可以
根据自身登录页面ajax处理结果的属性来定,必须保持一致。
    页面代码就不复制了,做一个简单html就行。
复制代码

四、Remember Me

remember me顾名思义就是记住我,在登录成功认证以后服务端会发送cookie给浏览器,同时把用户名、base64加密随
机序列、生成token存入persistent_logins表中。当我下次再访问时,会读取cookie中的token与数据库中的token做
对比,若一直能表示认证通过,会重新生成新的token存入数据库中,序列户不变,再重新生成新的cookie发给浏览器。
下面看具体源码:
复制代码

    在第二部分说了登录认证成功后会调用rememberMeServices的loginSuccess方法,这是最后调用的方法,我们看
到了它构造了一个rememberMeToken,将其存入数据库,并发送cookie。
复制代码

    当下次登录,会请求到RememberMeAuthenticationFilter,它调用rememberMeServices接口的autoLogin方法,
去对比cookie中的token与数据库中的token,又判断了token是否过期,验证通过会根据token中的用户名调用
userDetailsService的loadUserByUsername方法获取用户信息。最后根据用户信息构造
一个RememberMeAuthenticationToken认证成功。
复制代码

四、Remember Me实现

remember me实现较为简单,只需要在申明的config类中添加
复制代码

@Autowired
private DataSource dataSource;

@Autowired
    private UserDetailsService userDetailsService;

@Bean
    public PersistentTokenRepository persistentTokenRepository() {

        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
复制代码
在configure方法里加
复制代码
 
.rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(3600) //有效时间
                .userDetailsService(userDetailsService)
复制代码

五、代码地址

https://github.com/qumaoming/spring-security
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值