美文网首页
springsecurity讲解1

springsecurity讲解1

作者: 念䋛 | 来源:发表于2022-11-08 11:49 被阅读0次

    SpringSecurity是一个权限验证的安装框架,有身份验证(用户名密码)和用户授权(权限)等功能.

    快速上手

    SpringSecurity在整合springboot的时候非常简单,只在maven文件中引入jar即可
    pom.xml

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    

    在日志中有密码,在访问restfull接口的时候,需要输入用户名和密码,
    密码是在日志中提示的,用户名是user

    Using generated security password: afdda172-b982-40aa-9552-18560c8e8ecc
    
    进阶

    快速上手是springsecurity默认的,如何定制化呢
    需要实现WebSecurityConfigurerAdapter,首先实现类要用@Configuration
    和@EnableWebSecurity标注
    重写两个方法configure(HttpSecurity http)配置规则和configure(AuthenticationManagerBuilder auth)身份认证,从内存或者数据库中获取用户信息,configure(WebSecurity web)可以规定忽略哪些路径

    @Configuration
    @EnableWebSecurity
    public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception{
            http
                    .authorizeRequests()
                    .antMatchers("/hello").permitAll() // 对于hello路径放行
                    .anyRequest().authenticated()
                    .and()
                    .formLogin().and()
            ;  //浏览器以form表单形式
    
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception{
            // 用户信息存储在内存中
            auth.inMemoryAuthentication().withUser("user")
                .password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception{
    //忽略/world路径
            web.ignoring().antMatchers("/world");
        }
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            // 官网建议的加密方式,相同的密码,每次加密都不一样,安全性更好一点
            return new BCryptPasswordEncoder();
        }
    

    上面的示例,在内存中生成了用户,使用form表单的方式,放行了 hello和world路径的访问

    filter

    那springsecurity是如何工作的,就是依托于servlet的filter调用链,加上springsecurity的filter,这个filter中又维护了一个filter链,springsecurity终止自己的调用链后,继续执行servlet的filter
    org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter看这个方法就能理解security的filter的大致工作原理,currentPosition为security的filter自己调用链的数量,如果都执行完,就继续执行servlet的filter
    springsecurit主要的filter为
    --UsernamePasswordAuthenticationFilter 校验用户名密码,主要的作用为验证用户名密码,如果正确则退出springsecurity自己的过滤器链.
    --ExceptionTranslationFilter 如果FilterSecurityInterceptor权限不通过,重定向到登入页
    --FilterSecurityInterceptor 校验权限等
    当然不止这三个,所有filter之间的order为100,目的是为了可以将自定义的filter放到容器中并指定顺序,
    一般都是放到UsernamePasswordAuthenticationFilter 前后,加入自己的filter之后,访问都会执行整个调用链,也就都会访问自己添加的filter,所以需要考虑加判断,不满足条件直接跳过,执行下一个filter,获取或者终止调用链,直接跳过当前filter就是执行chain.doFilter(request, response);终止就是不执行
    chain.doFilter(request, response);那调用链就结束了.如果要深入了解可以关注
    org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter
    这里分析UsernamePasswordAuthenticationFilter ,因为我们自定义的filter都是遵循
    UsernamePasswordAuthenticationFilter 的模式,比如想做一个手机验证.
    UsernamePasswordAuthenticationFilter 是如何工作的,这里就不详细分析每一个细节了,大概分析思路,就可以扩展了.
    UsernamePasswordAuthenticationFilter 是AbstractAuthenticationProcessingFilter的子类,在构造方法中初始化了AntPathRequestMatcher


    image.png

    意思是我只对login路径,方法为post进行拦截,自定义自己的filter的时候,就可以模仿这个构造器
    doFilter方法在父类AbstractAuthenticationProcessingFilter中,可以看到调用了
    attemptAuthentication方法,

    authResult = attemptAuthentication(request, response);
    

    UsernamePasswordAuthenticationFilter 的attemptAuthentication方法的返回值

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

    this.getAuthenticationManager()返回的是ProviderManager,那看一下authenticate
    方法中的for循环

    for (AuthenticationProvider provider : getProviders())
    

    for循环里关注两个代码,
    if (!provider.supports(toTest))和result = provider.authenticate(authentication);
    toTest是放进来的Token,这里指的是UsernamePasswordAuthenticationToken,
    如果想要自定义,比如手机验证使用自定义mobileAuthenticationToken,
    还需要提供自己的provider,因为getProviders()返回的是provider集合,那如何知道使用哪个provider,就是通过if (!provider.supports(toTest))
    举例手机验证就能清晰一点
    MobileAuthenticationToken 直接复制UsernamePasswordAuthenticationToken代码,改一下类名就可以,
    MobileAuthenticationProvider继承AuthenticationProvider,实现两个方法
    authenticate(Authentication authentication)和supports(Class<?> authentication)
    这两个方法就对应上面说的for循环中的两行代码
    authenticate(Authentication authentication)方法中,验证手机验证码是否正确,返回MobileAuthenticationToken 中this.authenticated要复制为true,证明已经验证成功了,这个非常重要,
    创建MobileAuthenticationFilter,复制UsernamePasswordAuthenticationFilter就好,简单改一下就可以,构造方法中创建AntPathRequestMatcher,拦截路径/mobile/form,方法POST,那如何将自定义的filter放到springsecurity的调用链呢,最开始说的WebSecurityConfigurerAdapter子类重写的configure(HttpSecurity http)方法中
    另一个要考虑的问题就是,登入之后其他路径是怎么被放行的呢,先讲解通过session的方式


    image.png

    这就是登入成功之后,可以被访问的原因,这是其中一个filter,SecurityContextPersistenceFilter,
    通过sessionid从内存中获取MobileAuthenticationToken ,并放到SecurityContextHolder中,是ThreadLocal类型的变量,该线程就可以使用了,会在springsecurity的最后一个filter(FilterSecurityInterceptor),从SecurityContextHolder获取MobileAuthenticationToken,并判断
    token中的authenticated是否为true,如果是则证明已经认证过,当然还有判断是否有访问该接口的权限,就不展开了.
    说回UsernamePasswordAuthenticationToken,

    流程就是先调用父类的AbstractAuthenticationProcessingFilter的doFilter
    -->authResult = attemptAuthentication(request, response);
    UsernamePasswordAuthenticationFilter#attemptAuthentication
    -->this.getAuthenticationManager().authenticate(authRequest);
    在for循环中调用
    result = provider.authenticate(authentication);
    UsernamePasswordAuthenticationFilter在调用attemptAuthentication方法的时候,使用UsernamePasswordAuthenticationToken,在for循环里调用provider的support方法,判断应该使用DaoAuthenticationProvider,provider.authenticate(authentication)方法其实是父类的authenticate方法,authenticate作用是通过调用DaoAuthenticationProvider#retrieveUser方法,获取用户信息,并将用户密码和前端输入的密码比较,如果成功之后,要返回UsernamePasswordAuthenticationToken,并将authenticated要为true,证明已经验证过了,这个在自定义filter的时候非常重要.
    filter中还有认证成功处理器和认证失败处理器


    image.png

    如果是前后端分离的,就response写入信息就好了.
    刚提到的DaoAuthenticationProvider#retrieveUser方法,获取用户信息,这也是一个扩展点,这种源码用到了成员变量,基本就是可以扩展的地方,流程就是默认给一个值,也可以程序员手动的赋值.

    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    

    这里说的成员变量,就是 this.getUserDetailsService()的userDetailsService
    创建一个类实现UserDetailsService,重写loadUserByUsername方法,该方法从数据库里获取用户信息,并封装为UserDetails返回即可.
    流程就是这样,下篇文章实战,对上面说的能有个更好的理解.

    相关文章

      网友评论

          本文标题:springsecurity讲解1

          本文链接:https://www.haomeiwen.com/subject/vueltdtx.html