认证与授权流程与spring boot整合 spring security

2022年9月13日08:13:11

一   spring security

1.1 spring security的作用

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源

Spring Security对Web资源的保护是靠Filter实现的,当初始化Spring Security时,会创建一个名为SpringSecurityFilterChainServlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此 类。如下图所示:

FilterChainProxy是一个代理,真正起作用的是FilterChainProxySecurityFilterChain所包含的各个Filter,同时 这些Filter作为BeanSpring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理,如下图所示:

 1.2 spring security的过滤器的请求过程

spring Security功能的实现主要是由一系列过滤器链相互配合完成

1.SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository中获取SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将SecurityContextHolder持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除securityContextHolder 所持有的 SecurityContext
2.UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler

AuthenticationFailureHandler,这些都可以根据需求做相关改变;
3.FilterSecurityInterceptor是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问
4.ExceptionTranslationFilter能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出

 1.3 spring security认证流程

1.3.1.认证流程

1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证

3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。

4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过

SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。

咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

1.认证管理器(AuthenticationManager)委托 AuthenticationProvider完成认证工作AuthenticationProvider是一个接口,如下图

 在上图中authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户所提交的用户、密码等。而返回值也是一Authentication,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。

2.Spring Security中维护着一个List<AuthenticationProvider>列表,存放多种认证方式,不同的认证方式使用不 同的AuthenticationProvider。每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,

web表单提交用户名密码时,Spring SecurityDaoAuthenticationProvider处理。

3.Authentication(认证信息)的结构,它是一个接口,我们之前提到的

UsernamePasswordAuthenticationToken就是它的实现之一:

1Authenticationspring security包中的接口,直接继承自Principal类,而Principal是位于java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。

2getAuthorities()权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系 列字符串。

3getCredentials()凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。

4getDetails()细节信息,web应用中的实现接口通常为WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值

5getPrincipal()身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细 信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。

1.3.2.UserDetailsService

DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个

Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份信息就是一个Object,大多数情况下它可以被强转为UserDetails对象。

DaoAuthenticationProvider中包含了一个UserDetailsService例,它负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的UserDetailsService公开为spring bean来定义自定义身份验证。

DaoAuthenticationProviderUserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。

UserDetails是用户信息:

AuthenticationgetCredentials()与 UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。

通过实现UserDetailsServiceUserDetails我们可以完成对用户信息获取方式以及用户信息字段的扩展。

Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

1.4 spring security的密码器PasswordEncoder

DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,DaoAuthenticationProvider通过PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:

 1.Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如 下声明即可,如下

NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则 校验失败。采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:

1、用户输入密码(明文 )
2DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
3DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通 过,否则校验失败。

实际项目中推荐使用BCryptPasswordEncoder,Pbkdf2PasswordEncoder,SCryptPasswordEncoder等,感兴趣 的大家可以看看这些PasswordEncoder的具体实现。

2.编写一个test类

package com.ljf.spt.security;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author Administrator
 * @version 1.0
 **/
@RunWith(SpringRunner.class)
public class TestBCrypt {

    @Test
    public void testBCrypt(){

        //对密码进行加密
        String hashpw = BCrypt.hashpw("456", BCrypt.gensalt());
        System.out.println(hashpw);

        //校验密码
        boolean checkpw = BCrypt.checkpw("123", "$2a$10$aFsOFzujtPCnUCUKcozsHux0rQ/3faAHGFSVb9Y.B1ntpmEhjRtru");
        boolean checkpw2 = BCrypt.checkpw("123", "$2a$10$HuClcUqr/FSLmzSsp9SHqe7D51Keu1sAL7tUAAcb..FyILiLdFKYy");
        System.out.println("test1:"+checkpw);
        System.out.println("test2:"+checkpw2);
    }
}

 执行结果:

1.5 spring security的授权

1.5.1 授权流程

Spring Security可以通过http.authorizeRequests() 对web请求进行授权保护
Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问

 流程如下:

1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor 的子 类拦截
2.获取资源访问策略FilterSecurityInterceptor会从SecurityMetadataSource的子类
DefaultFilterInvocationSecurityMetadataSource获取要访问当前资源所需要的权限
Collection<ConfigAttribute> 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读 取访问策略如

 3.最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资 源,否则将禁止访问。

AccessDecisionManager(访问决策管理器)的核心接口如下:

重说明一下decide的参数:
1.authentication:要访问资源的访问者的身份
2.object:要访问的受保护资源,web请求对应FilterInvocation
3.confifigAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。

decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

1.5.2 授权策略

AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源

通过上图可以看出,AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication 是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。

ACCESS_GRANTED表示同意,
ACCESS_DENIED表示拒绝,
ACCESS_ABSTAIN表示弃权。
如果一个AccessDecisionVoter不能判定当前 Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN

1.5.3 投票类

Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是
AffirmativedBasedConsensusBasedUnanimousBased
1.AffirmativedBased的逻辑是:

1只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票但是有人投反对票,则将抛出AccessDeniedException。
Spring security默认使用的是AffirmativedBased
2. ConsensusBased的逻辑是:
1)如果赞成票多于反对票则表示通过。
2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException
3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表 示通过,否则将抛出异常AccessDeniedException
参数allowIfEqualGrantedDeniedDecisions的值默认为true。
4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值
true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false
3.UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递
AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfifigAttribute
AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的
ConfifigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。
UnanimousBased的逻辑具体来说是这样的:
1)如果受保护对象配置的某一个ConfifigAttribute被任意的AccessDecisionVoter反对了,则将抛出
AccessDeniedException
2)如果没有反对票,但是有赞成票,则表示通过。
3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出
AccessDeniedException
Spring Security也内置一些投票者实现类如RoleVoterAuthenticatedVoterWebExpressionVoter等,可以 自行查阅资料进行学习。

二 spring boot整合spring security

2.1 工程结构

spring security内嵌了自己的登录页面

2.2 配置pom文件

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.ljf.spt.security</groupId>
  <artifactId>spt-security-demo</artifactId>
  <version>1.0-SNAPSHOT</version>
   <!-- springboot-parent -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
  </parent>
  <name>spt-security-demo</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13</version>
      <scope>test</scope>
    </dependency>
    <!-- 以下是>spring boot依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 以下是>spring security依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>


    <!-- 以下是jsp依赖-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <scope>provided</scope>
    </dependency>
    <!--jsp页面使用jstl标签 -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <scope>provided</scope>
    </dependency>
    <!--用于编译jsp -->
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>9.0.39</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.0</version>
    </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-jdbc</artifactId>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>

      <plugin>
        <artifactId>maven-resources-plugin</artifactId>
        <configuration>
          <encoding>utf-8</encoding>
          <useDefaultDelimiters>true</useDefaultDelimiters>
          <resources>
            <resource>
              <directory>src/main/resources</directory>
              <filtering>true</filtering>
              <includes>
                <include>**/*</include>
              </includes>
            </resource>
            <resource>
              <directory>src/main/java</directory>
              <includes>
                <include>**/*.xml</include>
              </includes>
            </resource>
          </resources>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

2.3  spring容器的配置

SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器。
将springmvc中的applicationConfig.java在spring boot项目改成了application.properties配置文件进行配置相关操作。

2.3.1 Spring Boot配置文件

resources下添加application.properties,内容如下:

#基本配置
server.port=8080
server.servlet.context-path=/spt-security
spring.application.name =springboot-security
#视图
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp
#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/user_db
spring.datasource.username=root
spring.datasource.password=mysql
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

2.3.2 配置启动类

@SpringBootApplication
public class App 
{
    public static void main( String[] args )
    {
        SpringApplication.run(App.class,args);
        System.out.println("启动完成!!!");
    }
}

2.4  web.config的配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc@ComponentScanWebConfifig如下
@Configuration//就相当于springmvc.xml文件
public class WebConfig implements WebMvcConfigurer {


    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }

}

2.5  安全配置文件WebSecurityConfig.config的配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebSecurityWebSecurityConfifig内容如下
package com.ljf.spt.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Administrator
 * @version 1.0
 **/
//@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //1.定义用户信息服务(查询用户信息)
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    //2.密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //3.安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/r1").hasAuthority("p1") //p1角色具有访问/user/r1读取权限
                .antMatchers("/user/r2").hasAuthority("p2")  //p2角色具有访问/user/r2读取权限
                .antMatchers("/user/**").authenticated()//所有/user/**的请求必须认证通过
                .anyRequest().permitAll()//除了/user/**,其它的请求可以不经过认证,就可以访问
                .and()
                .formLogin()//允许表单登录
                .successForwardUrl("/login-success");//自定义登录成功的页面地址,登录成功跳转的地址

    }
}

2.6  controller


    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        //提示具体用户名称登录成功
        return getUsername()+" 登录成功";
    }

2.7 测试

注意这是springboot启动方式不用在tomcat方式下启动:直接运行springboot的启动入口程序就行,如下图:

 如果启动tomcat,则报404

1. 未登录

访问资源r1,跳转到登录页面

 访问资源r2,跳转到登录页面

2.登录情况

 访问资源1:

访问资源2:

总结:

1、未登录成功时,访问/user/r1和/user/r2,均跳转到登录页面,进行认证登录

2、登录成功时,访问/user/r1和/user/r2,有权限时则正常访问,否则返回403(拒绝访问)

  • 作者:健康平安的活着
  • 原文链接:https://blog.csdn.net/u011066470/article/details/119299035
    更新时间:2022年9月13日08:13:11 ,共 14069 字。