文章目录

  • Spring Security快速入门
  • 1.Spring Security介绍
  • 2.创建工程
  • 1.创建maven工程
  • 2. Spring容器配置
  • 3.Servlet Context配置
  • 4.加载 Spring 容器
  • 3.认证
  • 1.认证页面
  • 2.安全配置
  • 3.Spring Security初始化
  • 4.默认根路径请求
  • 5.认证成功页面
  • 6. 测试
  • 4.授权
  • 5.小结
  • Spring Security 应用详解
  • 1.集成SpringBoot
  • 1.Spring Boot 介绍
  • 2.创建maven工程
  • 3.spring 容器配置
  • 4.Servlet Context配置
  • 5.安全配置
  • 6.测试
  • 2.工作原理
  • 1.结构总览
  • 2.认证流程
  • 1.认证流程
  • 2.AuthenticationProvider
  • 3.UserDetailsService
  • 4.PasswordEncoder
  • 3.授权流程
  • 1.授权流程
  • 2.授权决策
  • 3.自定义认证
  • 1.自定义登录页面
  • 1.认证页面
  • 2.配置认证页面
  • 3.安全配置
  • 4.测试
  • 2.连接数据库认证
  • 1.创建数据库
  • 2.代码实现
  • 3. 定义UserDetailService
  • 4.测试
  • 5.使用BCryptPasswordEncoder
  • 4.会话
  • 1.获取用户身份
  • 2.会话控制
  • 5.退出
  • 6.授权
  • 1.概述
  • 2.准备环境
  • 1.数据库环境
  • 2.修改UserDetailService
  • 3.web授权
  • 4.方法授权


Spring Security快速入门

1.Spring Security介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。

2.创建工程

1.创建maven工程

1)创建maven工程 security-spring-security,工程结构如下:

Spring Security现在还有必要学吗_mvc


2)引入以下依赖:

在security-springmvc的基础上增加spring-security的依赖:

<dependency> 
	<groupId>org.springframework.security</groupId> 
	<artifactId>spring‐security‐web</artifactId> 
	<version>5.1.4.RELEASE</version> 
</dependency> 
<dependency> 
	<groupId>org.springframework.security</groupId> 
	<artifactId>spring‐security‐config</artifactId> 
	<version>5.1.4.RELEASE</version> 
</dependency>

2. Spring容器配置

同security-springmvc。

@Configuration //相当于applicationContext.xml
@ComponentScan(basePackages = "com.itheima.security.springmvc",
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
    //在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

3.Servlet Context配置

同security-springmvc.

@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.itheima.security.springmvc",
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    //视图解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

4.加载 Spring 容器

在init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口, Spring容器启动时加载WebApplicationInitializer接口的所有实现类。

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

3.认证

1.认证页面

springSecurity默认提供认证页面,不需要额外开发。

Spring Security现在还有必要学吗_ide_02

2.安全配置

spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。

  1. 在config包下定义WebSecurityConfig,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
    @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;
    }

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

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

userDetailsService()方法中,我们返回了一个UserDetailsService给spring容器,Spring Security会使用它来 获取用户信息。我们暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了zhangsan、lisi两个用 户,并设置密码和权限。

而在configure()中,我们通过HttpSecurity设置了安全拦截规则,其中包含了以下内容:
(1)url匹配/r/**的资源,经过认证后才能访问。
(2)其他url完全开放。
(3)支持form表单认证,认证成功后转向/login-success。
关于HttpSecurity的配置清单:

方法

说明

openidLogin()

用于基于 OpenId 的验证

headers()

将安全标头添加到响应

cors()

配置跨域资源共享( CORS )

sessionManagement()

允许配置会话管理

portMapper()

允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定 向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口 8443,HTTP 端口80到 HTTPS 端口443

jee()

配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理

x509()

配置基于x509的认证

rememberMe

允许配置“记住我”的验证

authorizeRequests()

允许基于使用HttpServletRequest限制访问

requestCache()

允许配置请求缓存

exceptionHandling()

允许配置错误处理

securityContext()

在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将 自动应用

servletApi()

将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。当使用WebSecurityConfigurerAdapter时,这将自动应用

csrf()

添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用

logout()

添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来 清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”

anonymous()

允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用 org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”

formLogin()

指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面

oauth2Login()

根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证

requiresChannel()

配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射

httpBasic()

配置 Http Basic 验证

addFilterAt()

在指定的Filter类的位置添加过滤器

  1. 加载 WebSecurityConfig
    修改SpringApplicationInitializer的getRootConfigClasses()方法,添加WebSecurityConfig.class:
@Override 
protected Class<?>[] getRootConfigClasses() { 
	return new Class<?>[] { ApplicationConfig.class, WebSecurityConfig.class}; 
}

3.Spring Security初始化

Spring Security初始化,这里有两种情况。

  • 若当前环境没有使用Spring或Spring MVC,则需要将 WebSecurityConfig(Spring Security配置类)传入超 类,以确保获取配置,并创建spring context。
  • 相反,若当前环境已经使用spring,我们应该在现有的springContext中注册Spring Security(上一步已经做将WebSecurityConfig加载至rootcontext),此方法可以什么都不做。

在init包下定义SpringSecurityApplicationInitializer:

public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { 
	public SpringSecurityApplicationInitializer() { 
		//super(WebSecurityConfig.class); 
	} 
}

4.默认根路径请求

在WebConfig.java中添加默认请求根路径跳转到/login,此url为spring security提供:

//默认Url根路径跳转到/login,此url为spring security提供 
@Override 
public void addViewControllers(ViewControllerRegistry registry) { 	
	registry.addViewController("/").setViewName("redirect:/login"); 
}

spring security默认提供的登录页面。

5.认证成功页面

在安全配置中,认证成功将跳转到/login-success,代码如下:

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

spring security支持form表单认证,认证成功后转向/login-success。
在LoginController中定义/login-success:

@RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
public String loginSuccess(){
    return " 登录成功";
}

6. 测试

(1)启动项目,访问http://localhost:8080/security-spring-security/路径地址

Spring Security现在还有必要学吗_mvc_03


页面会根据WebConfig中addViewControllers配置规则,跳转至/login,/login是pring Security提供的登录页面。(2)登录

1、输入错误的用户名、密码

Spring Security现在还有必要学吗_ide_04


2、输入正确的用户名、密码,登录成功(3)退出

1、请求/logout退出

Spring Security现在还有必要学吗_mvc_05


2、退出 后再访问资源自动跳转到登录页面

4.授权

实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授 权实现方法。
在LoginController添加/r/r1或/r/r2

/**
 * 测试资源1
 * @return
 */
@GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
public String r1(){
    return " 访问资源1";
}

/**
 * 测试资源2
 * @return
 */
@GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
public String r2(){
    return " 访问资源2";
}

在安全配置类WebSecurityConfig.java中配置授权规则:

.antMatchers("/r/r1").hasAuthority("p1") 
.antMatchers("/r/r2").hasAuthority("p2")

.antMatchers("/r/r1").hasAuthority(“p1”)表示:访问/r/r1资源的 url需要拥有p1权限。
.antMatchers("/r/r2").hasAuthority(“p2”)表示:访问/r/r2资源的 url需要拥有p2权限。
完整的WebSecurityConfig方法如下:

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

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

5.小结

通过快速上手,咱们使用Spring Security实现了认证和授权,Spring Security提供了基于账号和密码的认证方式, 通过安全配置即可实现请求拦截,授权功能,Spring Security能完成的不仅仅是这些。

Spring Security 应用详解

1.集成SpringBoot

1.Spring Boot 介绍

Spring Boot是一套Spring的快速开发框架,基于Spring 4.0设计,使用Spring Boot开发可以避免一些繁琐的工程 搭建和配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 Spring Boot开发,例如:MyBatis、Dubbo等,Spring 家族更是如此,例如:Spring cloud、Spring mvc、 Spring security等,使用Spring Boot开发可以大大得高生产率,所以Spring Boot的使用率非常高。

本章节讲解如何通过Spring Boot开发Spring Security应用,Spring Boot提供spring-boot-starter-security用于开 发Spring Security应用。

2.创建maven工程

1)创建maven工程 security-spring-boot,工程结构如下:

Spring Security现在还有必要学吗_spring_06


2)引入以下依赖:

<?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.itheima.security</groupId>
    <artifactId>security-springboot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
    </parent>

    <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>
        <!-- 以下是>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>
        </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>
            <scope>provided</scope>
        </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>
        <finalName>security-springboot</finalName>
        <pluginManagement>
            <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>
        </pluginManagement>
    </build>
</project>

3.spring 容器配置

SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器。
1)Spring Boot配置文件
在resources下添加application.properties,内容如下:

server.port=8080 
server.servlet.context‐path=/security‐springboot 
spring.application.name = security‐springboot

2)Spring Boot 启动类

@SpringBootApplication 
public class SecuritySpringBootApp { 
	public static void main(String[] args) { 
		SpringApplication.run(SecuritySpringBootApp.class, args); 
	} 
}

4.Servlet Context配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc与@ComponentScan,WebConfig如下

@Configuration 
public class WebConfig implements WebMvcConfigurer { 
	//默认Url根路径跳转到/login,此url为spring security提供 
	@Override 
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/").setViewName("redirect:/login");  	
	} 
}

视频解析器配置在application.properties中

spring.mvc.view.prefix=/WEB‐INF/views/ 
spring.mvc.view.suffix=.jsp

5.安全配置

由于Spring boot starter自动装配机制,这里无需使用@EnableWebSecurity,WebSecurityConfig内容如下

@Configuration 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
	//内容跟Spring security入门程序一致 
}

6.测试

LoginController的内容同同Spring security入门程序。

@RestController
public class LoginController {

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

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限才可以访问
    public String r1(){
        return getUsername()+" 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限才可以访问
    public String r2(){
        return getUsername()+" 访问资源2";
    }

    //获取当前用户信息
    private String getUsername(){
        String username = null;
        //当前认证通过的用户身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用户身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }
}

测试过程:
1、测试认证
2、测试退出
3、测试授权

2.工作原理

1.结构总览

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。

当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此 类,下图是Spring Security过虑器链结构图:

Spring Security现在还有必要学吗_spring_07


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

Spring Security现在还有必要学吗_spring_08


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

Spring Security现在还有必要学吗_spring_09


下面介绍过滤器链中主要的几个过滤器及其作用:

SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;

UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;

FilterSecurityInterceptor是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前 面已经详细介绍过了;

ExceptionTranslationFilter能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

2.认证流程

1.认证流程

Spring Security现在还有必要学吗_mvc_10


仔细分析认证过程:

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

认证核心组件的大体关系如下:

Spring Security现在还有必要学吗_spring_11

2.AuthenticationProvider

通过前面的Spring Security认证流程我们得知,认证管理器(AuthenticationManager)委托 AuthenticationProvider完成认证工作。
AuthenticationProvider是一个接口,定义如下:

public interface AuthenticationProvider { 
	Authentication authenticate(Authentication authentication) throws AuthenticationException; 
	boolean supports(Class<?> var1); 
}

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

Spring Security中维护着一个 List 列表,存放多种认证方式,不同的认证方式使用不 同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用 AuthenticationProvider2等等这样的例子很多。

每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证, 在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面 封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider来处理它?

我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码:

public boolean supports(Class<?> authentication) { 
	return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); 
}

也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。

最后,我们来看一下Authentication(认证信息)的结构,它是一个接口,我们之前提到的 UsernamePasswordAuthenticationToken就是它的实现之一:

public interface Authentication extends Principal, Serializable { (1) 
	Collection<? extends GrantedAuthority> getAuthorities();      (2) 
	Object getCredentials(); 								 	  (3) 
	Object getDetails(); 										  (4) 
	Object getPrincipal(); 										  (5) 
	boolean isAuthenticated(); 
	void setAuthenticated(boolean var1) throws IllegalArgumentException; 
}

(1)Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security 包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。
(2)getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系 列字符串。
(3)getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
(4)getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地 址和sessionId的值。
(5)getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细 信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。

3.UserDetailsService

1)认识UserDetailsService
现在咱们现在知道DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个 Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为UserDetails对象。

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

public interface UserDetailsService { 
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 
}

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

上面一直提到UserDetails是用户信息,咱们看一下它的真面目:

public interface UserDetails extends Serializable { 
	Collection<? extends GrantedAuthority> getAuthorities(); 
	String getPassword(); 
	String getUsername(); 
	boolean isAccountNonExpired(); 
	boolean isAccountNonLocked(); 
	boolean isCredentialsNonExpired(); 
	boolean isEnabled(); 
}

它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与 UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形 成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。

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

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

2)测试
自定义UserDetailsService

@Service 
public class SpringDataUserDetailsService implements UserDetailsService { 
	@Override 
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
		//登录账号 
		System.out.println("username="+username);
		 //根据账号去数据库查询... 
		 //这里暂时使用静态数据 
		 UserDetails userDetails = User.withUsername(username).password("123").authorities("p1").build(); 
		 return userDetails; 
	} 
}

屏蔽安全配置类中UserDetailsService的定义

/* 
@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; 
}
*/

重启工程,请求认证,SpringDataUserDetailsService的loadUserByUsername方法被调用 ,查询用户信息。

4.PasswordEncoder

1)认识PasswordEncoder
DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?

在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:

public interface PasswordEncoder { 
	String encode(CharSequence var1); 
	boolean matches(CharSequence var1, String var2); 
	default boolean upgradeEncoding(String encodedPassword) { 
		return false; 
	} 
}

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

@Bean 
public PasswordEncoder passwordEncoder() { 
	return NoOpPasswordEncoder.getInstance(); 
}

NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1、用户输入密码(明文 )
2、DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通 过,否则校验失败。

NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则 校验失败。

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

2)使用BCryptPasswordEncoder
1、配置BCryptPasswordEncoder
在安全配置类中定义:

@Bean 
public PasswordEncoder passwordEncoder() { 
	return new BCryptPasswordEncoder(); 
}

测试发现认证失败,提示:Encoded password does not look like BCrypt。
原因:
由于UserDetails中存储的是原始密码(比如:123),它不是BCrypt格式。
跟踪 DaoAuthenticationProvider第33行代码查看 userDetails中的内容 ,跟踪第38行代码查看 PasswordEncoder的类型。

2、测试BCrypt
通过下边的代码测试BCrypt加密及校验的方法
添加依赖:

<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring‐boot‐starter‐test</artifactId> 
	<scope>test</scope> 
</dependency>

编写测试方法:

@RunWith(SpringRunner.class) 
public class TestBCrypt { 
	@Test 
	public void test1(){ 
		//对原始密码加密
		String hashpw = BCrypt.hashpw("123",BCrypt.gensalt()); 
		System.out.println(hashpw); 
		//校验原始密码和BCrypt密码是否一致 
		boolean checkpw = BCrypt.checkpw("123", "$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm"); 
		System.out.println(checkpw); 
	} 
}

3、修改安全配置类
将UserDetails中的原始密码修改为BCrypt格式

manager.createUser(User.withUsername("zhangsan").password("$2a$10$1b5mIkehqv5c4KRrX9bUj.A4Y2hug3IGCnMCL5i4RpQrYV12xNKye").authorities("p1").build());

实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。

3.授权流程

1.授权流程

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

Spring Security的授权流程如下:

Spring Security现在还有必要学吗_mvc_12


分析授权流程:

  1. 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子 类拦截。
  2. 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection<ConfigAttribute> 。
    SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读 取访问策略如:
http
	.authorizeRequests() 
		.antMatchers("/r/r1").hasAuthority("p1") 
		.antMatchers("/r/r2").hasAuthority("p2") 
		...
  1. 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资 源,否则将禁止访问。
    AccessDecisionManager(访问决策管理器)的核心接口如下:
public interface AccessDecisionManager { 
	/** 
	 * 通过传递的参数来决定用户是否有访问对应受保护资源的权限 
	 */
	void decide(Authentication authentication,Object object,Collection<ConfigAttribute> configAttributes ) throws
	AccessDeniedException, InsufficientAuthenticationException;   
	//略.. 
}

这里着重说明一下decide的参数:
authentication:要访问资源的访问者的身份。
object:要访问的受保护资源,web请求对应FilterInvocation。
configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

2.授权决策

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

Spring Security现在还有必要学吗_spring_13


通过上图可以看出,AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication 是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。

AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

public interface AccessDecisionVoter<S> { 
	int ACCESS_GRANTED = 1; 
	int ACCESS_ABSTAIN = 0; 
	int ACCESS_DENIED = ‐1; 
	boolean supports(ConfigAttribute var1); 
	boolean supports(Class<?> var1); 
	int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3); 
}

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

Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是 AffirmativeBasedConsensusBasedUnanimousBased

AffirmativeBased的逻辑是:
(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。 Spring security默认使用的是AffirmativeBased。

ConsensusBased的逻辑是:
(1)如果赞成票多于反对票则表示通过。
(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。
(3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表 示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。
(4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值 为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。

UnanimousBased的逻辑是:
与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递 给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给 AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的 ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。 UnanimousBased的逻辑具体来说是这样的:
(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出 AccessDeniedException。
(2)如果没有反对票,但是有赞成票,则表示通过。
(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出 AccessDeniedException。
Spring Security也内置一些投票者实现类如RoleVoterAuthenticatedVoterWebExpressionVoter等,可以 自行查阅资料进行学习。

3.自定义认证

Spring Security提供了非常好的认证扩展方法,比如:快速上手中将用户信息存储到内存中,实际开发中用户信息 通常在数据库,Spring security可以实现从数据库读取用户信息,Spring security还支持多种授权方法。

1.自定义登录页面

快速上手中,你可能会想知道登录页面从哪里来的?因为我们并没有提供任何的HTML或JSP文件。Spring Security的默认配置没有明确设定一个登录页面的URL,因此Spring Security会根据启用的功能自动生成一个登录 页面URL,并使用默认URL处理登录的提交内容,登录后跳转的到默认URL等等。尽管自动生成的登录页面很方便 快速启动和运行,但大多数应用程序都希望定义自己的登录页面。

1.认证页面

将security-springmvc工程的login.jsp拷贝到security-springboot下,目录保持一致。

Spring Security现在还有必要学吗_spring_14

2.配置认证页面

在WebConfig.java中配置认证页面地址:

//默认Url根路径跳转到/login,此url为spring security提供 
@Override 
public void addViewControllers(ViewControllerRegistry registry) { 	
    registry.addViewController("/").setViewName("redirect:/login‐view"); 	 
	registry.addViewController("/login‐view").setViewName("login"); 
}
3.安全配置

在WebSecurityConfig中配置表章登录信息:

//配置安全拦截机制 
@Override 
protected void configure(HttpSecurity http) throws Exception { 
	http 
		.authorizeRequests() 
		.antMatchers("/r/**").authenticated() 
		.anyRequest().permitAll() 
		.and() 
		.formLogin()                             (1) 
			.loginPage("/login‐view")            (2) 
			.loginProcessingUrl("/login")        (3) 
			.successForwardUrl("/login‐success") (4) 
			.permitAll(); 
}

(1)允许表单登录。
(2)指定我们自己的登录页,spring security以重定向方式跳转到/login-view。
(3)指定登录处理的URL,也就是用户名、密码表单提交的目的路径。
(4)指定登录成功后的跳转URL。
(5)我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个 formLogin().permitAll() 方法允许 任意用户访问基于表单登录的所有的URL。

4.测试

当用户没有认证时访问系统的资源会重定向到login-view页面。

Spring Security现在还有必要学吗_spring_15


输入账号和密码,点击登录,报错:

Spring Security现在还有必要学吗_mvc_16


问题解决:

spring security为防止CSRF(Cross-site request forgery跨站请求伪造)的发生,限制了除了get以外的大多数方 法。

解决方法1:
屏蔽CSRF控制,即spring security不再限制CSRF。
配置WebSecurityConfig。

@Override 
protected void configure(HttpSecurity http) throws Exception { 
	http.csrf().disable() //屏蔽CSRF控制,即spring security不再限制CSRF ... 
}

本案例采用方法1

解决方法2:
在login.jsp页面添加一个token,spring security会验证token,如果token合法则可以继续请求。
修改login.jsp

<form action="login" method="post"> 
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> 
	... 
</form>

2.连接数据库认证

前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中,本节实现从数据库读取用户信 息。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。

1.创建数据库

创建user_db数据库

CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

创建t_user表

CREATE TABLE `t_user`  ( 
	`id` bigint(20) NOT NULL COMMENT '用户id',
	`username` varchar(64) NOT NULL, 
	`password` varchar(64) NOT NULL, 
	`fullname` varchar(255) NOT NULL COMMENT '用户姓名', 
	`mobile` varchar(11) DEFAULT NULL COMMENT '手机号', 
	PRIMARY KEY (`id`) USING BTREE 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
2.代码实现

1)定义dataSource
在application.properties配置

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)添加依赖

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

3)定义Dao
定义模型类型,在model包定义UserDto:

@Repository 
public class UserDao {
	@Autowired 
	JdbcTemplate jdbcTemplate;
	public UserDto getUserByUsername(String username){ 
		String sql ="select id,username,password,fullname from t_user where username = ?"; 
		List<UserDto> list = jdbcTemplate.query(sql,new Object[]{username}, new BeanPropertyRowMapper<>
		(UserDto.class)); 
		if(list == null && list.size() <= 0){ return null; }return list.get(0); 
	}
}
3. 定义UserDetailService

在service包下定义SpringDataUserDetailsService:

@Service 
public class SpringDataUserDetailsService implements UserDetailsService { 
	@Autowired 
	UserDao userDao; 
	@Override 
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
		//登录账号 
		System.out.println("username="+username); 
		//根据账号去数据库查询... 
		UserDto user = userDao.getUserByUsername(username); 
		if(user == null){ 
			return null; 
		}
		//这里暂时使用静态数据 
		UserDetails userDetails = User.withUsername(user.getFullname()).password(user.getPassword())  
		.authorities("p1").build(); 
		return userDetails; 
	} 
}
4.测试

输入账号和密码请求认证,跟踪代码。

5.使用BCryptPasswordEncoder

按照我们前边讲的PasswordEncoder的使用方法,使用BCryptPasswordEncoder需要完成如下工作:
1、在安全配置类中定义BCryptPasswordEncoder

@Bean 
public PasswordEncoder passwordEncoder() { 
	return new BCryptPasswordEncoder(); 
}

2、UserDetails中的密码存储BCrypt格式

前边实现了从数据库查询用户信息,所以数据库中的密码应该存储BCrypt格式。

Spring Security现在还有必要学吗_spring_17

4.会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管 理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取 用户身份。

1.获取用户身份

编写LoginController,实现/r/r1、/r/r2的测试资源,并修改loginSuccess方法,注意getUsername方法,Spring Security获取当前登录用户信息的方法为SecurityContextHolder.getContext().getAuthentication()。

@RestController
public class LoginController {

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

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限才可以访问
    public String r1(){
        return getUsername()+" 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限才可以访问
    public String r2(){
        return getUsername()+" 访问资源2";
    }

    //获取当前用户信息
    private String getUsername(){
        String username = null;
        //当前认证通过的用户身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用户身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }
}

测试

登录前访问资源

被重定向至登录页面。

登录后访问资源

成功访问资源,如下:

Spring Security现在还有必要学吗_spring_18

2.会话控制

我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:

机制

描述

always

如果没有session存在就创建一个

ifRequired

如果需要就创建一个Session(默认)登录时

never

SpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它。

stateless

SpringSecurity将绝对不会创建Session,也不使用Session

通过以下配置方式对该选项进行配置:

@Override 
protected void configure(HttpSecurity http) throws Exception { 
	http.sessionManagement() 
	    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}

默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired

若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了 session,那么Spring Security会用它的。

若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建 session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。

会话超时
可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s。
spring boot 配置文件:

server.servlet.session.timeout=3600s

session超时之后,可以通过Spring Security 设置跳转的路径。

http
	.sessionManagement() 
	.expiredUrl("/login‐view?error=EXPIRED_SESSION") 
	.invalidSessionUrl("/login‐view?error=INVALID_SESSION");

expired指session过期,invalidSession指传入的sessionid无效。

安全会话cookie
我们可以使用httpOnly和secure标签来保护我们的会话cookie:

  • httpOnly:如果为true,那么浏览器脚本将无法访问。
  • cookie secure:如果为true,则cookie将仅通过HTTPS连接发送。

5.退出

Spring security默认实现了logout退出,访问/logout,果然不出所料,退出功能Spring也替我们做好了。

Spring Security现在还有必要学吗_mvc_19


点击“Log Out”退出 成功。

退出 后访问其它url判断是否成功退出。

这里也可以自定义退出成功的页面:

在WebSecurityConfig的protected void configure(HttpSecurity http)中配置:

.and() 
.logout() 
.logoutUrl("/logout") 
.logoutSuccessUrl("/login‐view?logout");

当退出操作出发时,将发生:

  • 使HTTP Session 无效。
  • 清除 SecurityContextHolder。
  • 跳转到 /login-view?logout。
    但是,类似于配置登录功能,咱们可以进一步自定义退出功能:
@Override 
protected void configure(HttpSecurity http) throws Exception { 
	http .authorizeRequests() 
	     //... 
	     .and() 
	     	.logout()                                   (1) 
	     	.logoutUrl("/logout")                       (2) 
	     	.logoutSuccessUrl("/login‐view?logout")     (3) 
	     	.logoutSuccessHandler(logoutSuccessHandler) (4) 
	     	.addLogoutHandler(logoutHandler)            (5) 
	     	.invalidateHttpSession(true);               (6) 
}

(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用。
(2)设置触发退出操作的URL (默认是 /logout )。
(3)退出之后跳转的URL,默认是 /login?logout 。
(4)定制的LogoutSuccessHandler,用于实现用户退出成功时的处理,如果指定了这个选项那么 logoutSuccessUrl() 的设置会被忽略。 (5)添加一个LogoutHandler,用于实现用户退出时的清理工作默认SecurityContextLogoutHandler会被添加为最后一个LogoutHandler 。
(6)指定是否在退出时让 HttpSession无效,默认设置为 true。

注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF,必须使用 post方式请求/logout。

logoutHandler
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。

下面是Spring Security提供的一些实现:

  • PersistentTokenBasedRememberMeServices
    基于持久化token的RememberMe功能的相关清理。
  • TokenBasedRememberMeService 基于token的RememberMe功能的相关清理。
  • CookieClearingLogoutHandler 退出时Cookie的相关清理。
  • CsrfLogoutHandler 负责在退出时移除csrfToken。
  • SecurityContextLogoutHandler 退出时SecurityContext的相关清理。

链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如deleteCookies()。

6.授权

1.概述

授权的方式包括 web授权和方法授权,web授权是通过 url拦截进行授权,方法授权是通过 方法拦截进行授权。他 们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterceptor;若为方 法授权则拦截器为MethodSecurityInterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方 法授权,最后决策通过,则允许访问资源,否则将禁止访问。

类关系如下:

Spring Security现在还有必要学吗_spring_20

2.准备环境

1.数据库环境

在t_user数据库创建如下表:
角色表:

CREATE TABLE `t_role` ( 
	`id` varchar(32) NOT NULL,
	 `role_name` varchar(255) DEFAULT NULL, 
	 `description` varchar(255) DEFAULT NULL, 
	 `create_time` datetime DEFAULT NULL,
	 `update_time` datetime DEFAULT NULL,
	 `status` char(1) NOT NULL,
	  PRIMARY KEY (`id`), 
	  UNIQUE KEY `unique_role_name` (`role_name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_role`(`id`,`role_name`,`description`,`create_time`,`update_time`,`status`) values ('1','管理员',NULL,NULL,NULL,'');

用户角色关系表:

CREATE TABLE `t_user_role` ( 
	`user_id` varchar(32) NOT NULL,
	`role_id` varchar(32) NOT NULL,
	`create_time` datetime DEFAULT NULL,
	`creator` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`user_id`,`role_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_user_role`(`user_id`,`role_id`,`create_time`,`creator`) values ('1','1',NULL,NULL);

权限表:

CREATE TABLE `t_permission` ( 
	`id` varchar(32) NOT NULL, 
	`code` varchar(32) NOT NULL COMMENT '权限标识符',
	`description` varchar(64) DEFAULT NULL COMMENT '描述',
	`url` varchar(128) DEFAULT NULL COMMENT '请求地址', PRIMARY KEY (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_permission`(`id`,`code`,`description`,`url`) values ('1','p1','测试资源 1','/r/r1'),('2','p3','测试资源2','/r/r2');

角色权限关系表:

CREATE TABLE `t_role_permission` ( 
	`role_id` varchar(32) NOT NULL,
	`permission_id` varchar(32) NOT NULL,
	PRIMARY KEY (`role_id`,`permission_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_role_permission`(`role_id`,`permission_id`) values ('1','1'),('1','2');
2.修改UserDetailService

1、修改dao接口
在UserDao中添加:

//根据用户id查询用户权限
public List<String> findPermissionsByUserId(String userId){
    String sql = "SELECT * FROM t_permission WHERE id IN(\n" +
            "\n" +
            "SELECT permission_id FROM t_role_permission WHERE role_id IN(\n" +
            "  SELECT role_id FROM t_user_role WHERE user_id = ? \n" +
            ")\n" +
            ")\n";

    List<PermissionDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>
    (PermissionDto.class));	
    List<String> permissions = new ArrayList<>();
    list.forEach(c -> permissions.add(c.getCode()));
    return permissions;
}

2、修改UserDetailService

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    @Autowired
    UserDao userDao;

    //根据 账号查询用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //将来连接数据库根据账号查询用户信息
        UserDto userDto = userDao.getUserByUsername(username);
        if(userDto == null){
            //如果用户查不到,返回null,由provider来抛出异常
            return null;
        }
        //根据用户的id查询用户的权限
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        //将permissions转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        UserDetails userDetails = User.withUsername(userDto.getUsername())
        .password(userDto.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}

3.web授权

在上面例子中我们完成了认证拦截,并对/r/**下的某些资源进行简单的授权保护,但是我们想进行灵活的授权控 制该怎么做呢?通过给 http.authorizeRequests() 添加多个子节点来定制需求到我们的URL,如下代码:

@Override 
protected void configure(HttpSecurity http) throws Exception { 
	http 
		.authorizeRequests()                                                      (1) 
		.antMatchers("/r/r1").hasAuthority("p1") 								  (2) 
		.antMatchers("/r/r2").hasAuthority("p2") 								  (3) 
		.antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')") (4)
		.antMatchers("/r/**").authenticated() 									  (5)  
		.anyRequest().permitAll() 												  (6) 
		.and() 
		.formLogin() 
		// ... 
}

(1) http.authorizeRequests() 方法有多个子节点,每个macher按照他们的声明顺序执行。
(2)指定"/r/r1"URL,拥有p1权限能够访问 。
(3)指定"/r/r2"URL,拥有p2权限能够访问。
(4)指定了"/r/r3"URL,同时拥有p1和p2权限才能够访问。
(5)指定了除了r1、r2、r3之外"/r/**"资源,同时通过身份认证就能够访问,这里使用SpEL(Spring Expression Language)表达式。 (6)剩余的尚未匹配的资源,不做保护。

注意: 规则的顺序是重要的,更具体的规则应该先写。 现在以 /admin/** 开始的所有内容都需要具有 ADMIN 角色的身份验证用 户,即使是 /admin/login 路径(因为 /admin/login 已经被 /admin/** 规则匹配,因此第二个规则被忽略)。

.antMatchers("/admin/**").hasRole("ADMIN") 
.antMatchers("/admin/login").permitAll()

因此,登录页面的规则应该在/ admin / **规则之前例如:
小范围的匹配要放在前面。

.antMatchers("/admin/login").permitAll() 
.antMatchers("/admin/**").hasRole("ADMIN")

保护URL常用的方法有:
authenticated() 保护URL,需要用户登录。
permitAll() 指定URL无需保护,一般应用与静态资源文件。
hasRole(String role) 限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较。
hasAuthority(String authority) 限制单个权限访问。
hasAnyRole(String… roles)允许多个角色访问。
hasAnyAuthority(String… authorities) 允许多个权限访问。
access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制。
hasIpAddress(String ipaddressExpression) 限制IP地址或子网。

4.方法授权

现在我们已经掌握了使用如何使用 http.authorizeRequests() 对web资源进行授权保护,从Spring Security2.0版 本开始,它支持服务层方法的安全性的支持。本节学习@PreAuthorize,@PostAuthorize, @Secured三类注解。

我们可以在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注释来启用基于注解的安全性。 以下内容将启用Spring Security的 @Secured 注释。

@EnableGlobalMethodSecurity(securedEnabled = true) 
public class MethodSecurityConfig {
	// ...
}

然后向方法(在类或接口上)添加注解就会限制对该方法的访问。 Spring Security的原生注释支持为该方法定义了 一组属性。 这些将被传递给AccessDecisionManager以供它作出实际的决定:

public interface BankService { 
	@Secured("IS_AUTHENTICATED_ANONYMOUSLY") 
	public Account readAccount(Long id);
	 
	@Secured("IS_AUTHENTICATED_ANONYMOUSLY") 
	public Account[] findAccounts(); 
	
	@Secured("ROLE_TELLER") 
	public Account post(Account account, double amount); 
}

以上配置标明readAccount、findAccounts方法可匿名访问,底层使用WebExpressionVoter投票器,可从 AffirmativeBased第23行代码跟踪。

post方法需要有TELLER角色才能访问,底层使用RoleVoter投票器。

使用如下代码可启用prePost注解的支持

@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class MethodSecurityConfig { 
	// ... 
}

相应Java代码如下:

public interface BankService {
	@PreAuthorize("isAnonymous()") 
	public Account readAccount(Long id); 
	
	@PreAuthorize("isAnonymous()") 
	public Account[] findAccounts(); 
	
	@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')") 
	public Account post(Account account, double amount); 
}

以上配置标明readAccount、findAccounts方法可匿名访问,post方法需要同时拥有p_transfer和p_read_account 权限才能访问,底层使用WebExpressionVoter投票器,可从AffirmativeBased第23行代码跟踪。