springSecurity原理简析+基于springboot基

2021-04-19  本文已影响0人  灿烂的GL

一、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

上一篇下一篇

猜你喜欢

热点阅读