- 案例
- 新建工程,引入依赖
- 创建启动项和controller层
- 启动项目
- 自定义用户名和密码
- 配置文件中设置用户名和密码
- 关闭验证功能
- 默认用户认证模块涉及到的三个类
- UserDetailsService
- 返回值: UserDetails
- UserDetails的实现类之:User
- 方法参数
- 异常
- PasswordEncoder
- 接口介绍
- 内置解析器介绍
- BCryptPasswordEncoder 简介
- 代码演示
- 自定义登录逻辑
- 查看效果
- 自定义登录页面
- 编写登录页面
- 修改配置类
- 编写控制器
- 知识点: controller层中return "redirect:/main.html";可以重定向到在templates包外面的页面,默认return "main"是会进行视图解析器拼串,然后转发到templates包下面寻找对应的页面,没有找到就404
- 认证过程其他常用配置
- 失败跳转
- 编写页面error.html
- 修改表单配置
- 添加控制器的方法
- 设置error.html不需要认证
- 设置请求账户和密码的参数名
- 源码简介
- 修改配置
- 自定义登录成功处理器
- 源码分析
- 代码实现
- 自定义登录失败处理器
- 源码分析
- 代码实现
- 访问控制url匹配
- anyRequest()
- antMatcher()
- regexMatchers()
- 介绍
- 两个参数时使用方式----针对regexMatchers和antMatcher
- mvcMatchers()
- msp---servlet-path和context-path
- AntMatcher和MvcMatcher之间的区别
- 内置访问控制方法
- permitAll()
- authenticated()
- anonymous()
- denyAll()
- rememberMe()
- fullyAuthenticated()
- 角色权限判断
- hasAuthority(String)
- hasAnyAuthority(String ...)
- 基于角色控制访问
- hasRole(String)
- hasAnyRole(String ...)
- 基于ip控制访问
- hasIpAddress(String)
- 自定义403处理方案
- 新建类
- 修改配置类
- 基于表达式的访问控制
- access()方法使用
- 使用自定义方法
- 新建接口及实现类
- 修改配置类
- 基于注解的访问控制
- @Secured
- 开启注解,默认不启用注解配置
- @PreAuthorize/@PostAuthorize
- @PreAuthorize("hasRole('ROLE_abc')"),这里角色可以以ROLE_开头,也可以不以ROLE_开头,但是配置类不能以ROLE_开头
- RememberMe功能实现
- Thymeleaf中SpringSecurity的使用
- 获取属性
- 新建demo.html
- 权限判断
- 设置用户角色和权限
- 控制页面显示效果
- 退出登录
- logout其他常用配置源码解读
- SpringSecurity中的CSRF
- 什么是CSRF
- Spring Security中的CSRF
- 案例
- 请求头,ajax发送token
- CSRF相关源码解析
- 自定义RequestMatcher的实现类CsrfSecurityRequestMatcher
- post请求配置
- 源码解析---CsrfFilter的doFilterInternal方法
案例 新建工程,引入依赖
4.0.0 org.example SpringSecurity 1.0-SNAPSHOT 8 8 org.springframework.boot spring-boot-starter-parent 2.5.4 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.projectlombok lombok true org.springframework.boot spring-boot-devtools runtime true
创建启动项和controller层
@SpringBootApplication
public class main
{
public static void main(String[] args) {
SpringApplication.run(main.class,args);
}
}
@RestController
public class HelloController
{
@GetMapping("/hello")
public String hello()
{
return "hello spring security";
}
}
启动项目
启动日志会打印一个通过UUID随机生成的密码
访问controller,首先请求会被安全框架的aop机制拦截,要求使用用户名和密码验证登录
默认的用户名和密码为:
用户名: user
密码: 日志打印生成的uuid
自定义用户名和密码 配置文件中设置用户名和密码
spring:
security:
user:
name: 大忽悠
password: 123456
对应的绑定配置文件的类,如下:
@ConfigurationProperties(
prefix = "spring.security"
)
public class SecurityProperties {
public static final int BASIC_AUTH_ORDER = 2147483642;
public static final int IGNORED_ORDER = -2147483648;
public static final int DEFAULT_FILTER_ORDER = -100;
private final SecurityProperties.Filter filter = new SecurityProperties.Filter();
private final SecurityProperties.User user = new SecurityProperties.User();
public SecurityProperties() {
}
public SecurityProperties.User getUser() {
return this.user;
}
public SecurityProperties.Filter getFilter() {
return this.filter;
}
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List roles = new ArrayList();
private boolean passwordGenerated = true;
public User() {
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
if (StringUtils.hasLength(password)) {
this.passwordGenerated = false;
this.password = password;
}
}
public List getRoles() {
return this.roles;
}
public void setRoles(List roles) {
this.roles = new ArrayList(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
public static class Filter {
private int order = -100;
private Set dispatcherTypes;
public Filter() {
this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Set getDispatcherTypes() {
return this.dispatcherTypes;
}
public void setDispatcherTypes(Set dispatcherTypes) {
this.dispatcherTypes = dispatcherTypes;
}
}
}
关闭验证功能
主配置类中排除安全框架的配置
//排除security的配置,不启用
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class main
{
public static void main(String[] args) {
SpringApplication.run(main.class,args);
}
}
默认用户认证模块涉及到的三个类 UserDetailsService
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
返回值: UserDetails
返回值 UserDetails 是一个接口,定义如下
public interface UserDetails extends Serializable {
//获取用户权限
Collection extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
//账号是否未过期
boolean isAccountNonExpired();
//账号是否未被锁定
boolean isAccountNonLocked();
//凭证是否未过期,凭证就是密码
boolean isCredentialsNonExpired();
//用户是否启用状态
boolean isEnabled();
}
UserDetails的实现类之:User
要想返回 UserDetails的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。对于我们只需要使用里面的 User类即可。注意 User 的全限定路径是:
org.springframework.security.core.userdetails.User此处经常和系统中自己开发的 User 类弄混。
在 User 类中提供了很多方法和属性。
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 550L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
.....
}
其中构造方法有两个,调用其中任何一个都可以实例化
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。
public interface PasswordEncoder {
//加密
String encode(CharSequence var1);
//匹配
boolean matches(CharSequence var1, String var2);
//二次加密
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
内置解析器介绍
BCryptPasswordEncoder 简介
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认 10.
代码演示新建测试方法BCryptPasswordEncoder 用法。
@SpringBootTest(classes = main.class)
public class Test
{
@org.junit.jupiter.api.Test
public void test()
{
//创建解析器
PasswordEncoder pw = new BCryptPasswordEncoder();
//对密码加密
String encode = pw.encode("123");
System.out.println(encode);
//判断原字符和加密后内容是否匹配
boolean matches = pw.matches("123", encode);
System.out.println("==================="+matches);
}
}
自定义登录逻辑
当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的UserDetailsService和 PasswordEncoder。但是 Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder实例。所以不能直接 new 对象。
编写配置类
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
自定义逻辑
在 Spring Security 中实现 UserDetailService 就表示为用户详情服务。在这个类中编写用户认证逻辑。
@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"));
}
}
其框架会把提交的密码使用我们定义的passwordEncode加密后调用**org.springframework.security.crypto.password.PasswordEncoder#matches**方法,与 返回的User中的密码进行比对。配对正常就验证通过。
查看效果重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到 login.html 页面。
自定义登录页面
虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。
编写登录页面login.html
Title
修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapter,并重写 configure 方法。
-
successForwardUrl():登录成功后跳转地址
-
loginPage() :登录页面
-
loginProcessingUrl:登录页面表单提交地址,此地址可以不真实存在。
-
antMatchers():匹配内容
-
permitAll():允许
@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();
}
}
/toMain请求不需要被放行,是因为tomain请求时登录成功后执行的,此时已经有了凭证,即登录成功过后执行的请求会被自动放行,而没登录之前的请求都会被拦截
编写控制器
@Controller
public class LoginController {
//该方法不执行
// @RequestMapping("/login")
// public String login(){
// System.out.println("登录方法");
// return "main.html";
// }
@RequestMapping("/toMain")
public String toMain()
{
return "redirect:/main.html";
}
}
知识点: controller层中return “redirect:/main.html”;可以重定向到在templates包外面的页面,默认return "main"是会进行视图解析器拼串,然后转发到templates包下面寻找对应的页面,没有找到就404
认证过程其他常用配置 失败跳转
表单处理中成功会跳转到一个地址,失败也可以跳转到一个地址。
编写页面error.html
Title
操作失败,请重新登录 跳转
修改表单配置
在配置方法中表单认证部分添加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 访问方式。
@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请求。
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postonly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
修改配置
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//登录失败后跳转页面,POST请求
.failureForwardUrl("/toError")
.usernameParameter("myusername")
.passwordParameter("mypassword");
修改login.html
自定义登录成功处理器 源码分析
使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler()方法进行控制成功后交给哪个类进行处理
public FormLoginConfigurersuccessForwardUrl(String forwardUrl) { this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl)); return this; }
ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String forwardUrl;
public ForwardAuthenticationSuccessHandler(String forwardUrl) {
//判断是否是合法的url
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
return "'" + forwardUrl + "' is not a valid forward URL";
});
//合法url就保存
this.forwardUrl = forwardUrl;
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//然后转发
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
当需要控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。
代码实现自定义类
新建类 MyAuthenticationSuccessHandler 编写如下:
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
//输出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()方法
public FormLoginConfigurerfailureForwardUrl(String forwardUrl) { this.failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl)); return this; }
ForwardAuthenticationFailureHandler 中也是一个请求转发,并在request 作用域中设置 SPRING_SECURITY_LAST_EXCEPTION的 key,内容为异常对象。
public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final String forwardUrl;
public ForwardAuthenticationFailureHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
return "'" + forwardUrl + "' is not a valid forward URL";
});
this.forwardUrl = forwardUrl;
}
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
//转发到指定的url
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
代码实现
新建控制器
新建 MyForwardAuthenticationFailureHandler 实现AuthenticationFailureHandler。在方法中添加重定向语句
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();
配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面。
http.authorizeRequests()
.anyRequest().authenticated()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//error.html不需要被认证
.antMatchers("/error.html").permitAll();
//所有请求都必须被认证,必须登录后被访问
//.anyRequest().authenticated();
anyrequest要放在后面,拦截所有请求,前面放的是需要放行的请求
antMatcher()
方法定义如下
public C antMatchers(String... antPatterns)
数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
规则如下:
-
?: 匹配一个字符
-
*:匹配 0 个或多个字符
-
** :匹配 0 个或多个目录
在实际项目中经常需要放行所有静态资源,下面演示放行 js 文件夹下所有脚本文件。
.antMatchers("/js*.js").permitAll()
regexMatchers() 介绍
使用正则表达式进行匹配。和 antMatchers()主要的区别就是参数,antMatchers()参数是 ant 表达式,regexMatchers()参数是正则表达式。
演示所有以.js 结尾的文件都被放行。
.regexMatchers( ".+[.]js").permitAll()两个参数时使用方式----针对regexMatchers和antMatcher
无论是antMatchers()还是regexMatchers()都具有两个参数的方法,其中第一个参数都是 HttpMethod,表示请求方式,当设置了HttpMethod后表示只有设定的特定的请求方式才执行对应的权限设置。
枚举类型 HttpMethod内置属性如下:
public enum HttpMethod {
GET,
HEAD,
POST,
PUT,
PATCH,
DELETE,
OPTIONS,
TRACE;
....
}
.antMatchers(HttpMethod.POST,"/error.html").permitAll()
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()
msp—servlet-path和context-path
MSP是对整个项目的地址增加前缀,也就是
http://localhost:8080/MSP前缀/你的具体的控制器接口
MVC-Servlet-Path的配置:
application.yml配置
spring:
# spring.mvc.servlet.path
mvc:
servlet:
path: /xxx
application.properties配置
spring.mvc.servlet.path = /xxx
除了MVC之外,其实SpringBoot还允许设置项目根路径:
server:
servlet:
context-path: /ctx-path
那么是不是应该连接起来的情况:
http://localhost:8080/ctx-path/MSP前缀/你的具体的控制器接口
AntMatcher和MvcMatcher之间的区别
- antMatcher(String antPattern)-允许配置HttpSecurity仅在匹配提供的蚂蚁模式时被调用。
- mvcMatcher(String mvcPattern)-仅允许在匹配提供的Spring MVC模式时调用HttpSecurity。
通常,mvcMatcher比antMatcher更安全。例如:
antMatchers("/secured") 仅匹配确切的 /secured URL
mvcMatchers("/secured") 匹配/secured以及/secured/,/secured.html,/secured.xyz
,因此更通用,还可以处理一些可能的配置错误。
mvcMatcher使用与Spring MVC用于匹配的规则相同(使用@RequestMapping注释时)。
如果Spring MVC无法处理当前请求,则将使用ant模式的合理默认值
内置访问控制方法
Spring Security 匹配了 URL 后调用了permitAll()表示不需要认证,随意访问。在 Spring Security 中提供了多种内置控制。
permitAll()permitAll()表示所匹配的 URL 任何人都允许访问。
authenticated()authenticated()表示所匹配的 URL 都需要被认证才能访问。
anonymous()anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中
denyAll()
denyAll()表示所匹配的 URL 都不允许被访问。
rememberMe()
被“remember me”的用户允许访问
fullyAuthenticated()
如果用户不是被 remember me 的,才可以访问
角色权限判断
除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求
hasAuthority(String)判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。下图中 admin和normal 就是用户的权限。admin和normal 严格区分大小写。
在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。
.antMatchers("/main1.html").hasAuthority("admin")
hasAnyAuthority(String …)
如果用户具备给定权限中某一个,就允许访问。
下面代码中由于大小写和用户的权限不相同,所以用户无权访问
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")
基于角色控制访问 hasRole(String)
如果用户具备给定角色就允许访问。否则出现 403。
参数取值来源于自定义登录逻辑 UserDetailsService实现类中创建 User 对象时给 User 赋予的授权。
在给用户赋予角色时角色需要以:ROLE_开头,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。
使用 hasRole()时参数也只写 abc 即可。否则启动报错。
给用户赋予角色:
在配置类中直接写 abc 即可。
.antMatchers("/main1.html").hasRole("abc")
hasAnyRole(String …)
如果用户具备给定角色的任意一个,就允许被访问
基于ip控制访问 hasIpAddress(String)
如果请求是指定的 IP 就运行访问。
可以通过 request.getRemoteAddr()获取 ip 地址。
需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
当浏览器中通过 localhost 进行访问时控制台打印的内容:
当浏览器中通过 127.0.0.1 访问时控制台打印的内容:
当浏览器中通过具体 ip 进行访问时控制台打印内容:
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
自定义403处理方案
使用 Spring Security 时经常会看见 403(无权限),默认情况下显示的效果如下:
而在实际项目中可能都是一个异步请求,显示上述效果对于用户就不是特别友好了。Spring Security 支持自定义权限受限。
新建类实现 AccessDeniedHandler
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{"status":"error","msg":"权限不足,请联系管理员!"}");
out.flush();
out.close();
}
}
修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
myAccessDeniedHandler 是在配置类中进行自动注入的。
//异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
基于表达式的访问控制 access()方法使用
之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)
可以通过access()实现和之前学习的权限控制完成相同的功能。
以 hasRole 和 和 permitAll 举例
使用自定义方法
虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。
判断登录用户是否具有访问当前 URL 权限。
新建接口及实现类MyService.java
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
public interface MyService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
MyServiceImpl.java
@Component
public class MyServiceImpl implements MyService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//取出主体
Object obj = authentication.getPrincipal();
//判断是否是UserDetails类或者其子实现类
if (obj instanceof UserDetails){
UserDetails userDetails = (UserDetails) obj;
//取出当前主体所拥有的权限集合
Collection extends GrantedAuthority> authorities = userDetails.getAuthorities();
//以url作为当前主体的权限进行判断
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
修改配置类
在 access 中通过@bean的id名方法(参数)的形式进行调用配置类中修改如下:
//url拦截
http.authorizeRequests()
//login.html不需要被认证
// .antMatchers("/login.html").permitAll()
.antMatchers("/login.html").access("permitAll")
// .antMatchers("/main.html").hasRole("abc")
.antMatchers("/main.html").access("hasRole('abc')")
//@bean的id名(方法参数1,方法参数2...)
.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)")
自定义登录逻辑处理:
@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,/main.html"));
}
}
此时访问/main.html,用户拥有这个url的访问权限,因此可以访问
基于注解的访问控制
在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报 500
这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。
访问所有请求前,都会先判断是否登录过,然后再对权限进行验证,如果没有权限访问,才会报出500错误
@Secured
@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。
开启注解,默认不启用注解配置
在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@documented
@import({GlobalMethodSecuritySelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
boolean prePostEnabled() default false;
boolean securedEnabled() default false;
boolean jsr250Enabled() default false;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
在控制器方法上添加@Secured 注解
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//url拦截
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//所有请求都必须被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}
@PreAuthorize/@PostAuthorize
-
@PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
-
@PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。
开启注解
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}
添加@PreAuthorize
在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式
@PreAuthorize("hasRole('ROLE_abc')")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
@PreAuthorize(“hasRole(‘ROLE_abc’)”),这里角色可以以ROLE_开头,也可以不以ROLE_开头,但是配置类不能以ROLE_开头
RememberMe功能实现
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问
添加依赖
pring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 Spring-JDBC。以后多使用 MyBatis 框架而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需要添加 MySQL 驱动
org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.1 mysql mysql-connector-java 8.0.18
配置数据源
在 application.properties 中配置数据源。请确保数据库中已经存在security数据库
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver spring.datasource.url= jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username= root spring.datasource.password= root
编写配置
RememberMeConfig.java
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
//如果不注释掉,那么多次启动,每一次都会执行一次建表语句,会报错
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
修改SecurityConfig.java
在SecurityConfig中添加RememberMeConfig和UserDetailsService实现类对象,并自动注入。
在 configure 中添加下面配置内容。
http.rememberMe()
//登录逻辑交给哪个对象
.userDetailsService(userService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
在客户端页面添加复选框
在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。
有效时间
默认情况下重启项目后登录状态失效了。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录。
http.rememberMe()
//失效时间,单位秒
.tokenValiditySeconds(120)
//登录逻辑交给哪个对象
.userDetailsService(userService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
每一次用户登录,通过token凭证,和登录的时间,当下一次用户访问时,取出数据库中上一次登录的时间,和失效时间比较,判断是否需要重新登录
Thymeleaf中SpringSecurity的使用
Spring Security 可以在一些视图技术中进行控制显示效果。例如:JSP或 Thymeleaf。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf作为视图展示技术。
Thymeleaf 对 Spring Security 的 支 持 都 放 在thymeleaf-extras-springsecurityX中,目前最新版本为 5。所以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖。。
org.thymeleaf.extras thymeleaf-extras-springsecurity5 org.springframework.boot spring-boot-starter-thymeleaf
在 html 页面中引入 thymeleaf 命名空间和 security 命名空间
获取属性
可以在html页面中通过sec:authentication=""获取
UsernamePasswordAuthenticationToken中所有 getXXX的内容,包含父类中的 getXXX的内容。
根据源码得出下面属性:
-
name:登录账号名称
-
principal:登录主体,在自定义登录逻辑中是 UserDetails
-
credentials:凭证
-
authorities:权限和角色
-
details:实际上是 WebAuthenticationDetails的实例。可以获取remoteAddress(客户端 ip)和 sessionId(当前 sessionId)
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
private final Collection authorities;
private Object details;
private boolean authenticated = false;
....
}
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 550L;
private final Object principal;
private Object credentials;
....
}
public class WebAuthenticationDetails implements Serializable {
private static final long serialVersionUID = 550L;
private final String remoteAddress;
private final String sessionId;
....
}
新建demo.html
在项目 resources 中新建 templates 文件夹,在 templates 中新建demo.html 页面
Title
登录账号:
登录账号:
凭证:
权限和角色:
客户端地址:
sessionId:
编写Controller
thymeleaf 页面需要控制转发,在控制器类中编写下面方法
@RequestMapping("/demo")
public String demo(){
return "demo";
}
权限判断 设置用户角色和权限
设定用户具有 admin,/insert,/delete 权限 ROLE_abc 角色。
return new User(username,password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc,/insert,/delete"));
控制页面显示效果
在页面中根据用户权限和角色判断页面中显示的内容
通过权限判断:
通过角色判断:
退出登录
用户只需要向 Spring Security 项目中发送/logout退出请求即可。
实现退出非常简单,只要在页面中添加/logout 的超链接即可。
退出登录
为了实现更好的效果,通常添加退出的配置。默认的退出 url 为/logout,退出成功后跳转到/login?logout
源码:
public final class LogoutConfigurer> extends AbstractHttpConfigurer , H> { private List logoutHandlers = new ArrayList(); //退出登录的处理器 private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler(); //退出成功后,跳转的url private String logoutSuccessUrl = "/login?logout"; //退出成功后的处理器 private LogoutSuccessHandler logoutSuccessHandler; //要退出登录需要发起的url---默认值如下 private String logoutUrl = "/logout"; //退出登录请求的url匹配器 private RequestMatcher logoutRequestMatcher; private boolean permitAll; private boolean customLogoutSuccess; private linkedHashMap defaultLogoutSuccessHandlerMappings = new linkedHashMap(); .... }
如果不希望使用默认值,可以通过下面的方法进行修改。
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
logout其他常用配置源码解读
addLogoutHandler(LogoutHandler)
默认是 contextLogoutHandler
//创建默认的退出成功处理器
private LogoutSuccessHandler createDefaultSuccessHandler() {
SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
//设置默认的退出成功后重定向的url
urlLogoutHandler.setDefaultTargetUrl(this.logoutSuccessUrl);
if (this.defaultLogoutSuccessHandlerMappings.isEmpty()) {
return urlLogoutHandler;
} else {
DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(this.defaultLogoutSuccessHandlerMappings);
//设置默认的退出成功处理器
successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
return successHandler;
}
}
SimpleUrlLogoutSuccessHandler
public class SimpleUrlLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
public SimpleUrlLogoutSuccessHandler() {
}
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//调用父类方法进行处理
super.handle(request, response, authentication);
}
}
AbstractAuthenticationTargetUrlRequestHandler 的handle方法:
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String targetUrl = this.determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
this.logger.debug(LogMessage.format("Did not redirect to %s since response already committed.", targetUrl));
} else {
//退出后,重定向到指定的url
this.redirectStrategy.sendRedirect(request, response, targetUrl);
}
}
LogoutFilter:专门来拦截/logout请求,然后使用处理器对当前登出请求进行处理
SecurityContextLogoutHandler : 处理器之一,还有上面介绍的处理器
public class SecurityContextLogoutHandler implements LogoutHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
//是否销毁 HttpSession 对象,默认为 true
private boolean invalidateHttpSession = true;
//是否清除认证状态,默认为 true
private boolean clearAuthentication = true;
public SecurityContextLogoutHandler() {
}
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (this.invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Invalidated session %s", session.getId()));
}
}
}
if (this.clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication((Authentication)null);
}
SecurityContextHolder.clearContext();
}
.....
}
处理完毕后,调用上面讲的退出成功处理器logoutSuccessHandler
也可以自己进行定义退出成功处理器。只要实现了LogoutSuccessHandler接口。与之前讲解的登录成功处理器和登录失败处理器极其类似。
SpringSecurity中的CSRF
从刚开始学习Spring Security时,在配置类中一直存在这样一行代码:http.csrf().disable();如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护。
从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。
什么是CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“oneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个 session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
Spring Security中的CSRF
从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
在默认配置下,即便已经登录了,页面中发起PATCH,POST,PUT和DELETE请求依然会被拒绝,并返回403,需要在请求接口的时候加入csrfToken才行。
案例
编写控制器方法,跳转到 templates 中 login.html 页面。
@RequestMapping("/showLogin")
public String showLogin(){
return "login";
}
新建login.html
红色部分是必须存在的否则无法正常登录。
Title
修改配置类
在配置类中注释掉 CSRF 防护失效
//关闭csrf防护 // http.csrf().disable();
请求头,ajax发送token
如果你使用了freemarker之类的模板引擎或者jsp,针对表单提交,可以在表单中增加如下隐藏域:
如果您使用的是JSON,则无法在HTTP参数中提交CSRF令牌。相反,您可以在HTTP头中提交令牌。一个典型的模式是将CSRF令牌包含在元标记中。下面显示了一个JSP示例:
head>
然后,您可以将令牌包含在所有Ajax请求中。如果您使用jQuery,可以使用以下方法完成此操作:
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$.ajax({
url:url,
type:'POST',
async:false,
dataType:'json', //返回的数据格式:json/xml/html/script/jsonp/text
beforeSend: function(xhr) {
xhr.setRequestHeader(header, token); //发送请求前将csrfToken设置到请求头中
},
success:function(data,textStatus,jqXHR){
}
});
CSRF相关源码解析 自定义RequestMatcher的实现类CsrfSecurityRequestMatcher
这个类被用来自定义哪些请求是不需要进行拦截过滤的。如果配置csrf,所有http请求都被会CsrfFilter拦截,而CsrfFilter中有一个私有类DefaultRequiresCsrfMatcher。
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet allowedMethods;
private DefaultRequiresCsrfMatcher() {
this.allowedMethods = new HashSet(Arrays.asList(new String[]{"GET", "HEAD", "TRACE", "OPTIONS"}));
}
public boolean matches(HttpServletRequest request) {
//返回false表示需要验证token
return !this.allowedMethods.contains(request.getMethod());
}
}
从这段源码可以发现,POST方法被排除在外了,也就是说只有GET|HEAD|TRACE|OPTIONS这4类方法会被放行,其它Method的http请求,都要验证_csrf的token是否正确,而通常post方式调用rest接口服务时,又没有_csrf的token,所以会导致我们的rest接口调用失败,我们需要自定义一个类对该类型接口进行放行。来看下我们自定义的过滤器:
public class CsrfSecurityRequestMatcher implements RequestMatcher {
private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/rest/.*", null);
@Override
public boolean matches(HttpServletRequest request) {
if(allowedMethods.matcher(request.getMethod()).matches()){
return false;
}
return !unprotectedMatcher.matches(request);
}
}
说明:一般我们定义的rest接口服务,都带上 /rest/ ,所以如果你的项目中不是使用的这种,或者项目中没有rest服务,这个类完全可以省略的。
post请求配置一般我们的项目中都有一个通用的jsp文件,就是每个页面都会引用的,所以我们可以在通用文件中做如下配置:
$.ajaxSetup的意思就是给我们所有的请求都加上这个header和token,或者放到form表单中。注意, _csrf这个要与spring security的配置文件中的配置相匹配,默认为_csrf。
源码解析—CsrfFilter的doFilterInternal方法
我们知道,既然配置了csrf,所有的http请求都会被CsrfFilter拦截到,所以看下CsrfFilter的源码就对原理一目了然了。这里我们只看具体过滤的方法即可:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if(missingToken) {//如果token为空,说明第一次访问,生成一个token对象
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
//把token对象放到request中,注意这里key是csrfToken.getParameterName()= _csrf,所以我们页面上才那么写死。
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//这个macher就是我们在Spring配置文件中自定义的过滤器,也就是GET,HEAD, TRACE, OPTIONS和我们的rest都不处理
if(!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if(actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if(!csrfToken.getToken().equals(actualToken)) {
if(this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if(missingToken) {
this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
从源码中可以看到,通过我们自定义的过滤器以外的post请求都需要进行token验证。
利用spring-security解决CSRF问题



