spring boot整合 spring security

Posted 健康平安的活着

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring boot整合 spring security相关的知识,希望对你有一定的参考价值。

一   spring security

1.1 spring security的作用

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

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

FilterChainProxy是一个代理 ,真正起作用的是 FilterChainProxy SecurityFilterChain 所包含的各个 Filter ,同时 这些Filter 作为 Bean Spring 管理,它们是 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 Security DaoAuthenticationProvider处理。

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是用户信息:

Authentication getCredentials() 与 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 、用户输入密码(明文 )
2 DaoAuthenticationProvider 获取 UserDetails (其中存储了用户的正确密码)
3 DaoAuthenticationProvider 使用 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 实现类如下,它们分别是
AffiffiffirmativeBased ConsensusBased UnanimousBased
1. AffiffiffirmativeBased 的逻辑是:
1 只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票 但是有人投反对票,则将抛出AccessDeniedException。
Spring security默认使用的是AffiffiffirmativeBased
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 也内置一些投票者实现类如 RoleVoter AuthenticatedVoter WebExpressionVoter 等,可以 自行查阅资料进行学习。

二 spring boot整合spring security

2.1 工程结构

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 容器。

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 @ComponentScan WebConfifig 如下
@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 自动装配机制,这里无需使用 @EnableWebSecurity WebSecurityConfifig 内容如下
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
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 测试

1. 未登录

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

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

2.登录情况

 访问资源1:

访问资源2:

总结:

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

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

以上是关于spring boot整合 spring security的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot如何整合Redis

Spring Boot如何整合Redis

Spring Boot:Spring Boot整合FreeMarker

spring boot 系列之四:spring boot 整合JPA

Spring Boot系列Spring Boot整合持久层

Spring Boot 2.X - Spring Boot整合AMQP之RabbitMQ