美文网首页
springSecurity原理简析+基于springboot基

springSecurity原理简析+基于springboot基

作者: 灿烂的GL | 来源:发表于2021-04-19 17:33 被阅读0次

    一、springSecurity

    1、springSecurity原理概述

    springSecurity原理:简单理解为过滤器链支持不同的身份认证方式(主要比如:UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter...ExceptionTranslationFilter--捕获异常引导用户登录成功、FilterSecurityInterceptor),请求和响应都会经过过滤器然后才能到我们最终的服务,其中访问顺序绿色的可选。


    image.png

    2、原理概述对应代码逻辑
    filter的产生是通过WebSecurityConfigurerAdapter,就是自己写的配置类继承的类(我这里是SecurityConfig),WebSecurityConfigurerAdapter里有一个初始化方法,放了一个HttpSecurity ,保证用户可以通过表单或者 http 的方式进行认证

        public void init(final WebSecurity web) throws Exception {
            final HttpSecurity http = this.getHttp();
            web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
                public void run() {
                    FilterSecurityInterceptor securityInterceptor = (FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
                    web.securityInterceptor(securityInterceptor);
                }
            });
        }
    

    filter注入入口是@EnableWebSecurity,@EnableWebSecurity中import了WebSecurityConfiguration,而WebSecurityConfiguration中的springSecurityFilterChain放入filter,spring会通过DelegatingFilterProxy获取filter

        public Filter springSecurityFilterChain() throws Exception {
            boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
            if (!hasConfigurers) {
                WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
                });
               //一个add方法
                this.webSecurity.apply(adapter);
            }
    
            return (Filter)this.webSecurity.build();
        }
    

    二、认证和授权

    1、认证处理逻辑

    AuthenticationManager 将收集到的用户信息,循环遍历AuthenticationProvider找到适合(support方法)的登录方式(比如用户密码登录、第三方登录等---图中示例的是用户密码登录的过滤器),AuthenticationProvider做相应的校验处理,其中包含调用userDetails数据库中用户信息进行校验,拿到数据库用户信息与登录信息做预检查(preAuthenticationChecks),检查userDetails中的三个方法(账户是否锁定、过期等),然后进行附加检查(additionalAuthenticationChecks),包含passwordEncoder校验当前密码是否匹配,最后进行后检查(postAuthenticationChecks),包含userDetails中最后一个方法检查,如果均验证成功会创建一个SucessAuthentication,生成已认证的Authentication,successfulAuthentication会调用登录成功的处理器(可以在handler中自定义登录成功处理器)。如果期间出现任何异常会调用unsuccessfulAuthentication,在handler里可以调自定义失败的处理器

    image.png
    image.png

    SecurityContextPersistenceFilter作用是检查请求里是否有securityContext,请求来时有SecurityContext则拿出来放在线程里,如果请求返回时有则拿出来放到session里(同一个线程里SecurityContextHolders.getContext随时可以拿到用户信息)

    2、认证的代码逻辑
    认证是通过UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter,主要方法是doFilter

      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest)req;
            HttpServletResponse response = (HttpServletResponse)res;
            //不需要验证的不拦截,到下一个filter
            if (!this.requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Request is to process authentication");
                }
    
                Authentication authResult;
                try {
                  //进行验证
                    authResult = this.attemptAuthentication(request, response);
                    if (authResult == null) {
                        return;
                    }
    
                    this.sessionStrategy.onAuthentication(authResult, request, response);
                } catch (InternalAuthenticationServiceException var8) {
                    this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                    this.unsuccessfulAuthentication(request, response, var8);
                    return;
                } catch (AuthenticationException var9) {
                    this.unsuccessfulAuthentication(request, response, var9);
                    return;
                }
    
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }
    
                this.successfulAuthentication(request, response, chain, authResult);
            }
        }
    

    验证的逻辑如下,生成UsernamePasswordAuthenticationToken ,UsernamePasswordAuthenticationToken 继承了AbstractAuthenticationToken,可以理解为UsernamePasswordAuthenticationToken 实例化了Authentication接口,继而按照流程,将其传递给AuthenticationMananger调用身份验证核心完成相关工作。

      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            } else {
                String username = this.obtainUsername(request);
                String password = this.obtainPassword(request);
                if (username == null) {
                    username = "";
                }
    
                if (password == null) {
                    password = "";
                }
    
                username = username.trim();
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
    
    image.png

    最后逻辑就很清晰了调的是UserDetailsService().loadUserByUsername方法,我们可以通过重写loadUserByUsername来对数据库和界面用户信息校验

    2、主要代码分析

    代码结构


    image.png

    SecurityConfig文件,通过重写WebSecurityConfigurerAdapter中的方法来自定义拦截授权配置,这里添加了
    AuthenticationFailureHandler 和AuthenticationSuccessHandler 方便定位问题。

    
    @Configuration
    @EnableWebSecurity// 这个注解必须加,开启Security
    @EnableGlobalMethodSecurity(prePostEnabled = true)//保证post之前的注解可以使用
    @Slf4j
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDetailsService myUserDetailsService;
        @Autowired
        private  AuthenticationFailureHandler failureHandler;
        @Autowired
        private  AuthenticationSuccessHandler successHandler;
    
    
        @Override
        public void configure(WebSecurity web) throws Exception {
    
            web.ignoring().antMatchers("/login.html");
        }
    
        //拦截在这配,授权
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.formLogin()
                    .successHandler(successHandler)
                    .failureHandler(failureHandler)
                    .loginPage("/index")
                    //进行登录校验
                    .loginProcessingUrl("/login").and()
                    .authorizeRequests()
                    .antMatchers( "/index").permitAll()
                    //匹配器登录成功跳转页面,这里加了角色校验
    //                .antMatchers("/home").hasAnyRole("st")
                    .anyRequest().authenticated()   // 剩下所有的验证都需要验证
                    .and()
                    .csrf().disable().cors();  // 禁用 Spring Security 自带的跨域处理
        }
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoderBean() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    

    重写的UserDetailsService方法,这里数据是自己模拟的。 实体类User 和SecurityUser自己补一下。

    @Service
    public class MyUserDetailsService implements UserDetailsService {
    
        /***
         * 根据账号获取用户信息
         * @param :
         * @return: org.springframework.security.core.userdetails.UserDetails
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //数据库信息
            User user = new User();
            user.setUsername("lili");
            user.setPassword(  new BCryptPasswordEncoder().encode("123456"));
            if(!user.getUsername().equals(username) ) {
                throw new UsernameNotFoundException("用户名或密码错误");
            }
            return new  SecurityUser(user, getGrantedAuthority(user));
        }
        public List<GrantedAuthority> getGrantedAuthority(User user){
            List<GrantedAuthority> list = new ArrayList<>();
            list.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            return list;
        }
    }
    

    前端页面 login.html,其中action这里是登录后要跳转的页面

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
    <body>
    <h1>Spring Security</h1>
    <form method="post" action="/test/login">
        <div>
            用户名:<input type="text" name="username" id="username">
        </div>
        <div>
            密码:<input type="password" name="password" id="password">
        </div>
        <div>
            <!--        <label><input type="checkbox" name="remember-me" id="remember-me"/>自动登录</label>-->
            <button type="submit">登陆</button>
        </div>
    </form>
    </body>
    </html>
    

    持续更新中。。。


    三、添加durid数据源
    pom依赖

      <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>1.5.12.RELEASE</version>
            <relativePath/>
        </parent>
    
        <properties>
            <mysql-connector-java.version>8.0.21</mysql-connector-java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.1</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- druid -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.10</version>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/log4j/log4j -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
            <!--安全-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
            <!--jwt-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
    

    配置文件,这里需要注意datasource下面没有druid这层,否则我这里出现了可以访问页面但无法识别filter的现象,就拦截不到sql

    spring.datasource.type= com.alibaba.druid.pool.DruidDataSource
    spring.datasource.filters= stat,wall,log4j
    spring.datasource.filter.stat.enabled=true
    spring.datasource.filter.stat.db-type=mysql
    spring.datasource.filter.stat.log-slow-sql=true
    spring.datasource.filter.stat.slow-sql-millis=2000
    
    #是否启用StatFilter默认值true
    spring.datasource.web-stat-filter.enabled=true
    ##spring.datasource.druid.web-stat-filter.url-pattern=
    spring.datasource.web-stat-filter.exclusions=/static/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
    
    # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
    #是否启用StatViewServlet默认值true
    spring.datasource.stat-view-servlet.enabled=true
    spring.datasource.stat-view-servlet.url-pattern=/druid/*
    spring.datasource.stat-view-servlet.reset-enable=false
    
    #初始化时建立物理连接的个数
    spring.datasource.initial-size=5
    最小连接池数量
    spring.datasource.min-idle=5
    spring.datasource.max-active=20
    spring.datasource.max-wait=60000
    spring.datasource.time-between-eviction-runs-millis=3000000
    spring.datasource.validation-query=select 1 from dual
    spring.datasource.test-on-borrow=false
    spring.datasource.test-on-return=false
    spring.datasource.test-while-idle=true
    spring.datasource.pool-prepared-statements=true
    spring.datasource.max-pool-prepared-statement-per-connection-size=20
    spring.datasource.use-global-data-source-stat=true
    spring.datasource.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    

    我这里用了springSecurity所以过滤的请求要将durid的请求拦截去掉,不然登录页面走的是springSecurity页面会出现登录无法跳转情况


    image.png

    请求链接localhost:port/druid/sql.html


    image.png

    参考链接

    1、表单登录权限验证

    2、springSecurity的filter详解

    3、视频参考

    4、UsernamePasswordAuthenticationFilter

    5、官方链接

    6、AuthenticationManagerBuilder

    相关文章

      网友评论

          本文标题:springSecurity原理简析+基于springboot基

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