3.3.4 默认根路径请求
在WebConfig.java中添加默认请求根路径跳转到/login ,此url为spring security提供:
package com.uncle.security.springmvc.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.stereotype.Controller; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; /** * @program: security-springmvc * @description: * @author: 步尔斯特 * @create: 2021-07-22 21:34 */ @Configuration//就相当于springmvc.xml文件 @EnableWebMvc @ComponentScan(basePackages = "com.uncle.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; } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("redirect:/login"); } }
spring security默认提供的登录页面。
3.3.5 认证成功页面
在安全配置中,认证成功将跳转到/login-success ,代码如下:
package com.uncle.security.springmvc.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; /** * @program: spring-security-springmvc * @description: * @author: 步尔斯特 * @create: 2021-07-23 00:41 */ @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/**").authenticated()//所有/r/**的请求必须认证通过 .anyRequest().permitAll()//除了/r/**,其它的请求可以访问 .and() .formLogin()//允许表单登录 .successForwardUrl("/login-success");//自定义登录成功的页面地址 } }
spring security支持form表单认证,认证成功后转向/login-success。
在 Logincontroller 中定义/login-success:
package com.uncle.security.springmvc.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; /** * @program: security-springmvc * @description: * @author: 步尔斯特 * @create: 2021-07-22 23:33 */ @RestController public class LoginController { @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"}) public String loginSuccess(){ return " 登录成功"; } }
3.3.6 测试
启动项目,访问http://localhost:8080/spring-security-springmvc路径地址
页面会根据WebConfig中addViewControllers配置规则,跳转至/login , /login是Spring
Security提供的登录页面。
登录
输入错误的用户名、密码
输入正确的用户名、密码,登录成功
退出
请求/logout退出
退出后再访问资源自动跳转到登录页面
3.4 授权
实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授权实现方法。
在LoginController 添加/r/r1 或/r/r2
package com.uncle.security.springmvc.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; /** * @program: security-springmvc * @description: * @author: 步尔斯特 * @create: 2021-07-22 23:33 */ @RestController public class LoginController { @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"}) public String loginSuccess(){ return " 登录成功"; } /** * 测试资源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中配置授权规则:
package com.uncle.security.springmvc.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; /** * @program: spring-security-springmvc * @description: * @author: 步尔斯特 * @create: 2021-07-23 00:41 */ @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");//自定义登录成功的页面地址 } }
测试
登录成功
访问/r/r1和/r/r2 ,有权限时则正常访问,否则返回403 (拒绝访问)
四、Spring Security
4.1 集成 SpringBoot
4.1.1 Spring Boot 简介
Spring Boot是一套Spring的快速开发框架,基于Spring 4.0设计,使用Spring Boot开发可以避免一些繁琐的工程配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持 SpringBoot开发,例如:MyBatis、Dubbo等,Spring 家族更是如此,例如:Spring Cloud、Spring mvc、Spring Security等,使用Spring Boot开发可以大大得高生产率,所以Spring Boo的使用率非常高。
本节讲解如何通过Spring Boot开发Spring Security应用,SpringBoot提供spring-boot-starter-security用于开发Spring Security应用。
4.1.2 创建maven工程
创建maven工程结构如下:
引入以下依赖
<?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.uncle</groupId> <artifactId>spring-boot-security</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>
4.1.3 spring容器配置
SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器。
Spring Boot配置文件
在resources下添加application.yml,内容如下:
server: #端口 port: 8080 #应用的上下文路径,也可以称为项目路径,是构成url地址的一部分 servlet: context-path: /spring-boot-security #项目名 spring: application: name: spring-boot-security #默认的配置为/templates/和.html #这里笔者就不用jsp了,前面用jsp旨在让读者理解配置前缀和后缀 #spring.mvc.view.prefix=/WEB-INF/view/ #spring.mvc.view.suffix=.jsp
Spring Boot 启动类
package com.uncle.seciruty.springboot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-07-23 19:35 */ @SpringBootApplication public class SecuritySpringBootApp { public static void main(String[] args) { SpringApplication.run(SecuritySpringBootApp.class,args); } }
4.1.4 Servlet Context配置
由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc与@ComponentScan
WebConfig如下
package com.uncle.seciruty.springboot.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-07-23 19:38 */ @Configuration//就相当于springmvc.xml文件 public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("redirect:/login"); } }
关于视图解析器
#默认的配置为/templates/和.html
#这里笔者就不用jsp了,前面用jsp旨在让读者理解视图解析器的配置
#spring.mvc.view.prefix=/WEB-INF/view/
#spring.mvc.view.suffix=.jsp
4.1.5 安全配置
由于Spring boot starter自动装配机制,这里无需使用@EnableWebSecurity
WebSecurityConfig内容如下
@Configuration 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");//自定义登录成功的页面地址 } }
4.1.6 测试
Logincontroller的内容
package com.uncle.seciruty.springboot.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-07-23 19:41 */ @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"}) public String r1(){ return getUsername()+" 访问资源1"; } /** * 测试资源2 * @return */ @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"}) public String r2(){ return getUsername()+" 访问资源2"; } }
测试过程
不出意外的话,此时应该是报错了
原因:
这是因为添加了数据库组件,所以autoconfig会去读取数据源配置,而新建的项目还没有配置数据源URL地址错误,所以会导致异常出现。
解决方案:
在启动类的@EnableAutoConfiguration或@SpringBootApplication中添加exclude ={DataSourceAutoConfiguration.class},排除此类的autoconfig,启动以后就可以正常运行。
package com.uncle.seciruty.springboot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; /** * @program: spring-boot-security * @description: * @author: 步尔斯特 * @create: 2021-07-23 19:35 */ @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class SecuritySpringBootApp { public static void main(String[] args) { SpringApplication.run(SecuritySpringBootApp.class,args); } }
接下来,我们开始正式测试
1、测试认证
2、测试退出
3、测试授权
4.2 工作原理
4.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过滤器链结构图:
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的关系图:
spring Security功能的实现主要是由一系列过滤器链相互配合完成
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder,在请求完成后将SecurityContextHolder持有的Securitycontext再保存到配置好的 SecurityContextRepository ,同时清除SecurityContextHolder 所持有的SecurityContext
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和
AuthenticationFailureHandler,这些都可以根据需求做相关改变
Filtersecurityinterceptor
Filtersecurityinterceptor是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问
ExceptionTranslationFilter
ExceptionTranslationFilter能够捕获来自FilterChain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和 AccessDeniedException ,其它的异常它会继续抛出。
4.2.2 认证流程
流程图
认证过程分析:
1.用户提交用户名 密码被 SecurityFilterChain 中的UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication ,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
3.认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除)Authentication实例。
4.SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口 ,也是发起认证的出发点,它的实现类为ProviderManager。而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
认证核心组件的大体关系如下: