SpringSecurity简介
安全框架概述
什么是安全框架? 解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。
常用安全框架
- Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了
Spring IoC
,DI(控制反转Inversion of Control,DI:Dependency Injection 依赖注入)
和AOP(面向切面编程)
功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。 - Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。
Spring Security简介
概述
Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。
历史
Spring Security 以“The Acegi Secutity System for Spring”的名字始于2003年年底。其前身为 acegi 项目。起因是 Spring 开发者邮件列表中一个问题,有人提问是否考虑提供一个基于 Spring 的安全实现。限制于时间问题,开发出了一个简单的安全实现,但是并没有深入研究。几周后,Spring 社区中其他成员同样询问了安全问题,代码提供给了这些人。2004 年 1 月份已经有 20 人左右使用这个项目。随着更多人的加入,在 2004 年 3 月左右在 sourceforge 中建立了一个项目。在最开始并没有认证模块,所有的认证功能都是依赖容器完成的,而 acegi 则注重授权。但是随着更多人的使用,基于容器的认证就显现出了不足。acegi 中也加入了认证功能。大约 1 年后 acegi 成为 Spring子项目。在 2006 年 5 月发布了 acegi 1.0.0 版本。2007 年底 acegi 更名为Spring Security。
快速入门
导入依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.yjxxt</groupId> <artifactId>springsecurity-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springsecurity-demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!--spring security 组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--web 组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- test 组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
前端页面
login.html
<!DOCTYPE html> <html lang="en"> <head> <title>Title</title> </head> <body> <form action="/login" method="post"> 用户名:<input type="text" name="username" /><br/> 密码:<input type="password" name="password" /><br/> <input type="submit" value="登录" /> </form> </body> </html>
访问页面
导入spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
默认的 username 为 user,password 打印在控制台中。
在浏览器中输入账号和密码后会显示 login.html 页面内容。
UserDetailsService详解
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
返回值
返回值 UserDetails 是一个接口,定义如下
要想返回 UserDetails
的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。对于我们只需要使用里面的 User
类即可。注意 User 的全限定路径是:
org.springframework.security.core.userdetails.User
此处经常和系统中自己开发的 User 类弄混。
在 User 类中提供了很多方法和属性。
其中构造方法有两个,调用其中任何一个都可以实例化
UserDetails
实现类 User
类的实例。而三个参数的构造方法实际上也是调用 7 个参数的构造方法。
username
:用户名password
:密码authorities
:用户具有的权限。此处不允许为 null
此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password
和客户端传递过来的 password
进行比较。如果相同则表示认证通过,如果不相同表示认证失败。
authorities
里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”)
来创建authorities
集合对象的。参数是一个字符串,多个权限使用逗号分隔。
方法参数
方法参数表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username
,否则无法接收。
异常
UsernameNotFoundException
用户名没有发现异常。在loadUserByUsername
中是需要通过自己的逻辑从数据库中取值的。如果通过用户名没有查询到对应的数据,应该抛出UsernameNotFoundException
,系统就知道用户名没有查询到。
PasswordEncoder 密码解析器详解
Spring Security 要求容器中必须有PasswordEncoder
实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder
的bean对象。
接口介绍
encode()
:把参数按照特定的解析规则进行解析。matches()
:验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。upgradeEncoding()
:如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。
内置解析器介绍
在 Spring Security 中内置了很多解析器。
BCryptPasswordEncoder 简介
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt
强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认 10.
代码演示
新建测试方法BCryptPasswordEncoder 用法。
package com.yjxxt.springsecuritydemo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @SpringBootTest public class MyTest { @Test public void test(){ //创建解析器 PasswordEncoder pw = new BCryptPasswordEncoder(); //对密码加密 String encode = pw.encode("123"); System.out.println(encode); //判断原字符和加密后内容是否匹配 boolean matches = pw.matches("1234", encode); System.out.println("==================="+matches); } }
自定义登录逻辑
当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的UserDetailsService
和 PasswordEncoder
。但是 Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder
实例。所以不能直接 new 对象。
编写配置类
package com.yjxxt.springsecuritydemo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { @Bean public PasswordEncoder getPw(){ return new BCryptPasswordEncoder(); } }
自定义逻辑
在 Spring Security 中实现 UserDetailService 就表示为用户详情服务。在这个类中编写用户认证逻辑。
package com.yjxxt.springsecuritydemo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder pw; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1.查询数据库判断用户名是否存在,如果不存在抛出UsernameNotFoundException异常 if (!"admin".equals(username)){ throw new UsernameNotFoundException("用户名不存在"); } //2.把查询出来的密码(注册时已经加密过)进行解析,或直接把密码放入构造方法中 String password = pw.encode("123"); return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal")); } }
查看效果
重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到 login.html 页面。
自定义登录页面
虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。
编写登录页面
login.html
<!DOCTYPE html> <html lang="en"> <head> <title>Title</title> </head> <body> <form action="/login" method="post"> 用户名:<input type="text" name="username" /><br/> 密码:<input type="password" name="password" /><br/> <input type="submit" value="登录" /> </form> </body> </html>
修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapter,并重写 configure 方法。
successForwardUrl()
:登录成功后跳转地址loginPage()
:登录页面loginProcessingUrl
:登录页面表单提交地址,此地址可以不真实存在。antMatchers()
:匹配内容permitAll()
:允许
package com.yjxxt.springsecuritydemo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义登录页面 .loginPage("/login.html") //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl .loginProcessingUrl("/login") //登录成功后跳转页面,POST请求 .successForwardUrl("/toMain"); http.authorizeRequests() //login.html不需要被认证 .antMatchers("/login.html").permitAll() //所有请求都必须被认证,必须登录后被访问 .anyRequest().authenticated(); //关闭csrf防护 http.csrf().disable(); } @Bean public PasswordEncoder getPw(){ return new BCryptPasswordEncoder(); } }
编写控制器
package com.yjxxt.springsecuritydemo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * 登录 */ @Controller public class LoginController { //该方法不执行 // @RequestMapping("/login") // public String login(){ // System.out.println("登录方法"); // return "main.html"; // } /** * 成功后跳转页面 * @return */ @RequestMapping("/toMain") public String toMain(){ return "redirect:/main.html"; } }
认证过程其他常用配置
失败跳转
表单处理中成功会跳转到一个地址,失败也可以跳转到一个地址。
编写页面error.html
<!DOCTYPE html> <html lang="en"> <head> <title>Title</title> </head> <body> 操作失败,请重新登录 <a href= "/login.html">跳转</a> </body> </html>
修改表单配置
在配置方法中表单认证部分添加failureForwardUrl()
方法,表示登录失败跳转的 url。此处依然是 POST 请求,所以跳转到可以接收 POST请求的控制器/error中。
//表单提交 http.formLogin() //自定义登录页面 .loginPage("/login.html") //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl .loginProcessingUrl("/login") //登录成功后跳转页面,POST请求 .successForwardUrl("/toMain") //登录失败后跳转页面,POST请求 .failureForwardUrl("/toError");
添加控制器的方法
在控制器类中添加控制器方法,方法映射路径/error。此处要注意:由于是 POST 请求访问/error。所以如果返回值直接转发到 error.html 中,即使有效果,控制台也会报警告,提示 error.html 不支持 POST 访问方式。
/** * 失败后跳转页面 * @return */ @RequestMapping("/toError") public String toError(){ return "redirect:/error.html"; }
设置error.html不需要认证
http.authorizeRequests() //login.html不需要被认证 .antMatchers("/login.html").permitAll() //error.html不需要被认证 .antMatchers("/error.html").permitAll() //所有请求都必须被认证,必须登录后被访问 .anyRequest().authenticated();
设置请求账户和密码的参数名
源码简介
当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器。
usernamePasrameter
:账户参数名passwordParameter
:密码参数名postOnly=true
:默认情况下只允许POST请求。
修改配置
//表单提交 http.formLogin() //自定义登录页面 .loginPage("/login.html") //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl .loginProcessingUrl("/login") //登录成功后跳转页面,POST请求 .successForwardUrl("/toMain") //登录失败后跳转页面,POST请求 .failureForwardUrl("/toError") .usernameParameter("myusername") .passwordParameter("mypassword");
修改login.html
<form action="/login" method="post"> 用户名:<input type="text" name="myusername" /><br/> 密码:<input type="password" name="mypassword" /><br/> <input type="submit" value="登录" /> </form>
自定义登录成功处理器
源码分析
使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler()
方法进行控制成功后交给哪个类进行处理
ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。
当需要控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。
代码实现
自定义类
新建类 com.yjxxt.handler.MyAuthenticationSuccessHandler 编写如下:
package com.yjxxt.springsecuritydemo.handler; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private String url; public MyAuthenticationSuccessHandler(String url) { this.url = url; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //Principal 主体,存放了登录用户的信息 User user = (User) authentication.getPrincipal(); System.out.println(user.getUsername()); //输出null System.out.println(user.getPassword()); System.out.println(user.getAuthorities()); response.sendRedirect(url); } }
修改配置项
使用 successHandler()方法设置成功后交给哪个对象进行处理
//表单提交 http.formLogin() //自定义登录页面 .loginPage("/login.html") //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl .loginProcessingUrl("/login") //登录成功后跳转页面,POST请求 // .successForwardUrl("/toMain") //和successForwardUrl不能共存 .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com")) //登录失败后跳转页面,POST请求 .failureForwardUrl("/toError") .usernameParameter("myusername") .passwordParameter("mypassword");
自定义登录失败处理器
源码分析
failureForwardUrl()内部调用的是failureHandler()
方法
ForwardAuthenticationFailureHandler 中也是一个请求转发,并在request 作用域中设置 SPRING_SECURITY_LAST_EXCEPTION
的 key,内容为异常对象。
代码实现
新建控制器
新建 com.yjxxt.handler.MyForwardAuthenticationFailureHandler 实现AuthenticationFailureHandler。在方法中添加重定向语句
package com.yjxxt.springsecuritydemo.handler; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler { private String url; public MyForwardAuthenticationFailureHandler(String url) { this.url = url; } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.sendRedirect(url); } }
修改配置类
修改配置类中表单登录部分。设置失败时交给失败处理器进行操作。failureForwardUrl
和 failureHandler
不可共存
//表单提交 http.formLogin() //自定义登录页面 .loginPage("/login.html") //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl .loginProcessingUrl("/login") //登录成功后跳转页面,POST请求 // .successForwardUrl("/toMain") //和successForwardUrl不能共存 .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com")) //登录失败后跳转页面,POST请求 // .failureForwardUrl("/toError") .failureHandler(new MyForwardAuthenticationFailureHandler("/error.html")) .usernameParameter("myusername") .passwordParameter("mypassword");
访问控制url匹配
在前面讲解了认证中所有常用配置,主要是对http.formLogin()
进行操作。而在配置类中 http.authorizeRequests()
主要是对url进行控制,也就是我们所说的授权(访问控制)。http.authorizeRequests()
也支持连缀写法,总体公式为:
- url 匹配规则.权限控制方法
通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。这些内容进行各种组合就形成了Spring Security中的授权。
在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面。
anyRequest()
在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证。
.anyRequest().authenticated();
antMatcher()
方法定义如下
public C antMatchers(String... antPatterns)
参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
规则如下:
?
: 匹配一个字符*
:匹配 0 个或多个字符**
:匹配 0 个或多个目录
在实际项目中经常需要放行所有静态资源,下面演示放行 js 文件夹下所有脚本文件。
.antMatchers("/js/**","/css/**").permitAll()
还有一种配置方式是只要是.js 文件都放行
.antMatchers("/**/*.js").permitAll()
regexMatchers()
介绍
使用正则表达式进行匹配。和 antMatchers()
主要的区别就是参数,antMatchers()
参数是 ant 表达式,regexMatchers()
参数是正则表达式。
演示所有以.js 结尾的文件都被放行。
.regexMatchers( ".+[.]js").permitAll()
两个参数时使用方式
无论是antMatchers()
还是regexMatchers()
都具有两个参数的方法,其中第一个参数都是 HttpMethod
,表示请求方式,当设置了HttpMethod
后表示只有设定的特定的请求方式才执行对应的权限设置。
枚举类型 HttpMethod
内置属性如下:
mvcMatchers()
mvcMatchers()适用于配置了 servletPath 的情况。
servletPath
就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在 application.properties 中添加下面内容设置 ServletPath
spring.mvc.servlet.path=/yjxxt
在 Spring Security 的配置类中配置.servletPath()
是 mvcMatchers()返回值特有的方法,antMatchers()和 regexMatchers()没有这个方法。在servletPath()
中配置了servletPath
后,mvcMatchers()直接写 SpringMVC 中@RequestMapping()中设置的路径即可。
.mvcMatchers("/demo").servletPath("/yjxxt").permitAll()
如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效
.antMatchers("/yjxxt/demo").permitAll()