SpringSecurity介绍
在前面博客中(上一个项目,具体在72章博客)
我们发现,若我们自己解决权限操作(或者说身份的验证,虽然并不明显)的话
是非常困难的,于是接下来我们说明一下如何更加简单的解决权限问题
SpringSecurity入门:
Spring Security简介:
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,它是用于保护基于Spring的应用程序的实际标准
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权
与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求
安全技术方案对比:
目前在整个Java开发的系统中,需要用于身份验证和访问控制框架的框架除了Spring Security,还有一个就是Apache shiro框架
Shiro:
Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密
如下是它所具有的特点:
易于理解的 Java Security API
简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等)
对角色的简单的鉴权(访问控制),支持细粒度的鉴权
支持一级缓存,以提升应用程序的性能
内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境
异构客户端会话访问
非常简单的加密 API
不跟任何的框架或者容器捆绑,可以独立运行
Spring Security:
除了不能脱离Spring,shiro的功能它都有,而且Spring Security对Oauth、OpenID也有支持
Shiro则需要自己手动实现,Spring Security的权限细粒度更高
Spring Security框架功能简介 :
认证:用户登录,解决的是"你是谁?"
授权:判断用户拥有什么权限,可以访问什么资源,解决的是"你能干什么?"
安全防护,防止跨站请求,session 攻击等
SpringSecurity应用场景:
用户登录,传统基于web开发的项目的登录功能
用户授权,在系统中用户拥有哪些操作权限
单一登录,一个账号只能在同一时间只能在一个地方进行登录,如果在其他地方进行第二次登录,则剔除之前登录操作
集成cas,做单点登录,即多个系统只需登录一次,无需重复登录
集成oauth2,做登录授权,可以用于app登录和第三方登录(QQ,微信等),也可以实现cas的功能
SpringSecurity入门案例:
快速体验SpringSecurity功能:
创建Spring Boot 工程(若不知道什么是spring boot,可以看看88章博客):
使用Spring Initializr 快速过构建Spring Boot工程:
一般来说,现在选择不了2.3.5(因为上面是以前的操作,可能一般是因为老的版本的idea或者以前的默认地址的原因,一般是默认地址造成的,借鉴学习时的图片)
现在根据我使用的如下(我的是有点新版的idea):
那么我们就选择2.7.2,只要不选择后面括号里的快照版即可,基本可以操作,这是最稳定的
至此,对应的目录如下(记得删除没有必要的文件):
现在我们在securitydemo包下创建controller包,并创建HelloSecurityController类:
package com.lagou.securitydemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
*/
@RestController
public class HelloSecurityController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
当然如果你没有学习spring boot,没有关系,可以到88章博客里面去学习,就知道为什么这样的操作了
接下来我们运行启动类,访问http://localhost:8080/hello,即可得到返回的数据
接下来整合SpringSecurity:
添加SpringSecurity依赖
<!--添加Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
重启Spring Boot启动类,再次访问http://localhost:8080/hello:
出现如下:
默认有访问/login,该/login会优先于写的controller的/login,即自己写的/login
所以这时候也最好不要在对应的控制层上加上/login了,因为基本没有什么作用
实际上过滤器产生的页面基本都会优先(SpringSecurity操作过滤器链),因为访问时,需要先经过过滤器
我们发现,引入该依赖后,有对应的表单(一般是代码操作的页面,很少是自带的生成,除非自己配置)
spring boot整合他时,会有的,当然里面的底层操作自然也是通过他们之间的整合方式进行
我们来观察下帮我们生成的表单页面:
咱们先看看这个页面源代码,这里有三点需要大家注意下:
表单的提交方式和路径:post,/login,与原来一样,使得可以证明操作是否成功,并随时判断是否返回当前页面
input输入项的name值:username password
隐藏域input的name值为:_csrf,value值为:d4329889-796a-447a-9d08-69e56bc7c296
该value值,一般是不同的,每次认证过,都是不同的,当然,若是浏览器有缓存(一般与sessionid对应)
在没有认证过之前,就会使用缓存的,若没有缓存,自然使用自己生成的
所以刷新(其中一般是带有参数的访问,即表单的参数,但因为缓存,所以不变,一般会有来源)或者重新访问一般是不会变的
SpringSecurity一般需要对应的这些value,即账号,密码,_csrf的值,都需要,且对应的值要正确,才会认证成功
_csrf的不正确一般会出现错误页面
默认d4329889-796a-447a-9d08-69e56bc7c296在生成时,是正确的,所以不需要修改
上面的操作,使得提交时,去进行验证(post,/login),从而决定是否通过认证
一般情况下,查看页面源代码是本身的页面信息,而不是去访问得到的,但有些时候,会去访问得到,且不经过浏览器的操作
而不是当前的元素页面内容,所以页面的源代码并不一定与当前页面代码相同
比如这里(当然,可能是因为查看源代码也会操作变量值的原因,所以导致又访问了)
这里可以看看对应的_csrf改变后,即提交后改变(提交要输入后面对应的用户名和密码)
在另外一个窗口查看元素(该窗口在认证界面),然后查看页面源代码,发现,页面源代码的 _csrf是改变的
而元素的 _csrf没有改变,我们试着进行提交,发现,使用是元素的,这是自然的,所以也就会出现报错,到错误页面
通过测试,只要我们认证成功,那么页面的源代码就会去访问一次
或者说,认证操作的参数(可能是sessionid变化造成的)可能会使得浏览器做出了某些操作使得页面源代码进行了重新的访问
但元素却没有,因为使用本身的,不操作访问
SpringBoot已经为SpringSecurity(实际上他自己提供)提供了默认配置,默认所有资源都必须认证通过才能访问
那么问题来了,此刻并没有连接数据库,也并未在内存中指定认证用户,如何认证呢:
其实SpringBoot(他们自己结合的依赖,Spring Boot只是负责装配罢了,所以实际上还是SpringSecurity提供)已经提供了默认用户名user,密码在项目启动时随机生成,如图:
认证通过后(输入用户名和密码),就访问了处理器资源(没拦截了),即返回了对应的hello:
也就是说,对应的登录实际上是因为SpringSecurity给出的拦截(或者说过滤器的操作),使得访问时需要认证等等
一般来说对应的页面是代码生成的,可能也是自带的(上面已经说明了一次,当然这并不需要考虑)
SpringSecurity认证:
案例介绍:
现在为了操作该SpringSecurity认证,现在给出一个半成品的项目
地址如下(包含数据库文件):
链接:https://pan.baidu.com/s/13769wUhvlos9WdBwf1jv0A
提取码:alsk
在这之前,先看看对应的配置文件里的数据库的配置是否正确
当然,若有乱码,自己将对应文件的编码修改成当前idea显示的编码即可
如我这里:
一般这修改对应的编码为gbk(因为我的idea显示的就是gbk),当然该编码问题并不需要解决,因为基本只是显示而已
在执行sql语句之前,需要执行如下sql:
CREATE DATABASE security_management CHARACTER SET utf8
因为对应的语句,并没有创建数据库来操作,所以需要自己创建,否则一般会默认第一个数据库
至此,执行对应的sql语句,运行启动类,访问如下地址,可以看到如下
下面一般验证码的右边会出现一个图片的标志(因为没有找到,会显示一个对应的标志)
如:
我们将这个简称为没图标志,后面操作图形验证码时,对多次的讲到
但可能是因为程序关闭导致点击后使得没有出现(一般会出现),具体看自己的显示:
启动项目进入首页(直接访问根目录即可,因为是半成品的):
系统管理界面:
基础数据界面(也就是商品管理):
项目最终目录结构:
工程导入:
注意检查: maven仓库配置
一般情况下,这个并不需要操作
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 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.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lagou</groupId>
<artifactId>lagou-security-management</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>lagou-security-management</name>
<description>lagou-security-management</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!--添加thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--添加web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--添加热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--添加lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--添加mp 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<!--添加mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<!--添加redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
至此,该项目的结构大致了解清楚了
SpringSecurity认证基本原理与认证2种方式:
在已导入的工程中添加Spring Security的依赖:
<!--添加Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在使用SpringSecurity框架,该框架会默认自动地替我们将系统中的资源进行保护
每次访问资源的时候都必须经过一层身份的校验,如果通过了则重定向到我们输入的url中,否则访问是要被拒绝的
这时我们再次进行登录,发现,在我们确定可以进入后
对应从数据库得到的信息或者是说对应的页面连接被拒绝访问了,这个后面进行解决
那么SpringSecurity框架是如何实现的呢:
Spring Security功能的实现主要是由一系列过滤器相互配合完成,也称之为过滤器链
过滤器链介绍:
过滤器可以看成是一种典型的AOP思想,因为在访问之前,中间进行了操作
下面简单了解下这些过滤器链,后续再源码剖析中在涉及到过滤器链在仔细讲解
在对应的日志中,可以找到如下:
可以找到如下15个过滤器类:
1:org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter:
根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调用处理拦截器
2:org.springframework.security.web.context.SecurityContextPersistenceFilter:
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存 或更新一个SecurityContext
SecurityContext:Security的上下文对象,一般保存对应的认证信息和权限信息等等
并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文
SecurityContext中存储了当前用户的认证以及权限信息
3:org.springframework.security.web.header.HeaderWriterFilter:
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4:org.springframework.security.web.csrf.CsrfFilter:
csrf又称跨域请求伪造, SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息
如果不包含,则报错,起到防止csrf攻击的效果
5:org.springframework.security.web.authentication.logout.LogoutFilter:
匹配URL为/logout的请求,实现用户退出,清除认证信息
6:org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter:
表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求
7:org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter:
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面
8:org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter:
由此过滤器可以生产一个默认的退出登录页面
9:org.springframework.security.web.authentication.www.BasicAuthenticationFilter:
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息
10:org.springframework.security.web.savedrequest.RequestCacheAwareFilter:
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
11:org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter:
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12:org.springframework.security.web.authentication.AnonymousAuthenticationFilter:
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中
spring security为了兼容未登录的访问,也走了一套认证流程, 只不过是一个匿名的身份
13:org.springframework.security.web.session.SessionManagementFilter:
securityContextRepository限制同一用户开启多个会话的数量
14:org.springframework.security.web.access.ExceptionTranslationFilter:
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
15:org.springframework.security.web.access.intercept.FilterSecurityInterceptor:
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限
从上面可以看出:Spring Security默认加载15个过滤器,但是随着配置可以增加一些过滤器(比如记住我)或者删除一些过滤器
通常一些配置会进行删除(如上面的CsrfFilter,DefaultLoginPageGeneratingFilter,DefaultLogoutPageGeneratingFilter)
当然也可以说有些会替换,等等,如DefaultLoginPageGeneratingFilter,DefaultLogoutPageGeneratingFilter
但他们的出现,却主要是因为对应没有使用了我们操作的访问认证页面而出现的
当然,若这时没有操作自定义的登录页面或者退出登录,那么实际上也会使用默认,因为他们只是覆盖而已(覆盖一样的)
有些配置会进行增加(但通常需要自己增加,如后面的图形验证)
这里就不做说明了,因为并没有什么意义
认证方式:
HttpBasic认证:
HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式
它的目的并不是保障登录验证的绝对安全,而是提供一种"防君子不防小人"的登录验证
在使用的Spring Boot早期版本为1 .X版本,依赖的Security是4.X版本,那么就无需任何配置
启动 项目访问则会弹出默认的httpbasic认证,现在使用的是spring boot2.0以上版本(依赖Security一般是5.X版本或者以上)
HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单模式
HttpBasic模式要求传输的用户名密码使用Base64模式进行加密
如果用户名是 “admin” , 密码是" admin",则将字符串"admin:admin" 使用Base64编码算法加密
加密结果可能是:
YWtaW46YWRtaW4=,HttpBasic模式真的是非常简单又简陋的验证模式, Base64的加密算法是可逆的,想要破解并不难
所以说他是一种简陋的验证模式,比如说如下(对应的页面或者提示并不是一成不变的,一般是版本的问题)
formLogin登录认证模式(简称为表单认证):
Spring Security的HttpBasic模式,该模式比较简单,只是进行了通过携带Http的Header进行简单的登录验证
而且没有定制的登录页面,所以使用场景比较窄
对于一个完整的应用系统,与登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式
甚至可以自己操作加密算法等
这就需要Spring Security支持我们自己定制登录页面,即操作表单认证模式(formLogin登录认证模式)
spring boot2.0以上版本(依赖Security是5.X版本或者以上)默认会生成一个登录页面
通常是这样的形式,就如前面的登录认证需要的页面类似的
Spring Security一般默认操作/login,且对应的页面也基本是代码生成,当然也会与版本有关
表单认证:
自定义表单登录页面:
一般情况下,对应Spring Security操作认证的/login路径,他会默认访问到对应的templates目录下的login.html文件
在lagou包下,创建config包,并编写SecurityConfiguration配置类并继承WebSecurityConfigurerAdapter类
主要是对应的WebSecurityConfigurerAdapter类的作用,使得我们可以进行操作设置:
package com.lagou.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.
builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//这里只需要对应的三个方法即可,并不需要都进行重写,配置类的注解是核心
//使得可以从ioc里得到,并进行注入操作(一般只能是得到WebSecurityConfigurerAdapter类型或者其子类)
//所以需要继承WebSecurityConfigurerAdapter类,从而操作如下的方法,当然这是底层的缘故
//若没有注入,自然对应的变量一般会设置为null了,实际上可能以前也有过说明,没有注入,就是null
//在正常情况下,没有注入是会报错的,但是却可以进行判断,然后赋值,具体方式,可以百度进行查找
//大体思路是,我们得到容器(当然这是很简单,一般底层都可以得到,要不然怎么操作注入呢)
//然后循环,判断是否是WebSecurityConfigurerAdapter类的父类,然后就决定是否赋值为null了
//所以我们在特殊情况下,我们也认为没有注入则是null
//当然,大多数都是报错的
//可能以前或者以后的博客并没有说明,但一般不会,注意即可,因为总不能不会忘记吧,或者出错吧
//当为null时,那么对应的方法也就操作不了,也就不会进行设置,或者使得初始化的信息等等
//如使得不操作默认认证(或者说认证页面)了
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth); //一般来说,重写父类的方法,一般会执行父类的方法
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//选择(开启)HttpBasic认证模式
//注意:分开的话(其他行的分开,不能是一行,否则在注释"//"后面的也会认为是注释内容)
//是可以使用注释的,不会影响运行,大多数的语言都可以
http.httpBasic() //开启HttpBasic认证
.and().authorizeRequests().anyRequest().authenticated();
//and()进行拼接作用
//authorizeRequests()授权我们的请求
//anyRequest()所有的请求(自然也包括静态资源的请求等等)
//authenticated()都需要认证
//即所有请求,都需要认证之后才可访问(即认证后进行授权,就可访问了,这时基本所有的请求都授权了)
}
}
//最后注意:这些方法的运行,在启动类还没启动完毕时进行初始化的,即,那时候进行执行,可以使用断点进行验证
一般情况下,默认是formLogin登录认证模式,当你加上上面的代码时,则使用对应方法的结果,而不会使用默认
那么当对应的方法没有任何操作时,那么也就没有任何认证
我们可以观测,对应继承了WebSecurityConfigurerAdapter类,在15个过滤器中,并不能找到对应的是该类的过滤器
实际上spring Security操作的认证会由WebSecurityConfigurerAdapter来决定
即他也是一个过滤器,对应的方法configure(HttpSecurity http)会执行
只是我们加上的,所以HttpSecurity是可以用来决定认证的
至此我们重新的运行启动类,最好清除浏览器的缓存
因为大多数的情况下,只要浏览器有对应的信息存在,就不需要认证,无论密码是否正确
这也就使得,接下来无论怎么访问,都不会出现对应的认证界面
因为在实际情况下,我们的认证方式基本是不会变的,所以也就使得,存在信息,就不需要认证
然后访问,就会出现如下(在不同的浏览器中,对应的页面弹框一般是显示不同的结果,你可以试一试):
至此你可以试着这样做:将对应的类的配置删除(配置类的配置),不让他是配置类,那么会发现,访问时使用默认的认证
若让他是配置类,但对应configure(HttpSecurity http)方法
什么都没操作,那么会发现,访问时没有认证,若有操作,就使用有操作的
接下来试验,不同的浏览器是否是真的不同页面弹框的显示,上面的是试验360浏览器的,当然也可以操作其他浏览器
接下来我们试验谷歌浏览器:
我们发现,的确,页面弹框的显示不同
但是我们也知道,对应的HttpBasic认证模式是简陋的,所以我们需要修改对应的代码,回到SecurityConfiguration类进行修改:
首先将对应的configure(HttpSecurity http)方法的内容都进行注释,加上如下代码:
http.formLogin() //开启表单认证
.and().authorizeRequests().anyRequest().authenticated();
//我们发现,除了认证的代码,后面都是一样的,当然了,对应的操作本来就是符合认证的,一样的也是正确的
清除浏览器缓存(即浏览器数据,简称为浏览器缓存,后面就这样说了)
发现,表单认证的结果就是之前的认证,也更加的证明了默认操作的就是表单认证:
但是我们说过,需要使用自定义的页面,实际上在没有操作时,大概是默认使用代码显示的页面
现在我们进行指定,那么就会使用指定的页面了
configure(HttpSecurity http)方法的内容代码进行改变,改变如下:
http.formLogin().loginPage("/login.html") //开启表单认证
.and().authorizeRequests().anyRequest().authenticated();
//我们发现,除了认证的代码,后面都是一样的,当然了,对应的操作本来就是符合认证的,一样的也是正确的
//其中loginPage("/login.html")将默认使用代码生成的页面(一般是这样),变成我们自己的页面
再次运行启动类,清除浏览器缓存,我们会发现如下(谷歌浏览器里面测试,其他浏览器可能没有具体提示,很难找到提示):
为什么会这样呢:
因为设置登录页面为login.html时,首先要知道,我们肯定是需要访问这个页面的
具体如何的访问,一般是重定向(而不是代码操作的页面了,因为设置的)
但是在重定向后,由于对应的配置过滤,是所有的请求
那么我们在访问途中,又需要过滤,然而这个过滤又回到了这个访问(又重定向了,因为设置为这个页面的)
所以说,就进行了多次的重定向
当然,实际上他并没有放行,所以虽然对应的资源不在静态文件夹下,即一般并不会出现没有页面的提示
总体来说,后面配置的是所有请求都登录认证(使得包括了自己的页面)
即陷入了死循环,所以需要将login.html放行不需要登录认证
这样重定向自己时,不会经过过滤:
具体的configure(HttpSecurity http)方法的内容代码如下:
http.formLogin().loginPage("/login.html") //开启表单认证
.and().authorizeRequests().
antMatchers("/login.html").permitAll() //放行对应的页面,而不经过验证过滤
.anyRequest().authenticated();
至此我们再次访问,但由于对应的进行了放行,那么就需要考虑对应的资源是否在静态资源文件夹下了
很明显,项目中并没有对应的资源文件,所以一般会出现如下(再次运行启动类,清除浏览器缓存):
即这里的spring boot整合thymeleaf 之后 所有的静态页面以放在resources/templates下面
所以得通过请求访问到模板页面,将/login.html修改为/toLoginPage,代码如下:
http.formLogin().loginPage("/toLoginPage") //开启表单认证,记得在对应的LoginController类里面进行操作
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()
//放行对应的页面,而不经过验证过滤,基本操作所有放行
.anyRequest().authenticated();
//其中记得都变成toLoginPage,因为放行需要对应,不对应的怎么会放行呢,是吧
但是这时候,我们发现,对应的样式却没有,如下(再次运行启动类,清除浏览器缓存):
因为访问login.html需要一些js , css , image等静态资源信息,正是因为我们只放行了对应的一个请求,但资源的请求没有放行
所以需要将静态资源放行,不需要认证,具体代码如下(另外一个方法了):
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题,基本只能操作认证的放行
web.ignoring().antMatchers("/css/**","/images/**","/js/**");
}
实际上也可以如下:
http.formLogin().loginPage("/toLoginPage") //开启表单认证
.and().authorizeRequests().
antMatchers("/toLoginPage","/css/**","/images/**","/js/**").permitAll()
//放行对应的页面,而不经过验证过滤
.anyRequest().authenticated();
甚至可以这样:
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage") //开启表单认证
.and().authorizeRequests()
//当然,对应的"."也可以放在这里(后面),只是不整齐而已,或者所有的点写后面,只是不美观
//这里就都写在前面了,后续也是如此
//放行对应的页面,而不经过验证过滤
.anyRequest().authenticated();
}
//也就是说,将放行都给configure(WebSecurity web)方法
//实际上该方法也可以用来操作资源的放行的(容易一点),且也可以操作某些安全控制(只是难一点而已)
//下面的configure(HttpSecurity http)方法也可以操作放行(容易一点)
//因为存在permitAll()方法,且必须要这个(难一点),否则后面的方法延续不能进行,会报错
//所以总体来说,WebSecurity web大于HttpSecurity http
//根据难易程度来说,操作安全控制,一般由HttpSecurity http来操作,放行,由WebSecurity web来操作
最后注意:对应的放行后面,需要以"/"开头
否则一般不会操作放行(因为不会加"/"来进行判断,使用的是静态资源文件夹的样式)
至此我们再次运行启动类,清除浏览器缓存,发现,对应的界面如下:
也就是说,使用了该页面,那么是不是所有的页面都可以,是的,只要你有,那么就会返回
因为认证的操作,主要是对应的字段提交的操作,只要对应的name是需要操作的提交字段即可
访问的对象也是,那么就可以操作认证,可以看看前面的源代码的地方就知道了
Spring Security中,安全构建器HttpSecurity和WebSecurity的区别是:
1:WebSecurity不仅通过HttpSecurity定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制
2:HttpSecurity仅用于定义需要安全控制的请求(当然HttpSecurity也可以指定某些请求不需要安全控制)
3:可以认为HttpSecurity是WebSecurity的一部分,的一个概念
4:构建目标不同
WebSecurity构建目标是整个Spring Security安全过滤器FilterChainProxy
HttpSecurity的构建目标仅仅是FilterChainProxy中的一个SecurityFilterChain
表单登录:
通过讲解过滤器链中我们知道有个过滤器UsernamePasswordAuthenticationFilter是处理表单登录的,操作post请求的/login的
那么下面我们来通过源码观察下这个过滤器:
在idea里使用ctrl+n,复制粘贴UsernamePasswordAuthenticationFilter进行搜索
点击对应的类(一般是唯一的,若不是唯一,看看前面的过滤器的总名称):
在源码中可以观察到,表单中的input的name值是username和password
并且表单提交的路径为/login,表单提交方式method为post,这些可以修改为自定义的值
代码如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
//可以理解为UsernamePasswordAuthenticationFilter就是操作formLogin(),后面的都是进行设置
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1") //设置表单提交的路径,而不是默认的/login了
//这样也可以访问/login1到对应的页面里面
.usernameParameter("username1") //设置对应用户名的name
.passwordParameter("password1") //设置对应密码的name
//上面的设置,使得前端的对应字段需要按照他这样的格式即可
//当然,若没有按照,那么自然后端返回的是null了
//也就不会认证成功了
.successForwardUrl("/")
//登录成功后,跳转的路径,一般情况下,我们访问时
//后端会得到对应的访问路径,使得跳转到对应的路径
//当然,我们可以指定路径的跳转,上面就是指定路径跳转到"/"下,这个项目就是index.html了
.and().authorizeRequests().anyRequest().authenticated();
}
对应的部分前端(对应的login.html):
需要按照后端的操作进行修改
运行启动类,访问,会发现,并没有认证成功,这时因为对应的name为_csrf的值并没有,自然后端得到的是null值
当然,你可以复制对应的值,即复制前面源代码的,如:
<input name="_csrf" type="hidden" value="4ac3b39d-7cc3-42bd-ac6c-51b5da93e454" />
<!--
其中4ac3b39d-7cc3-42bd-ac6c-51b5da93e454是认证之前或者清除缓存之前出现的值
一般每次的清除缓存或者认证之后(可能认证之后也是改变缓存),都会使用自己生成的,而不会使用缓存的
清除缓存:清除浏览器缓存,也叫做清除浏览器的数据
-->
所以也要记得,要在认证之前或者清除缓存之前操作,因为认证之后或者清除缓存之后,对应的值也就变化了
在他们之前操作,基本会使得认证成功,之前并没有说明他是干什么的,接下来说明一下他的作用:
name为_csrf的值主要是用来操作跨站请求的,主要是用来进行csrf防护
后面会进行说明,因为后面会说明对应的具体作用,先不要着急
那么如何不让Spring Security操作该作用呢,加上如下代码即可:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
//设置表单提交的路径,而不是默认的/login了,但是却不能直接的访问了
//默认操作post,所以操作get会访问失败,自然的又会回到该页面
.usernameParameter("username1") //设置对应用户名的name
.passwordParameter("password1") //设置对应密码的name
//上面的设置,使得前端的对应字段需要按照他这样的格式即可
//当然,若没有按照,那么自然后端返回的是null了
//也就不会认证成功了
.successForwardUrl("/")
//登录成功后,跳转的路径(是转发的操作,而不是重定向),一般情况下,我们访问时
//后端会得到对应的访问路径,使得跳转到对应的路径
//当然,我们可以指定路径的跳转,上面就是指定路径跳转到"/"下,这个项目就是index.html了
.and().authorizeRequests().anyRequest().authenticated();
//这下面就是加上的代码
//关闭csrf防护,会使得不会生成对应的_csrf值,所以前端得到值会出现报错,当然这是后面的操作
//即他并不是只有全部不防护的意思,而有直接的不操作_csrf值,使得基本不进行防护
http.csrf().disable();
//在没有其他的操作时,他一般会使得对应的过滤器删除,若有其他操作
//比如
//
//http.csrf().ignoringAntMatchers("/user/saveOrUpdate"); 后面会操作到
//那么不会删除对应的过滤器,即这时无论你是否关闭,即是否添加http.csrf().disable();,都不会删除
//当然,若什么都不写,对应的过滤器自然是存在的,即不会删除
//因为是默认的配置,或者说默认的添加上的(本来是0,最后是15个默认,那不就是添加吗)
//所以说,基本只有单独的http.csrf().disable();才会进行删除
}
至此我们就可以不需要name为_csrf的值,就可以认证了,可以试着运行启动类,访问即可,发现,的确认证成功,且进行了跳转
至此,总结一下,在没有配置之前
Spring Security默认的操作:
自带的代码操作的页面,使用/login可以访问到
对应默认认证路径是/login,其中该路径有代码生成页面的操作,看看是不是/login而给出页面
如果认证后,再次访问/login,进行认证,若失败了,则就使得认证重置(重定向,表单的认证失败),需要重新认证了
除非成功,一般成功后默认到自己进入的认证页面之前的路径(重定向,而不是转发)
默认post,默认username,默认password
可以直接的访问/login到对应自带的代码操作的页面
我们进行了设置:
自己写的页面,使用/toLoginPage可以访问到
对应默认认证路径是/login1,认证页面的访问变成了/toLoginPage(相当于/login,变成了/toLoginPage)
也就是说,设置后,就没有代码生成页面的操作了,而是自己的
即/login不能操作了(变成了/toLoginPage,即访问时,浏览器一般会提示没有该页面),没有该代码的生成,即访问什么就是什么
当然也要防止缓存,比如缓存在时,访问/login1会操作转发,因为我们认证成功了
即默认是认证成功的,但他只对当前窗口有效,在没有指定当前窗口有效的,一般认为对所有的窗口有效
之所以只对他一个窗口有效
前面的_csrf值的说明也是类似,但是他的单纯的访问,却因为正好存在,所以可以访问到,也刚好是一样的
主要是因为他(不是上面的说明)进行了有参数(表单的参数)的请求导致
并不是单纯的访问/login1,所以说是缓存的作用也是不为过的
当然,若再次回到/toLoginPage进行认证,若失败了,则就使得认证重置(重定向,表单的认证失败),需要重新认证了
默认post,默认username1,默认password1
可以直接的访问/toLoginPage到对应自己的页面
当然对应的代码生成页面,并不是绝对,可能是spring Security自带的页面,暂时规定是代码生成的
总结完毕,但是却又有一个问题,前面我说过,认证进入后,对应从数据库得到的信息或者是说对应的页面连接被拒绝访问了
如图,点击用户管理或者商品管理,鼠标到对应的中间的这个页面,就会出现如下
只有鼠标到了,才会有对应的提示"localhost拒绝了我们的连接请求":
这个时候又出现新的问题了,这个是什么原因呢,我们来看出现问题的具体是哪里:
找到该整体界面的html(就是项目里的index.html),对应的部分代码如下:
<li><a href="/user/findAll" target="right"><span class="icon-caret-right"></span>用户管理</a></li>
<li><a href="/product/findAll" target="right"><span class="icon-caret-right"></span>商品管理</a></li>
我们发现,对应的target指向的是right,找到对应的代码:
<iframe scrolling="auto" rameborder="0" src="images/bg.jpg" name="right" width="100%" height="100%"></iframe>
发现,指向上面的iframe,即发现行内框架iframe这里出现问题了
Spring Security下,X-Frame-Options默认为DENY,这也使得认证后,不会进行连接访问
非Spring Security环境下,X-Frame-Options的默认大多也是DENY
但这里可以试着将Spring Security依赖删除,发现可以连接访问,即是SAMEORIGIN情况,不属于大多数的
在DENY这种情况下,浏览器拒绝当前页面加载任何Frame页面
对应的设置含义如下:
DENY:浏览器拒绝当前页面加载任何Frame页面,此选择是默认的
SAMEORIGIN:frame页面的地址只能为同源域名下的页面(即不跨域的),通常正常的处理是这个,框架下基本默认都会是DENY
一般的iframe都不能操作跨域的页面,除非有特殊的操作,具体可以百度
允许iframe加载:
//关闭csrf防护
http.csrf().disable();
//加上下面的代码即可
//加载同源域名下的iframe页面
http.headers().frameOptions().sameOrigin();
这是,我们再次进行运行启动类,访问,可以看到对应的页面如下:
至此操作成功
基于数据库实现认证功能:
之前我们所使用的用户名和密码是来源于框架自动生成的,那么我们如何实现基于数据库中的用户名和密码功能呢
换言之就是如何使用我们自己的用户名和密码呢:
要实现这个得需要实现security的一个UserDetailsService接口,重写这个接口里面的loadUserByUsername方法即可
在项目的service包下面的impl包下
编写MyUserDetailsService类并实现UserDetailsService接口,然后重写loadUserByUsername方法:
package com.lagou.service.impl;
import com.lagou.domain.User;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
/**
*基于数据库完成认证
*/
@Service
//只有实现了他,才可以使得注入后当成参数传递,因为参数一般需要对应的UserDetailsService类型或者其子类
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
//只有该一个接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库得到信息,具体如何得到的,并不需要知道
//因为我们只需要学习对应的spring security,而不是学习该项目
User byUsername = userService.findByUsername(username);
if(byUsername == null){
throw new UsernameNotFoundException("用户没有找到,"+username);
}
//权限的集合,现在我们并不需要操作权限,所以设置为空的集合,但不要设置成null,否则报错
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{noop}"+byUsername.getPassword() //密码
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//"{noop}"+byUsername.getPassword()代表不使用密码加密
//实际上是{noop}代表了不使用的意思,而该参数正好是密码的参数,所以是不使用密码加密
return userDetails;
}
}
//注意:在认证时,该方法才会进行运行,使得操作密码的方式,当然,用户名需要自己进行验证
//比如说,我们将username修改成"username",会发现,还是可以认证成功
//所以用户名需要我们自己进行验证,对应的认证基本只会认证密码
//至此,如果我们重新运行启动类,没有清除缓存,即认为认证的,那么对应的UserDetails会根据缓存得到
//而不会执行该方法,所以这时该值不变,也就使得后面的得到用户名操作的值不会变
编写好后,对应的方法是如何执行呢,看如下
对应的SecurityConfiguration类的configure(AuthenticationManagerBuilder auth)方法以及对应的注入:
@Autowired
private UserDetailsService userDetailsService;
/**
* 身份安全管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService) //交给其管理,使得认证时,操作对应的方法
//我们输入用户名和密码时,当成参数提交,当然
//实际上对应的判断,只会判断密码,所以我们需要自己判断用户名)数据库判断)
//可以执行对应的方法,得到对应的数据库信息的返回结果进行处理(处理密码)
//总体使得只要用户名和密码可以被数据库里进行找到,那么对应的结果自然可以使得认证成功
//那么这个时候,就不会操作默认的用户名和密码了
//那么原来的默认用户名和密码是怎么来的呢:
//是对应的super.configure(auth)操作得到的(得到的也是默认)
//当我们有UserDetailsService的实例(前面的实现)
//那么就不会使得操作默认的用户名和密码,无是否有super.configure(auth)和是否有配置类
//而是操作对应的userDetailsService结果,即看看是否有对应的返回数据,从而完成认证
//当然,若也没有对应的auth.userDetailsService(userDetailsService)操作,对应的实例有无
//也是没有默认的操作
//那么基本不可能能够认证成功的,若没有配置类,且没有对应的实例,则操作默认的,否则什么都没有
//具体自己进行测试
}
我们知道这个配置类会被调用进行执行方法,那么对应的注入也自然可以进行注入
接下来我们看看数据库的信息:
我们将对应的密码加密的密码进行修改,直接修改成123456
因为我们现在并没有操作加密(不使用密码加密,上面设置的"{noop}")
这时运行启动类,清除浏览器缓存,可以发现日志并没有密码出现了,使用表里面的数据,对应的加密的密码也可以
因为在不使用加密操作时,对应的密码就是数据库的,而不会操作加密
若认证成功,即到达了对应的首页,则操作成功
密码加密认证:
在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加{noop}前缀
那么下面 Spring Security 中的密码编码进行一些探讨:
Spring Security 中PasswordEncoder就是我们对密码进行编码的工具接口,该接口只有两个功能:
一个是匹配验证,另一个是密码编码:
idea中,使用ctrl+n搜索PasswordEncoder,得到如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
对应的实现类有很多个,其他主要的两个操作密码的如下:
BCryptPasswordEncoder(加密操作)和NoOpPasswordEncoder(不使用加密操作,相当于操作了"{noop}"):
BCrypt算法介绍:
任何应用考虑到安全,绝不能明文的方式保存密码,密码应该通过哈希算法进行加密
有很多标准的算法比如SHA或者MD5,结合salt(盐,加上随机数,比如操作UUID等等)是一个不错的选择
Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码
BCrypt强哈希方法,每次加密的结果都不一样,所以更加的安全
bcrypt算法相对来说是运算比较慢的算法,在密码学界有句常话:越慢的算法越安全
黑客破解成本越高,通过salt和cost这两个值来减缓加密过程,它的加密时间(百ms级)远远超过md5(大概1 ms左右)
对于计算机来说, Bcrypt 的计算速度很慢,但是对于用户来说,这个过程不算慢
bcrypt是单向的,而且经过salt和cost的处理,使其受攻击破解的概率大大降低,同时破解的难度也提升不少
相对于MD5等加密方式更加安全,而且使用也比较简单
/*
bcrypt加密后的字符串形如:
$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
*/
其中$是分割符,无意义,2a是bcrypt加密版本号,10是cost的值,而后的前22位是salt值,再然后的字符串就是密码的密文了
这里的cost值即生成salt的迭代次数,默认值是10,推荐值12
那么分开来说就是:
$2a $10 $wouq9P/HNgvYj2jKtUN8rO JJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
在项目中使用BCrypt
首先看下PasswordEncoderFactories 密码器工厂(idea中,使用ctrl+n搜索PasswordEncoderFactories 即可找到):
部分代码:
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
上面是对应的前缀的作用,使得操作加密或者不加密的方式,在运行启动类时,会进行初始化,可以试着打断点进行查看
可以发现,在还没有启动完毕时,会执行
之前我们在项目中密码使用的是明文,即操作的是noop,代表不加密使用明文密码,现在用BCrypt只需要将noop换成bcrypt即可
修改后,如下:
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{bcrypt}"+byUsername.getPassword() //密码
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//"{bcrypt}"+byUsername.getPassword()代表使用密码加密
//当然,他并不是对他自己进行加密,而是使得他操作我们输入的值进行加密
//即该前缀是携带操作我们输入密码的加密方式的意思
为了得到一个加密的值,我们在对应的MyUserDetailsService类里进行测试,代码如下:
public static void main(String[] args) {
//使用加密的类,进行加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//得到123456加密后的值
String code = bCryptPasswordEncoder.encode("123456");
System.out.println(code); //$2a$10$qbhLCxg43v7bv0oPk1dNz.vXCL.qT4.kEmMRdTJlh/CFY9v.OwAtq
//注意:对应的值,多次的运行并不是一样的,因为有随机的存在
//每个随机数Spring Security会进行保存的,使得可以解密
//当然并不是一定会进行保存,因为解密的操作可能也是需要对应加密后的字符串的,使得得到值
//当然这里并不需要我们进行理解,只需要知道他能这样做即可
}
我们将生成的加密后的密码赋值到数据库里进行后续的运行操作
接下来,运行启动类,清除浏览器缓存,再次的输入用户名和密码进行操作,如果认证成功,则操作完毕
这里需要思考个问题,那么对应的"{noop}“和”{bcrypt}",可以不加吗,或者说有没有默认的:
通过试验发现,必须加上对应的值,否则会报错
即我们需要在前面加上对应的PasswordEncoderFactories类里面的createDelegatingPasswordEncoder方法里面的map集合的key的值
基本是这样,否则会报错
获取当前登录用户:
在传统web系统中,我们将登录成功的用户放入session中,在需要的时候可以从session中获取用户
那么Spring Security中我们如何获取当前已经登录的用户呢:
SecurityContextHolder:
保留系统当前的安全上下文SecurityContext,其中就包括当前使用系统的用户的信息
SecurityContext:
安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
代码实现:
找到UserController类,加上如下方法:
/**
* 获取当前登录的用户
*/
@GetMapping("/loginUser1")
@ResponseBody
public UserDetails getCurrentUser1(){
UserDetails principal =
(UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(principal);
return principal;
}
/**
* 获取当前登录的用户
*/
@GetMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser2(Authentication authentication){
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
return principal;
}
/**
* 获取当前登录的用户
*/
@GetMapping("/loginUser3")
@ResponseBody
//@AuthenticationPrincipal注解必须加上,否则报错
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails){
System.out.println(userDetails.getUsername());
System.out.println(userDetails.getPassword());
System.out.println(userDetails);
return userDetails;
}
//上面是三种方式获取用户名
可以得到对应数据库的用户名,但是密码不会给出,这是肯定的
remember me 记住我:
在大多数网站中,都会实现RememberMe这个功能,使得前端,方便用户在下一次登录时直接登录
避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现,下面我们来看下他的原理图:
简单的Token生成方法:
下面的RememberMeAuthenticationFilter主要是操作对应的remember me的过滤器,中间的是对应的底层操作,这些作用知道即可
并不需要死磕
Token=MD5(username+分隔符+expiryTime+分隔符+password,进行加密得到值),比如操作得到的值是:
YWRtaW46MTY2MTI0MDQzODEwNjpjMmZlZDdkZDY0YWNkNmJhZTZmNThjYjg2ZGY3MDE1YQ(举个例子)
注意:这种方式不推荐使用,有严重的安全问题,就是密码信息在前端浏览器cookie中存放
如果cookie被盗取很容易破解(MD5现在并不是很安全)
代码实现:
首先开启记住我(remember me)的功能:
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.and().rememberMe() //这里加上该代码,代表开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认设置为2周(1209600秒)
.rememberMeParameter("remember-mer")
//代表表单里对应的input的name值,一般默认设置为remember-me,这里就设置成remember-mer了
//基本是随便设置的
.and().authorizeRequests().anyRequest().authenticated();
前端页面需要增加remember-me的复选框(在对应的login.html下可以看到如下代码):
<div class="form-group">
<div>
<!--
"记住我"name一般为remember-me,一般要与上面设置的remember-mer一样
value值可选true yes 1 on 都行,后面的源码介绍,会知道为什么可以取这四个-->
<input type="checkbox" name="remember-mer" value="true"/>记住我
</div>
</div>
<!--
上面图片中,说明的remember-me的意思是与表单设置的name一致即可
只是大多数都是remember-me,所以就那样的写上去
-->
这时我们运行启动类,并清除浏览器缓存,访问根目录,即http://localhost:8080/,这时需要我们认证
先不点击"记住我"这个按钮,登录,然后退出浏览器,继续进入浏览器,访问根目录,即http://localhost:8080/
发现,还是需要认证,这时,我们点击记住我,登录后,退出浏览器,访问根目录,即http://localhost:8080/,这时不需要认证了
即我们直接的跳过了认证,查看对应的cookie,会发现,多出了一个cookie,名称是remember-me,可以发现
我们在设置表单名称时也最好设置为remember-me,虽然这里设置成了remember-mer,但也是为了规范
最好也是设置成remember-me,我之所以设置成remember-mer,是为了让后面的解释更加清楚,有异常的变化,好观察
我们仔细看看该cookie,看到对应的基本操作我们的根目录,即在当前根目录下可以使用该cookie
所以其他的窗口也是如此(虽然cookie在路径基本一致的情况下,会共享),如果忘记了cookie的基础,可以看看50章博客
至此可以知道,我们去访问时,该cookie会给服务器,由于对应的值中,保存了用户名和密码,通过反向的解密
然后进行认证,也就是对比,也就是密码的对比,即UserDetails里面的参数
通过前面的三个方式,不难得到,当然这是底层的操作
当然,一般都会认证成功的,因为我们并没有进行改变
当然他并不会操作设置的认证成功后的地址,而是自己的当前访问的地址
也就相当于我们输入了用户名和密码,从而使得认证成功
这时,我们设置退出浏览器关闭cookie时,会发现对应的访问根目录,即http://localhost:8080/,又需要认证了
即他是操作cookie来进行的,即每次的重新访问,会根据cookie而使得不用我们认证,且是我们之前操作的用户名
那么该cookie的值,如何而来,在我们传递对应的name为remember-mer时(当然也包括其他信息)
若对应的设置表单也是remember-mer,那么就会准备cookie返回给浏览器
否则不会准备返回给浏览器,也就相当于不操作"记住我",即也相当于没有点击"记住我"
即不同时,无论你是否点击"记住我",也没有对应的cookie,既然没有对应的cookie
那么就相当于对应的值是不对的,也就是对比失败,会使得没有认证
自然会重定向到认证页面,除非已经认证的缓存还在,当操作了该cookie时
要注意:操作这个cookie时,对应的用户名(对比UserDetails里面的)需要与你传递的用户名一致
否则会报错,使得认证不了,没有操作之前不会
之所以要这样,是防止用户名不一致的情况,因为他是帮我们跳过认证
所以对应的用户名会进行判断一次,防止不同,即严谨了些
但是,这个时候,虽然我们有对应的cookie,如果重新启动的话,还会自动认证吗:
通过试验,若你重新启动,那么对应的cookie认证是一般也可以成功,因为对应的对比会根据缓存创建(重新启动了)
会使得可以认证,缓存也可以说是cookie数据
当然若操作那个有参数(表单的参数)请求的(自然在这里通常是包括认证的)
则也是自然还是会到当前页面(对应的请求页面,也就是当前页面,刷新的当前页面,而不会重定向到认证)
但是若是没有缓存(退出浏览器)的其他的请求,则需要认证了,因为不是有参数(表单的参数)请求和有对应的对比
至此该操作我们进行完毕
最后注意一下:一些扩展基本与cookie是无关的
如保存网站的用户名和密码的操作,使得下次可以自动帮我们输入,一般浏览器都会自带的
是否退出浏览器或者是否清除缓存与他们无关
且没有使用remember-me时,对应的关闭浏览器后
再次的打开由于使得sessionid变化了(当然清除缓存,自然也会使得得到新的sessionid,一般也叫做sessionid值)
所以默认不会使得认证成功(判断使得),然后重定向到认证页面
而操作remember-me时,则会解析他的值,从而使得认证,从而得到信息
但是只要认证后,那么基本就不会操作remember-me了,因为已经认证了
使得有对应的信息,绑定对应的sessionid
基本只有sessionid的值改变了(该sessionid与认证的信息有关的,比如后面的账号在线数)
其他的cookie基本没有
可以自己查看认证成功后的sessionid的值,一般都会进行改变(自然对应的其他相同请求路径也会改变,刷新即可,这是cookie的原因)
即就可以相当于直接的访问了
当然清除缓存的话,自然也需要继续认证
总体来说就是:通过用户名和密码以及其他条件生成的token,当成cookie进行验证
只要对应的token存在,那么就基本不需要我们输入用户名和密码了
但是上面的我们基本只要关闭删除对应的cookie,那么就不会自动的认证了,如使得持久化呢:
持久化的Token生成方法:
存入数据库Token包含:
token:随机生成策略,每次访问都会重新生成
series:登录序列号,随机生成策略,用户输入用户名和密码登录时,该值重新生成,使用remember-me功能,该值保持不变
expiryTime:token过期时间
CookieValue(cookie的值)=encode(series+token)
代码实现:
在配置类SecurityConfiguration里面加上如下代码:
@Autowired
private DataSource dataSource;
/**
* 负责token与数据库之间的相关操作
* @return
*/
@Bean
public PersistentTokenRepository getpersistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); //设置数据源
//启动时帮助我们创建一张表,主要用来存放token的信息
//第一次启动设置为true,第二次启动设置为false,或者注释掉
//true代表创建表,false代表不创建,当然,注释的话,自然不会操作表
//因为若有对应的表的话,设置为true时,启动时会报错,说明表已经存在
//或者说,只要数据库里有表存在,就需要设置为false或者注释
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
修改配置类的部分configure(HttpSecurity http)方法:
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository()) //这里是添加的代码
.and().authorizeRequests().anyRequest().authenticated();
也就是说,在启动之前,即初始化之前,我们会进行创建一个对应存放token的表
接下来我们先观察数据库的表(删除掉对应的persistent_logins表,否则启动时会报错,说明表已存在)
然后运行启动类,看看数据库是否有对应的表创建,即该persistent_logins表是否创建,若创建则表示操作成功
对应的表信息如下:
创建好后,注释掉对应的代码,重新运行启动类,并清除浏览器缓存,点击"记住我",然后认证登录,查看数据库的数据
会发现,多了一条数据,注意:必须要点击"记住我",否则不会添加数据
因为他需要判断是否有携带设置的remember-me的字段,当然我们这里设置的是remember-mer
当然不一致的话,自然也不会操作"记住我",即也相当于没有点击"记住我",也使得不会操作持久化
总体来说就是:在前面操作的remember-me基础上,保存token的信息到数据库,实现持久化,每次的remember-me解析认证
都会使得数据库的信息发生变化,即我们关掉浏览器,再次访问(解析认证)
查看数据库,发现对应的token字段值和token过期时间值发生变化了,那么该值有什么作用呢:
接下来进行测试:
我们首先清空对应的数据库,重启启动类,清除浏览器缓存,进行登录认证,查看数据库是否创建
发现已经创建了,退出浏览器,方法根目录(http://localhost:8080/)
那么他是操作remember-me使得对比,还是操作数据库信息使得对比呢,在这之前,先看如下的试验数据:
接下来我们修改token字段的值,随便怎么修改,退出浏览器,继续访问根目录,发现需要你登录了
这时我们查看数据库的信息,发现
对应的那条数据被删除(且使得remember-me的cookie被删除,比如设置过期时间,使得当场失效)
这时我们再次认证登录,查看数据库的信息,又多出一条
那么修改过期时间怎么样,通过我的试验,对应的过期时间的修改并不会影响当时的认证(大概主要是给我们看的吧)
可能在某些时候有点作用
那么对应的series字段呢,通过我的试验,当我们修改他的值时,无论其他字段是否修改,都不会删除该数据
但也要重新认证,且remember-me的cookie也被删除,对应的用户名不同也是如此
从以上的试验可以得出结论,series和用户名是唯一的(不会删除),token是不唯一的(会删除),过期时间不用考虑
我们通过观察浏览器的token和数据库的信息,可以发现,并没有可以直观的看到联系
那么可以这样的理解(可能不对),浏览器得到的token虽然用来解析进行对比
解析的一般是操作密码的信息,因为用户名是保存的,然后进行对比信息
认证的对比,不是数据库对比,实际上用户名主要是帮我们得到对比信息,后面的源码会说明
虽然也会操作认证对比,测试即可,将对比的用户名修改成与原来的用户名不同,会发现,重定向到认证页面,没有后缀
但是在这之前,解析的值也需要与数据库进行对比查看,首先查询的是series值
如果series值存在,且对应的token值也正好正确,以及用户名对比成功(大概会检查是否改变,对比的检查)
那么才进行对比认证,如果token值不正确,则对比认证不成功,相当于使得没有认证,且删除该条数据
重新进行认证(相当于重定向到认证页面)
如果series值和用户名不存在,则直接对比不成功,那么就相当于没有认证过
重新认证(相当于重定向到认证页面),但不会进行删除
对比之后,解析为密码,这时用户名和密码都有了,那么就可以对比(认证的对比)了
由此可见,对应数据库的作用,是为了进一步的提高安全,实际上也可以说浏览器上的token是他们进行合并加密的
至此,也提高了安全的作用,因为需要多次的解密
也完成了保存(持久化)的作用,保存解密的在数据库里(虽然可能还是需要解密变成密码进行对比)
实际上也算是一种永久的身份令牌(要看如何使用)
而这样的令牌可以操作很多种情况,在生活中我们也可以知道,在某些应用中,可以使用微信进行登录
而不需要我们自己输入用户名和密码,只要你登录过了即可,这就是一种类似的情况
在以后,我们可以交给用户这个令牌,那么他就可以不需要用户名和密码登录了
虽然这里是cookie的形式,但这只是一种比喻而已
Cookie窃取伪造演示:
前面说过,放在cookie里容易被获取,无论是否持久化都是如此,接下来我们来进行演示
在postman里,运行如下:
我们可以发现,返回的是页面,自然,我们试验postman时相当于操作一个浏览器的
所以自然也是需要认证,即会返回页面
接下来清空persistent_logins表数据,清除浏览器缓存,重新认证,点击"记住我",然后查看对应的remember-me的cookie的值
假装这个值被窃取(容易被窃取的)
回到postman点击如下:
点击Cookies,到如下:
点击这个Add Cookie加上一个cookie,左边的remember-me就是如下的(这是保存后的)
否则一般显示"Cookie_对应的第几个创建"
一般重启postman会重置这些操作,如第几个创建的数字变为1,添加的删除等等
添加后并保存
至此我们再次去访问该路径,发现,并不需要认证了,因为对应的数据进行对比成功了,也就不会给出重新认证的页面了
如:
只要该值的解析可以对比成功,那么以后都可以对比成功,甚至可以有多个这样的值(只要解析可以对比成功)
那么如何避免这样的方式呢:
安全验证:
找到对应的UserController类的getById方法,我们假设他是重要的方法,即我们对重要的方法进行安全验证
虽然也可以对不重要的方法验证,但并不完全需要
进行修改:
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
//获取认证的信息,相当于可以得到org.springframework.security.core.userdetails.User的信息一样
//只是需要再次获取
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication);
System.out.println(RememberMeAuthenticationToken.class.isAssignableFrom(
authentication.getClass()) == true);
//如果返回true,代表这个登录认证的信息,来源于自动登录
if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass()) == true){
//一般来说,spring security在操作异常时,会重定向到认证页面,一般并不会打印异常的信息
//但是,这是交给spring security进行操作的异常
//如果我们自己进行捕获,如使用try,则不会重定向到认证页面
throw new RememberMeAuthenticationException("认证来源于RememberMe");
//使用throw自然是直接的操作异常,而没有什么抛出以及捕获的操作
//到最终的抛出和不是最终的捕获的了
//但这是对他自己,直接的操作异常可以被捕获(只要是异常就可以被捕获,基本没有最终的捕获)
//所以他本身也是可以被捕获(try)的
//而抛出则会指向到他这样的格式,所以并不会在抛出了(最终的抛出)
}
User user = userService.getById(id);
return user;
//注意:使用表单登录时,对应的authentication值类型是如下:
//org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fa794fef
//而自动登录,即使用rememberme时,对应的值,如下:
//org.springframework.security.authentication.RememberMeAuthenticationToken@487ac513
//也就是说,对应的操作是有不同的,主要是Authentication认证的信息不同
//虽然他保存了认证信息,但是使用什么方式认证,决定了他的值的类型(是子类或者其本身,则返回true)
//这样就可以使得进行安全的认证,使得有些必须只能够操作表单认证才可
//否则认证的类型不会相同(因为认证后,就不需要认证了)
}
这时,我们重新启动项目,清除浏览器缓存,通过试验可以知道,对应的当不是表单时,对应的会跳转到认证页面
在重要操作步骤可以加以验证,true代表自动登录,则引导用户重新表单登录,false正常进行
自定义登录成功处理和失败处理:
在某些场景下,用户登录成功或失败的情况下用户需要执行一些后续操作,比如登录日志的搜集
或者在现在目前前后端分离的情况下,用户登录成功和失败后需要给前台页面返回对应的错误信息
由前台主导登录成功或者失败的页面跳转,因为在前后端分离的时候
对应的后端基本只有接口,没有页面,所以需要有错误的信息,虽然可以重定向使得前端进行访问
但这种耦合,即联系是最好不要存在的,不符合前后端分离,容易出现一个地方失败,使得多个地方失败
这个时候需要要到用到AuthenticationSuccessHandler接口与AnthenticationFailureHandler接口
自定义成功处理:
实现AuthenticationSuccessHandler接口,并重写onAnthenticationSuccesss()方法
自定义失败处理:
实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure()方法
在这之前,我们首先看如下的解释:
/*
在配置类中,我们可以知道有如下的操作loginPage("/toLoginPage")和successForwardUrl("/")
接下来我们看看对应的loginPage("/toLoginPage"),点击进去
public FormLoginConfigurer<H> loginPage(String loginPage) {
return (FormLoginConfigurer)super.loginPage(loginPage);
}
再次点击loginPage(loginPage);进去
protected T loginPage(String loginPage) {
this.setLoginPage(loginPage);
this.updateAuthenticationDefaults();
this.customLoginPage = true;
return this.getSelf();
}
点击this.updateAuthenticationDefaults();进去
protected final void updateAuthenticationDefaults() {
if (this.loginProcessingUrl == null) {
this.loginProcessingUrl(this.loginPage);
这里是操作没有认证或者是除了表单的认证失败的其他情况,就需要到这里,代表重定向到认证页面
}
if (this.failureHandler == null) {
this.failureUrl(this.loginPage + "?error"); //当表单失败认证时,操作这个,有后缀
}
LogoutConfigurer<B> logoutConfigurer = (LogoutConfigurer)
((HttpSecurityBuilder)this.getBuilder()).getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
}
当退出认证时,操作这个,一般是退出登录,有后缀,基本是重定向
}
上面的后缀,只是用来看的,实际上并不会有什么作用
你可以试着在后面随便怎么操作,如赋值,或者多加,结果都是认证页面,说明并没有什么作用
我们点击this.failureUrl(this.loginPage + "?error");进去
public final T failureUrl(String authenticationFailureUrl) {
T result = this.failureHandler(new
SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
this.failureUrl = authenticationFailureUrl;
return result;
}
点击new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl)进去
会发现SimpleUrlAuthenticationFailureHandler实现了AuthenticationFailureHandler接口
代表操作认证失败的,查看对应的方法,可以知道是重定向
由于该new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl)的当作参数
所以回到上一级(上一级表示,就是我们进入之前的地方),若是多个上一级,我会说明回到某某地方,并标识写出来
当然,并不会都会标识上一级,注意即可,总有漏网之鱼吧
找到this.failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
点击failureHandler进去
找到如下
public final T failureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return this.getSelf();
}
说明failureHandler是操作认证失败的
至此,可以得出认证失败,的确是重定向
那么认证成功呢,点击successForwardUrl("/")进去
public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
return this;
}
点击new ForwardAuthenticationSuccessHandler(forwardUrl)进去
会发现ForwardAuthenticationSuccessHandler实现类AuthenticationSuccessHandler接口
说明是操作认证成功的,看看对应的方法
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
发现是转发操作,至此对应的认证成功也的确是转发,回到上一级
由于new ForwardAuthenticationSuccessHandler(forwardUrl)是当成参数,我们点击如下
this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
点击successHandler进去
public final T successHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
return this.getSelf();
}
可以发现successHandler是操作认证成功的
至此可以得出结论,failureHandler操作认证失败,successHandler操作认证成功
但是他们却都是操作表单的认证或者是对应的提交认证
只要提交就会,当然,如HttpBasic认证也有这样的设置,自然也是一样的操作
只是一般没有,所以我们通常操作表单认证
而没有认证,或者不是表单认证失败的(如对比失败),基本都会重定向到认证页面,一般没有后缀
*/
说明完成,所以接下来我们在service包下的impl编写MyAuthenticationService类
然后实现AuthenticationSuccessHandler和AuthenticationFailureHandler接口,使得不使用默认的登录成功或者登录失败的操作
相当于覆盖他们
这里之所以实现两个接口,是将该类当成总体,而不用多创建一个类了:
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*自定义登录成功或失败处理器
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,AuthenticationFailureHandler{
//用来得到可以操作响应的response,可以用它来操作重定向(转发好像操作不了)
//虽然单纯的使用response也可以操作(也可以操作转发),但对于ajax来说
//重定向和转发并不会起作用,因为接收者不是浏览器,而是ajax,所以一般会使得报错
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper; //可以将map转换成json
/**
* 登录成功后处理的逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("登录成功后继续处理");
redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/");
//httpServletResponse.sendRedirect("/");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
//httpServletResponse.sendRedirect("/toLoginPage");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/toLoginPage");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
}
}
那么如何使用该接口呢:
在配置类SecurityConfiguration里的configure(HttpSecurity http)接口进行改变:
@Autowired
private MyAuthenticationService myAuthenticationService;
//这里进行注入,由于需要当成参数,且参数是对应的两个接口类型,所以该类需要实现对应的两个接口,使得当成总体参数
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService) //登录成功的处理
.failureHandler(myAuthenticationService) //登录失败的处理
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
进行测试(重启项目,清除缓存),若跳转了则操作成功
重新修改MyAuthenticationService类:
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*自定义登录成功或失败处理器
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,AuthenticationFailureHandler{
//用来得到可以操作响应的response,可以用它来操作重定向(转发好像操作不了)
//虽然单纯的使用response也可以操作,也可以使用request操作转发,但对于ajax来说
//无论重定向还是转发并不会起作用,因为接收者不是浏览器,而是ajax
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper; //可以将map转换成json
/**
* 登录成功后处理的逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("登录成功后继续处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/");
//httpServletResponse.sendRedirect("/");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
Map result = new HashMap();
result.put("code", HttpStatus.OK.value()); //对应的value一般返回的是200
result.put("message","登录成功");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
//httpServletResponse.sendRedirect("/toLoginPage");
//RequestDispatcher requestDispatcher = httpServletRequest.getRequestDispatcher("/toLoginPage");
//requestDispatcher.forward(httpServletRequest, httpServletResponse);
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //对应的value一般返回的是401
result.put("message","登录失败");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
}
//当然,这里你可以先不进行打印数据,而是先操作重定向来进行验证是否跳转
//虽然一般会进行跳转(虽然前面验证过了)
前端部分页面(login.html):
<div style="padding:30px;">
<input type="button" onclick="login()"
class="button button-block bg-main text-big input-big" value="登录">
</div>
<!--
不操作submit提交了,但当submit和onclick一起操作时,两个都会执行,onclick会先执行,submit后执行
那么onclick与submit是同步的吗,答:基本同步,也就是说
只有onclick先执行完后,才会执行submit,但是对于也基本不会影响
因为我经过测试,在login方法里,加上return,发现,login不执行了,但是submit还是执行的
所以login和submit互不影响,只是有执行顺序
可以在login里加上
for(var a = 0;a<100000;a++){
console.log(a)
}
进行测试,发现,在他快要执行完后,submit才执行
-->
<script>
//注意:使用这里的ajax会导致验证码不填写也会进行提交(点击框框返回的时候会提示),而没有对应的阻拦
function login(){
$.ajax({
type:"POST", //请求类型
dataType:"json", //服务器返回数据的类型
url:"/login1", //请求路径
data:$("#formLogin").serialize(), //将表单数据转化成json
success:function(data){
//alert(data)
//这里可以先将弹出框进行查看,而不用先执行下面的,即将下面注释掉进行测试,防止你出现了问题
if(data.code == 200){
//因为返回200,就说明了是认证成功,那么就可以直接的访问了
window.location.href = "/";
}else{
alert(data.message)
}
}
})
}
</script>
<!--
可以先这样进行测试
<script>
function login(){
$.ajax({
type:"POST", //请求类型
dataType:"json", //服务器返回数据的类型
url:"/login1", //请求路径
data:$("#formLogin").serialize(), //将表单数据转化成json
success:function(data){
alert(data) 进行测试
// if(data.code == 200){
// //因为返回200,就说明了是认证成功,那么就可以直接的访问了
// window.location.href = "/";
//
// }else{
// alert(data.message)
// }
}
})
}
</script>
-->
至此,我们进行重启项目和清除浏览器缓存后,就可以发现,操作我们自己设置的认证成功和认证失败了,至此则操作成功
那么原来的为什么不操作呢,看如下解释:
/*
我们先看看对应的两个方法
.successHandler(myAuthenticationService) //登录成功的处理
.failureHandler(myAuthenticationService) //登录失败的处理
点击.successHandler(myAuthenticationService) 进去
public final T successHandler(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
return this.getSelf();
}
看到这个successHandler应该知道,在前面说过,他是操作认证成功的,所以原来的设置是被他覆盖了
点击.failureHandler(myAuthenticationService)进去
public final T failureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return this.getSelf();
}
同样也是如此failureHandler也是覆盖原来的设置,这就是为什么使用我们自己的配置的原因
*/
回到这里,又给出一个问题,对应的配置类中,configure(HttpSecurity http)的方法内容的顺序可以改变吗:
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
//通过测试,上面的顺序基本是不能改变的,也就是说,的确会覆盖之前的认证成功和认证失败的结果(因为后执行)
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
退出登录:
org.springframework.security.web.authentication.logout.LogoutFilter是操作退出登录的,但在之前,首先看看默认的退出登录
匹配URL为/logout的请求,实现用户退出,清除认证信息
只需要发送请求,请求路径为/logout即可,当然这个路径也可以自行在配置类中自行指定
同时退出操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行
退出的同时如果有remember-me的数据,同时一并删除
对应的index.html部分页面:
<div class="head-l">
<a class="button button-little bg-red" href="/logout">
<span class="icon-power-off"></span>退出登录</a></div>
当我们点击这个退出登录(自己进行重启项目和清除浏览器缓存测试)
那么就会使得重定向到认证页面(前面有说过,一般有后缀),使得没有认证,若有"记住我"自然会删除
但是我们发现,对应的sessionid并没有变化,那么他没有变化,难道可以直接访问吗,答:不可以
虽然前面的认证成功后,会绑定一个变化的sessionid,但是对应的sessionid是获取对应的信息的
且一般存在(与是否启动项目无关,可能是持久数据)
这也是为什么重新启动项目时,还是可以访问的原因,而退出认证后,对应的信息没有了
会使得,没有认证(退出的认证),那么自然回到认证页面,重定向带后缀的
我们也可以进行改变,不使用"/logout",对应的配置类SecurityConfiguration里的configure(HttpSecurity http)方法如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)
.and().logout().logoutUrl("/out") //设置对应的退出请求为"/out"
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
至此,对应的index.html部分页面如下:
<div class="head-l">
<a class="button button-little bg-red" href="/out">
<span class="icon-power-off"></span>退出登录</a>
</div>
<!--
注意:修改成/out后,对应原来的/logout访问就不存在了,也就是404
就如我们设置了请求认证,如设置了/login1,那么原来的/login也就不存在了,也是404
至此,实际上他们都只是进行赋值的操作,所以会导致没有对应的原来的路径
-->
在前面我们可以通过接口,来覆盖对应的认证成功和认证失败,那么如何覆盖退出认证呢,很简单,前面说过的
org.springframework.security.web.authentication.logout.LogoutFilter是操作退出登录的
也就是说我们需要对应的MyAuthenticationService类进行实现退出登录的接口,如下:
package com.lagou.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
*自定义登录成功或失败处理器以及退出认证登录处理器
*/
@Service
public class MyAuthenticationService implements
AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {
//用来得到可以操作响应的response,可以用它来操作重定向
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private ObjectMapper objectMapper; //可以将map转换成json
/**
* 登录成功后处理的逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("登录成功后继续处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/");
Map result = new HashMap();
result.put("code", HttpStatus.OK.value()); //对应的value一般返回的是200
result.put("message","登录成功");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //对应的value一般返回的是401
result.put("message","登录失败");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, Authentication authentication) throws
IOException, ServletException {
System.out.println("退出之后进行处理");
redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/totoLoginPage");
}
}
对于的配置类SecurityConfiguration方法configure(HttpSecurity http),给出部分代码:
.successHandler(myAuthenticationService) //登录成功的处理
.failureHandler(myAuthenticationService) //登录失败的处理
.and().logout().logoutUrl("/out")
.logoutSuccessHandler(myAuthenticationService)
//这里进行覆盖原来的退出方法,使用我们自己配置的
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
至此,我们重启项目,清除缓存,测试,若控制台打印了信息,且跳转了,则操作成功
图形验证码验证:
图形验证码一般是防止恶意,人眼看起来都费劲,何况是机器
不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术
所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰
也有目前需要手动滑动的图形验证码,这种可以有专门去做的第三方平台
比如极验(https://www.geetest.com/),那么本次课程讲解主要针对图形验证码
spring security添加验证码大致可以分为三个步骤:
根据随机数生成验证码图片
将验证码图片显示到登录页面
认证流程中加入验证码校验
Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的
所以我们的验证码校验逻辑应该在这个过滤器之前,也就是在验证用户名和密码之前
即验证码通过后才能到后续的用户名和密码对应表单的对比操作,流程如下:
代码实现:
我们登录时,一般会发现,图片那里是没图标志(前面有过说明)
那时因为被拦截了,需要放行,对应的SecurityConfiguration配置类的configure(WebSecurity web)方法:
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**","/code/**");
//加上了"/code/**"
}
为什么要加上"/code/**",我们找到ValidateCodeController类,该类就是操作图形验证码的:
我们可以发现,对应需要redis,所以我们需要启动redis(否则一般会报错),对应的学习了88章博客,那么就知道如何操作
在对应的application.properties里面进行修改:
## redis相关配置
# Redis数据库索引(默认为0)
#spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.164.128
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=
#将对应的ip和端口的注释解除即可(端口也可以不用解除,因为默认也是6379)
#对应由于默认0数据库,所以可以不用解除,且我们也并没有操作密码
至此我们启动项目,清除缓存,访问http://localhost:8080/code/image,会得到一个图片,那个就是验证码图片
接下来我们只需要,修改对应的login.html的对应图片的代码:
<img src="/code/image" alt="" width="100" height="32" class="passcode"
style="height:43px;cursor:pointer;"
onclick="this.src=this.src+'?'">
<!--
this.src一般会给原来的src加上绝对路,若是服务器的项目
则加上对应的端口地址(比如说这里加上http://localhost:8080/)
若是本地的,则加上当前所在文件目录地址,当然会受到对应的目录..或者.影响(一个.基本无影响),但服务器项目不会
当然,若是本地的,直接一个/开头,一般是操作自己当前的盘开始
当然服务器项目这里却无论怎么加上/,都是对应的端口地址
当然,无论是本地还是服务器项目,对应的地址后面无论加上多少?对应结果并没有影响,因为?代表附带值
虽然附带了并不会影响访问(特别是对图片来说),但请求却可以操作,这里并没有操作,所以加不加都可以
多次点击就会加上对应的?,一个点击加上一个?
所以说只要有?后面的值都看出附加值,当然这个?只能是英文的而不能是中文的
-->
至此,我们重启,清缓存,看看,发现,对应有图形验证码了
在redis里查看,发现,多出来了一个数据,使用get命令查看值,正好是图形验证码上面的数字,至此操作成功
验证码以及生成,那么如何将我们输入的进行对比呢:
看如下:
首先我们在lagou包下,创建filter包,并在该包下创建ValidateCodeFilter类:
package com.lagou.filter;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码过滤器 OncePerRequestFilter 一次请求只会经过一次过滤器
*/
@Component
//这里为什么需要继承OncePerRequestFilter,主要是为了操作对应的类,后面会继续说明为什么
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, FilterChain filterChain) throws
ServletException, IOException {
//判断是否是登录请求
//equalsIgnoreCase不考虑大小写
if(httpServletRequest.getRequestURI().equals("/login1")&&
httpServletRequest.getMethod().equalsIgnoreCase("post")){
//imageCode是输入的图形验证码的name
String imageCode = httpServletRequest.getParameter("imageCode");
System.out.println(imageCode);
//具体的验证流程,后面会进行补充
}
//如果不是登录请求直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
对应的配置类SecurityConfiguration的configure(HttpSecurity http)方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
//将我们的验证过滤器加入到用户名密码验证过滤器的前面
//正是因为有这样的操作,使得spring security不止只操作默认的15个过滤器
http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
//只需要传递Filter接口及其子类即可(两个都是)
//validateCodeFilter父辈中有Filter接口
//第二个UsernamePasswordAuthenticationFilter.class他所在的参数类型是Class<? extends Filter>)
//所以说两个都是
http.formLogin().loginPage("/toLoginPage")
.loginProcessingUrl("/login1")
.usernameParameter("username1")
.passwordParameter("password1")
.successForwardUrl("/")
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)
.and().logout().logoutUrl("/out")
.logoutSuccessHandler(myAuthenticationService)
.and().rememberMe()
.tokenValiditySeconds(1209600)
.rememberMeParameter("remember-mer")
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().anyRequest().authenticated();
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
//http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
/*
那么可以写在后面吗,答:可以,因为这里只是进行设置,也就是顺序
只要指定了在谁的过滤器前面无论是先执行还是后执行,都会有标记的,使得在前面
那么可以执行两次吗,答可以但先标记的在后标记的前面,所以加载时会出现两个过滤器
只是后标记的会覆盖先标记的,所以只会执行一次的方法验证
*/
//这里我们回到前面的一个问题,为什么要继承OncePerRequestFilter类,而不直接的实现Filter接口呢:
//主要是对应的方便,虽然我们也可以实现Filter接口来进行意一样的操作
//但继承继承对应的OncePerRequestFilter类(他的父辈,有Filter接口)
//方便一些,不用进行强制转换了
//实现Filter接口操作HttpServletRequest和HttpServletResponse需要如下操作:
/*
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
接口的强制,编译期不会识别,因为该接口对应的实现类可能是强制转换的类的子类,虽然编译期一般不识别
//当然直接的类自然会识别,虽然不会识别接口,但运行期自然会识别,不对就会报错
*/
}
至此我们进行重新启动并清除缓存,当我们提交时,可以在控制台上进行观看,的确打印的认证的验证码信息
接下来我们进行操作认证流程:
先在lagou包下创建execption包,并在该包下创建ValidateCodeException:
package com.lagou.execption;
import org.springframework.security.core.AuthenticationException;
/**
* 验证码异常类
*/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
用来操作异常的(独有的异常操作,大概有特殊的打印信息)
回到对应的ValidateCodeFilter类进行修改代码:
package com.lagou.filter;
import com.lagou.controller.ValidateCodeController;
import com.lagou.execption.ValidateCodeException;
import com.lagou.service.impl.MyAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码过滤器 OncePerRequestFilter 一次请求只会经过一次过滤器
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private MyAuthenticationService myAuthenticationService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, FilterChain filterChain) throws
ServletException, IOException {
//判断是否是登录请求
//equalsIgnoreCase不考虑大小写
if(httpServletRequest.getRequestURI().equals("/login1")&&
httpServletRequest.getMethod().equalsIgnoreCase("post")){
//imageCode是输入的图形验证码的name
String imageCode = httpServletRequest.getParameter("imageCode");
System.out.println(imageCode);
//具体的验证流程
try {
validate(httpServletRequest, imageCode);
//只要这个方法出现了异常,就代表,对应的验证码操作失败,自然需要有对应的错误信息
}catch (ValidateCodeException e){
e.printStackTrace();
myAuthenticationService.onAuthenticationFailure(
httpServletRequest,httpServletResponse,e);
return;
}
}
//如果不是登录请求直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void validate(HttpServletRequest request,String imageCode){
//由于对应的验证码信息存在redis里面,所以我们需要取出redis的信息进行对比,使得验证成功
//因为过期时间,所以验证码也通常需要在一定的时间里进行验证,这也是redis的一个作用
String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE+"-"+request.getRemoteAddr();
String s = stringRedisTemplate.boundValueOps(redisKey).get();
//实际上也就是操作stringRedisTemplate.opsForValue().get(redisKey);,可以看对应的源码就知道了
//验证码的判断
if(!StringUtils.hasText(imageCode)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(s==null){
throw new ValidateCodeException("验证码已过期");
}
if(!s.equals(imageCode)){
throw new ValidateCodeException("验证码不正确");
}
//从redis中删除验证码信息
stringRedisTemplate.delete(redisKey);
}
}
上面操作的登录失败的方法,现在我们改一改对应的方法:
找到MyAuthenticationService类的onAuthenticationFailure方法:
/**
* 登录失败的处理逻辑
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AuthenticationException e) throws
IOException, ServletException {
System.out.println("登录失败后进行处理");
//redirectStrategy.sendRedirect(httpServletRequest,httpServletResponse,"/toLoginPage");
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value()); //对应的value一般返回的是401
result.put("message",e.getMessage()); //这里进行了改变
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
那么为什么要操作对应的登录失败的方法呢,我们先说明一下过滤器,在过滤器里面,也是可以操作重定向和转发的
并且可以输出内容,但是在ajax里面,重定向和转发不可以操作,且输入出的内容只能是一次性的(两个write会使得不会得到)
也就是不会执行对应成功的方法,而是错误的方法(一般是错误信息)
所以对应需要加上"return;"来进行中断,否则会执行两次登录失败的方法,而之所以操作登录失败的方法
是因为我们直接的操作他的内容,而不用我们自己写了
接下来我们重启项目,清除缓存,进行测试,发现,只要当验证码和密码都正确时
才会进行认证成功(当然用户名也是要正确的),而其他不用表单验证的,都放行,也就相当于该过滤器没有操作一样
至此操作验证码成功
session管理:
Spring Security可以与Spring Session库配合使用,只需要做一些简单的配置就可以实现一些功能
如(会话过期、一个账号只能同时在线一个、集群session等)
会话超时:
配置session会话超时时间,默认为30分钟,但是Spring Boot中的会话超时时间一般设置至少为60秒
也就是说,如果少于60秒,默认60秒
在application.properties文件里加上:
#session设置
#配置session超时时间
server.servlet.session.timeout=60
当session超时后,默认跳转到登录页面(重定向,基本上不是表单的操作,其余的失败基本都是重定向)
也就是使得sessionid换了一个
自定义设置session超时后地址并设置最大会话数量:
设置session管理和失效后跳转地址,在对应的配置类SecurityConfiguration里的configure(HttpSecurity http)方法里加上如下:
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1);//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
//.expiredUrl("/toLoginPage"); //被踢出时,进行跳转的页面
//实际上只要你被踢,那么相当于没有认证,而没有认证,就会重定向到认证页面
//只是对应的默认页面(转发的),是不需要经过认证的,也就是说,他是例外(设置的例外),使得不需要认证也可以访问
//所以不会默认重定向到登录页面,但他也只是操作一次,也就是说,只有第一次是例外,其他时候会进行重定向到认证页面
//我们可以发现,大多数都是重定向
//这也是为了防止前面操作什么路径认证后就会到什么路径的设置的原因,虽然并不会操作
我们重启项目,清除缓存,登录后,进行等待60秒(当然,我这里帮你进行了测试),的确默认设置的登录页面
当然,若你进行了这里的设置跳转页面,那么任然是重定向到你设置的跳转页面,但一般情况下
由于sessionid的变化,会使得不会认证成功(没有"记住我"的前提),所以任然是登录页面,在有"记住我"的前提,那么会进行跳转
这时,我们也进行测试,在谷歌浏览器访问登录,在360浏览器访问登录,或者其他浏览器访问登录
回到谷歌浏览器,再次的访问,会出现如下:
代表有人登录,把我踢出去了,所以简称为互踢
但是该页面的返回肯定是不好的,我们可以设置页面的返回,也就是被踢出后
回到那个页面(上面说明了一下,这里再次说明):
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1)//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
.expiredUrl("/toLoginPage"); //被踢出时,进行跳转的页面
//实际上只要你被踢,那么相当于没有认证,而没有认证,就会重定向到认证页面
//只是对应的默认页面(转发的),是不需要经过认证的,也就是说,他是例外(设置的例外),使得不需要认证也可以访问
//所以不会默认重定向到登录页面,但他也只是操作一次,也就是说,只有第一次是例外,其他时候会进行重定向到认证页面
//我们可以发现,大多数都是重定向
//这也是为了防止前面操作什么路径认证后就会到什么路径的设置的原因,虽然并不会操作
这时我们重新启动,清除缓存,测试,会发现到达登录页面(也就是认证页面)
阻止用户第二次(这里是第二次)或者超过会话数量的登录:
但是有时候我们并不想要互踢,也就是说,别人在登录时,你就不能登录,操作的代码如下:
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1)//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
.expiredUrl("/toLoginPage")//被踢出时,进行跳转的页面实际上只要你被踢
// 那么相当于没有认证,只是对应的默认页面,是不经过认证的,所以不会默认重定向到登录页面
.maxSessionsPreventsLogin(true); //如果达到最大会话数量,就阻止登录
//只要加上了maxSessionsPreventsLogin(true),那么对应的expiredUrl("/toLoginPage")也就没有作用了
//那么这里的顺序可以随便写吗,答:不可以随便写,但部分可以
//因为对应的返回都是一样的(一般表单的配置是通过and分开的,其他的基本可以互换)
//也就是说可以这样:
/*
http.sessionManagement() //设置session管理
.invalidSessionUrl("/toLoginPage") //session失效后跳转的页面,默认是自己设置的登录页面
//这里还是设置成对应的/toLoginPage
.maximumSessions(1)//session最大的会话数量
//1:代表同一时间,只能有一个用户可以登录,一般有个专业术语:互踢
.maxSessionsPreventsLogin(true) //如果达到最大会话数量,就阻止登录
.expiredUrl("/toLoginPage");//被踢出时,进行跳转的页面实际上只要你被踢
// 那么相当于没有认证,只是对应的默认页面,是不经过认证的,所以不会默认重定向到登录页面
*/
提示如下:
这个1代表只能有一个登录,且有一个登录了,当然设置2也可以,且只要你登录过了,对应的信息就会在服务器的加上1
也就是说,浏览器无论是退出,还是退出登录,还是清除缓存,对应的登录过的都会存在,因为对应的sessionid变化了
所以在服务器时,浏览器并没有影响,且退出登录却不会减1,而相同的sessionid继续登录时
认证后出现的哪个sessionid对应账号在线数的,即相同的sessionid,只是进行覆盖而已(使得又是新的sessionid覆盖旧的)
而不是保存到服务器(添加新的)
在同一个账号同时在线个数如果设置为1时,该账号在同一时间内只能有一个有效的登录,如果同一个账号又在其它地方登录
那么就将上次登录的会话过期(浏览器可以观察到),即后面的登录会踢掉前面的登录
注意:在没有认证之前,都是操作认证页面的,除非设置放行的
也就是认证登录不拦截(前面操作了认证页面的不拦截,防止多次重定向)
集群session:
实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡
用户访问nginx,nginx再决定去访问哪一台服务器,当一台服务宕机了之后,另一台服务器也可以继续提供服务
保证服务不中断,如果我们将session保存在Web容器(比如tomcat)中
如果一个用户第一次访问被分配到服务器1上面需要登录,当某些访问突然被分配到服务器2上
因为服务器2上没有用户在服务器1上登录的会话session信息,服务器2还会再次让用户登录
用户已经登录了还让登录就感觉不正常了
解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中
而是保存到一个单独的库(redis、mongodb、jdbc等,jdbc也可以称为mysql)中
所有服务器都访问同一个库,都从同一个库来获取用户的session信息
如用户在服务器1上登录,将会话信息保存到库中,用户的下次请求被分配到服务器2
服务器2从库中检查session是否已经存在,如果存在就不用再登录了,可以直接访问服务了
换言之,对应存放在库里面的信息,代表是否认证成功的信息
引用依赖:
<!-- 基于redis实现session共享,需要对应的redis地址设置,否则启动报错,比如设置:
对应的配置文件中application.properties如下的配置:
# Redis服务器地址
spring.redis.host=192.168.164.128
# Redis服务器连接端口
spring.redis.port=6379 这个可以不用设置,默认为6379
-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
设置session存储类型:
application.properties添加如下:
#使用redis共享session
spring.session.store-type=redis
#好像Spring Security对应会检查库依赖,在没有配置指定使用哪个库时,一般有顺序,通常是redis优先
#所以这个配置可以不用写,但最好写上,防止优先级的判断操作
#且也防止被其他人优先(虽然redis优先级别很高,基本是第一)
测试:
先开启多个实例(点击项目配置):
然后注释掉热部署依赖,并刷新,防止修改文件端口时,进行了部署,导致相同的端口,使得启动报错
这里就不操作nginx了,我们直接的手动启动两个端口进行测试,而不用nginx进行测试了
分别以8080和8081端口启动,我们在8080端口上进行启动登录,然后再8090上进行访问,发现可以得到信息
接下来注释掉对应的基于redis实现session共享依赖,再次进行测试,会发现,8090需要登录了
但也要记住,之所以加上依赖,是因为对应的依赖可以操作redis从而保存sessionid信息
每次的认证拦截(拦截使得到认证页面,而不是手动去),都会去redis取得sessionid
手动去的话,对应的Sping Security基本会删除对应的session的cookie的(删除方式很多,如过期时间),导致返回没有sessionid
如果redis没有sessionid,则将自己的放上去进行保存,当其他的服务器访问时
会取出对应的sessionid(有的情况下),那么就相当于操作的是同一个sessionid
那么就会使得对应的信息得到,已经认证的信息,那么就不需要登录了
但也要注意:他将对应的sessionid的名称由原来的JSESSIONID变成了SESSION(通常考虑访问不同服务器时,会改变对应的JSESSIONID值,因为不是同一个服务器),但也只是修改名称而已(实际上并不能修改,可以理解为操作了过期删除,然后创建,使得看起来修改了的意思)
如果该属性的值存在,多次的刷新他是不会覆盖的,就与不会覆盖sessionid一样,当然,大多数的cookie都是如此(因为基本都有判断,当然,如果我们自己操作时cookie时,可以在servlet里面不进行判断,而使用我们的方式,使得会有覆盖,这只是一个覆盖的说明而已,所以并不需要理会)
当然,他只是操作session,对于"记住我"并没有操作,但并不需要,我们只需要认证信息即可,使得不需要再次认证
相当于已经认证了,自然也就是不需要了
虽然一般一个浏览器的session是一样的,但是却可以被程序进行改变(如名称和值,因为他也是cookie)
上面是同一个浏览器里操作的,如果不同的浏览器,那么还需要登陆吗:
按道理来说,每次的拦截要认证时(拦截到认证页面),都会去得到session,那么会得到吗,答:不会,不同的浏览器得不到
因为浏览器的本身是不同的(一般可能也会包括ip等等信息,使得唯一)
而这个不同,不止决定了sessionid的获取,而且也因为这样使得操作不了同一个redis的对应session
所以说,在不同的浏览器里,操作的redis的对应数据不同,可以打开redis,查看对应的信息
发现,多出来了几条类似数据,那就是浏览器不同的对应数据
csrf防护机制:
什么是csrf:
CSRF(Cross-site request forgery),中文名称:跨站请求伪造
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求
CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账等等造成的问题
包括:个人隐私泄露以及财产安全
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注
08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞
如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI等等
而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为"沉睡的巨人"
CSRF的原理:
通常情况下,我们去访问网站b时,可能会点击某些按钮或者超链接,由于你的cookie存在
假设你点击的是对方的超链接,那么是可以得到你的cookie的,也就可以登录对应的网站a
但通常的来说,对应的超链接可能只是包含了一些信息,因为你并不知道,你带着这些信息去访问网站a
可能他里面会帮你访问,或者是一些恶意的url等等
而这些信息可能会对你的在网站上的信息造成影响,一般第一种是很难操作的,但操作成功,收益自然也是最大
因为在不同的浏览器上,或者说不同的地方的浏览器,一般是访问不了的,通常对应的服务器都会进行判断
就如前面说的,操作redis库时,当你在其他浏览器里(可能也有ip等等信息,保持唯一),需要登录
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成三个步骤:
登录受信任网站A,并在本地生成Cookie
在不登出A的情况下,访问危险网站B
触发网站B中的一些元素
所以说,只要你不乱点,那么基本不会受到csrf的攻击
CSRF的防御策略:
在业界目前防御 CSRF 攻击主要有三种策略:
验证 HTTP Referer 字段,在请求地址中添加 token 并验证,在 HTTP 头中自定义属性并验证
验证 HTTP Referer 字段:
根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,简称来源
谁的页面进行到这里(一般是需要的页面跳转,而不是url的直接赋值访问,通常表单的操作会使得显示来源)
它记录了该 HTTP 请求的来源地址
在通常情况下,访问一个安全受限页面的请求来自于同一个网站,在后台请求验证其 Referer 值
如果是以自身安全网站开头的域名,则说明该请求是是合法的
如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求
在前面的操作表单参数时,对应的刷新里面就有对应的来源,一般来自认证页面,可以自己进行查看
虽然是来源,但操作却根两个请求一样,所以也有时候称为多次请求
实际上却不是,只是请求头中包含了对应的信息而已,刷新也只是对请求头进行再次的发送进行访问
在请求地址中添加 token 并验证:
CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中
因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证
要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中
当然若黑客可以得到你的页面,自然也是可以进行操作,虽然并不困难
因为可以伪造源地址,或者其他方式,如拦截请求,拦截响应等等,使得网页被得到
可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token
如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求
在 HTTP 头中自定义属性并验证:
这种方法也是使用 token 并进行验证,和上一种方法不同的是
这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里
security中的csrf防御机制:
org.springframework.security.web.csrf.CsrfFilter:
csrf又称跨站请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息
如果不包含,则报错,起到防止csrf攻击的效果
通常是生成token,验证token
开启csrf防护:
一般是默认开启的,现在我们将对应的关闭这个csrf的防护的代码进行注释:
//关闭csrf防护
//http.csrf().disable();
//注意:当我们开启防护时,退出登录必须是post请求,且需要携带对应的_csrf值
//否则不会让你退出(一般是404错误),不让你找到
//当然了,也可以关闭防护进行解决,但大多数情况下,我们不会进行关闭,所以解决方式如下:
/*
找到对应的index.html修改如下即可:
<div class="head-l">
<!-- <a class="button button-little bg-red" href="/out">-->
<!-- <span class="icon-power-off"></span>退出登录</a>-->
<form method="post" action="/out">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button class="button button-little bg-red">
<span class="icon-power-off"></span>退出登录</button>
</form>
</div>
将原来的注释了,进行添加表单来操作post了
上面的原来的进行的注释,经过测试,必须是post,若是get,则任然报错404
然而这是对退出登录来说的规定,其他的若是get不会操作这样的防护,如后面的/user/saveOrUpdate
*/
至此我们进行启动,会发现,认证失败,ajax对应会到错误的方法,可以自己进行操作
一般不操作ajax的话,是重定向到认证页面,没有后缀
不同于表单认证失败的结果(有后缀),有他,先操作他的重定向,否则操作表单失败的重定向
当然,若进行了覆盖,那么使得覆盖的
所以说,这就是为什么ajax对应到错误方法的原因(重定向)
那么我们在对应的login.html里加上操作的csrf的name:
<div class="form-group">
<div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<!--得到生成的name和value,一般name是固定的,即_csrf,value则是变化的-->
</div>
</div>
接下来我们进行重启项目,清除缓存,到认证页面,然后登录,可以发现能够登录成功,但这是正常的情况
有些情况下,到对应的认证页面就会有重定向次数过多,而删除上面的html就没有这样的错误,那么这是为什么会出现呢:
虽然与前面的放行操作有点类似,但这里获得对应的csrf信息时,若是空的,则会进行重定向
使得继续获得,所以会出现多次的重定向,我们进行验证,在出现这种情况下时,我们回到LoginController类,加上如下代码:
package com.lagou.controller;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 处理登录业务
*/
@Controller
public class LoginController {
/**
* 跳转登录页面
*
* @return
*/
@RequestMapping("/toLoginPage")
public String toLoginPage(HttpServletRequest request,HttpServletResponse response) {
//下面是重新的生成对应的csrf信息
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
CsrfToken csrfToken = cookieCsrfTokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = cookieCsrfTokenRepository.generateToken(request);
cookieCsrfTokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//CsrfToken.class.getName()的值是CsrfToken类的地址
//其中csrfToken.getParameterName()的值是_csrf
//而我们操作前端得到的值,也就是csrfToken,虽然有两个方法获取
//一个是CsrfToken类的地址,一个是_csrf,而这里就是操作_csrf
//他们都是String类型,这是自然的,因为参数就是操作String类型的
//这时我们进行了生成信息,继续重新项目,清除缓存,可以发现,没有重定向了
//可以我们却登录不了,因为这是我们自己生成的
//那么这些代码如何来的呢,我们可以找到CsrfFilter过滤器
//在前面说过的15个过滤器中的其中一个,我们关闭对应的防护,基本会使得不会加载该过滤器
/*
我们继续看他的介绍
org.springframework.security.web.csrf.CsrfFilter:
csrf又称跨域请求伪造, SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息
如果不包含,则报错,起到防止csrf攻击的效果
他并没有说明生成的csrf信息,实际上他操作了生成csrf信息放在request里面,具体部分核心代码如下:
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) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName(
可以发现,对应的操作与我们这里生成的操作是一样的
但是,为什么他放在了request里面,却得到的是null呢,通过我的研究,发现,我们一般可以使用
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**","/code/**");
中操作的toLoginPage,代替原来操作的
antMatchers("/toLoginPage").permitAll()
虽然都是放行,但是前面的放行,基本只针对于认证,而不操作csrf防护
对应的防护相当于认证的意思,也就是防护后,后面就不需要防护了
使得csrf防护一直重定向,因为在没有放行的情况下,request中会进行删除,使得为null,然后重定向
如此反复,自然得不到,使得操作null,所以我们需要将
web.ignoring().antMatchers("/toLoginPage","/css/**","/images/**","/js/**","/code/**");
修改成
web.ignoring().antMatchers("/css/**","/images/**","/js/**","/code/**");
原来操作的表单中,的这个部分:
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests()
.anyRequest().authenticated();
修改成
.tokenRepository(getpersistentTokenRepository())
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()
.anyRequest().authenticated();
*/
return "login";
}
}
通过上面的解释,使得对应都进行放行,所以继续重启项目,清除缓存,可以发现,能够登录了
即我们需要进行全面的放行
但是我们可以试着添加用户,发现,添加不了,报错,一般是403错误(有权限错误的页面,就操作该错误页面)
这是因为我们需要进行设置页面不防护,因为SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息
而对应的添加并没有包含其对应的_csrf以及其value值,注意:第一次生成后的csrf的值,会一直保存
实际上我们第一次访问时就会创建了,所以后面就不需要创建
在认证后,或者清除缓存后(一般与sessionid对应),对应的值会再次创建覆盖,然后放在request里面
除了这两种,那么一般就是操作自己生成的了,也就是第一次
接下来为了不操作防护,加上如下代码:
//开启csrf防护,可以设置哪些不需要防护
http.csrf().ignoringAntMatchers("/user/saveOrUpdate"); //相当于局部的关闭防护,而不是全部进行关闭
//但也要明白,这是操作是否进行防护的,不需要防护时,基本与生成csrf值并没有关系,也就是说
//只要你需要对应的csrf值,那么如果是不防护的,那么与是否有csrf值没有关系
//也就是说,他使得不操作_csrf值,那么无论是否有_csrf值,也就没有关系了
//在对应的user_add.html里面可以找到post请求的地址
//经过测试,发现post确实需要防护的认证,因为操作get时不会报错
至此,我们重启项目,清除缓存,发现,添加成功
跨域与CORS:
跨域:
跨域,实质上是浏览器的一种保护处理,如果产生了跨域,服务器在返回结果时就会被浏览器拦截
注意:此时请求是可以正常发起的,只是浏览器对其进行了拦截,导致响应的内容不可用
产生跨域的几种情况有一下:
当前页面URL | 被请求页面URL | 是否跨域 | 原因 |
---|---|---|---|
http://www.lagou.com/ | http://www.lagou.com/index.html | 否 | 同源(协议,域名,端口号相同) |
http://www.lagou.com/ | https://www.lagou.com/index.html | 跨域 | 协议不同(http/https) |
http://www.lagou.com/ | http://www.baidu.com/ | 跨域 | 主域名不同(lagou/baidu) |
http://www.lagou.com/ | http://kaiwu.lagou.com/ | 跨域 | 子域名不同(www/kaiwu) |
http://www.lagou.com/ | http://www.lagou.com:8090 | 跨域 | 端口号不同(8080/8090) |
我们一般将主域名和子域名等等域名统称为域名,即他们的不同也可以统称为域名不同
解决跨域:
一般是我们自己进行解决跨域的,这也是为了保护我们,而不是对方解决了跨域我们就可以访问
防止我们操作了恶意的网站,这是一个防护,当然还有很多
JSONP:
浏览器允许一些带src属性的标签跨域,也就是在某些标签的src属性上写url地址是不会产生跨域问题
CORS解决跨域:
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)
CORS需要浏览器和服务器同时支持
目前,所有浏览器都支持该功能,IE浏览器不能低于IE10,所以我们一般只需要服务器支持即可
浏览器在发起真正的请求之前,会发起一个OPTIONS类型的预检请求,用于请求服务器是否允许跨域
在得到许可的情况下才会发起请求
我们可以进行测试,将index.html里面的如下代码的注释去掉:
function toCors() {
$.ajax({
// 默认情况下,标准的跨域请求是不会发送cookie的
xhrFields: {
withCredentials: true
},
url: "http://localhost:8090/user/1", // 根据ID查询用户
success: function (data) {
alert("请求成功." + data)
}
});
}
在启动一个8090的服务器,点击跨域测试,会发现,并没有进行弹框,即出现错误,该错误就是跨域的错误
基于Spring Security的CORS支持:
声明跨域配置源:
在配置类SecurityConfiguration类下加上如下代码:
/**
* 跨域配置信息源
*/
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许跨域的站点,*代表所有站点
corsConfiguration.addAllowedOrigin("*");
//允许跨域的http方法,*代表所有方法,站点可以跨域,但方法却也需要进行设置跨域
corsConfiguration.addAllowedMethod("*");
//允许跨域的请求头,*代表所有请求头,在方法之前,自然也是需要使得可以操作请求头的
corsConfiguration.addAllowedHeader("*");
//允许带凭证
corsConfiguration.setAllowCredentials(true);
//UrlBasedCorsConfigurationSource是实现CorsConfigurationSource的实现类
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource
= new UrlBasedCorsConfigurationSource();
//对所有的url都生效
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
return urlBasedCorsConfigurationSource;
}
对应的configure(HttpSecurity http)方法里面加上:
//开启跨域支持,操作跨域的信息
http.cors().configurationSource(corsConfigurationSource());
至此,我们重启项目,清除缓存,登录后,点击跨域测试,可以发现,有弹框了
SpringSecurity授权:
授权简介:
在认证的操作中我们讲解的都是用户认证,不管是用户名密码,还是图形验证码等,最终的目的都是一个:
让系统知道你到底是谁在访问你的系统,解决的问题是,你是谁?
接下来主要讲解你能在系统中做什么事情,针对这个有的叫做:授权,有的叫做:鉴权,还有叫权限控制
最终的目的就是你能在系统中能过做什么
Spring Security 对授权的定义:
安全权限控制问题其实就是控制能否访问url
Spring Security 授权原理:
在我们应用系统里面,如果想要控制用户权限,需要有2部分数据
系统配置信息数据:写着系统里面有哪些URL,每一个url拥有哪些权限才允许被访问
另一份数据就是用户权限信息:请求用户拥有权限
系统用户发送一个请求,系统配置信息和用户权限信息作比对,如果比对成功则允许访问
当一个系统授权规则比较简单,基本不变时候,系统的权限配置信息可以写在我们的代码里面去的
比如前台门户网站等权限比较单一,可以使用简单的授权配置即可完成
如果权限复杂,例如办公OA,电商后台管理系统等就不能使用写在代码里面了,需要RBAC权限模型设计
内置权限表达式:
Spring Security 使用Spring EL来支持,主要用于Web访问和方法安全上
可以通过表达式来判断是否具有访问权限
下面是Spring Security常用的内置表达式,ExpressionUrlAuthorizationConfigurer定义了所有的表达式
表达式 | 说明 |
---|---|
permitAll | 指定任何人都允许访问 |
denyAll | 指定任何人都不允许访问 |
anonymous | 指定匿名用户允许访问 |
rememberMe | 指定已记住的用户允许访问 |
authenticated | 指定任何经过身份验证的用户都允许访问,不包含anonymous |
fullyAuthenticated | 指定由经过身份验证的用户允许访问,不包含anonymous和rememberMe |
hasRole(role) | 指定需要特定的角色的用户允许访问,会自动在角色前面插入’ROLE_’ |
hasAnyRole([role1,role2]) | 指定需要任意一个角色的用户允许访问,会自动在角色前面插入’ROLE_’ |
hasAuthority(authority) | 指定需要特定的权限的用户允许访问 |
hasAnyAuthority([authority,authority]) | 指定需要任意一个权限的用户允许访问 |
hasIpAddress(ip) | 指定需要特定的IP地址可以访问 |
在前面操作中,实际上我们已经接触过了,如下:
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()
.anyRequest().authenticated();
//其中permitAll,也就是指定任何人都允许访问,也就是我们说的都进行放行的意思,可以将访问当成放行的意思
//authenticated,指定任何经过身份验证的用户都允许访问,不包含anonymous
//也就是认证后,可以访问,除了匿名用户
url安全表达式:
基于web访问使用表达式保护url请求路径
设置url访问权限:
在配置类SecurityConfiguration里的configure(HttpSecurity http)加上如下代码:
//加上这个相当于多出来了对应的访问放行,即设置/user开头的请求需要ADMIN权限
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
//当然如果删除的话,自然不需要权限就可以访问,这里使得,需要该权限访问,即加了限制
//所以对应的我们进行添加权限,是服务于这个的,若这个没有,无论你是否添加,都可以访问,所以我们也说这个是限制
/*
因为可以这样
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll()
.and().authorizeRequests().antMatchers("/css/**").permitAll()
或者
.and().authorizeRequests().antMatchers("/toLoginPage","/css/**").permitAll()
*/
//点击hasRole进去,可以发现,的确加上了ROLE_,即总体来说对应的是ROLE_ADMIN,注意是对应的
重启项目,清除缓存,登录认证后,我们点击用户管理,因为用户管理是/user/findAll的路径
需要对应的权限,所以访问失败(一般出现403错误)
403错误:403错误是网站访问过程中,常见的错误提示,意思为资源不可用,服务器理解客户的请求,但拒绝处理它
在这里通常是因为权限的问题使得的操作(主要是权限)
既然需要权限,那么如何进行赋予权限呢,也就是如何操作将权限给对应的用户呢:
找到MyUserDetailsService类,其中的loadUserByUsername方法的代码如下:
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
//只有该一个接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库得到信息,具体如何得到的,并不需要知道
//因为我们只需要学习对应的spring security,而不是学习该项目
User byUsername = userService.findByUsername(username);
if(byUsername == null){
throw new UsernameNotFoundException("用户没有找到,"+username);
}
//从这里开下面的进行改变
//权限的集合,前面我们并不需要操作权限,所以设置为空集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
//现在我们需要操作权限了,所以这里进行赋予权限
if(username.equalsIgnoreCase("admin")){
//SimpleGrantedAuthority是GrantedAuthority的实现类
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
//当是admin用户时,给他ROLE_ADMIN(下面的权限管理),由于前面操作"/user/**"时
//需要这个权限,所以我们登录后,只要是该用户,就可以访问
}else{
authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT"));
}
//到这里结束改变
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{bcrypt}"+byUsername.getPassword()
//密码的加密与否,这里是加密对比的(对比这个),而不是他去加密
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//根据权限的集合,知道有什么权限,从而,操作可以操作设置的对应的访问
//但我们发现,实际上这里只是根据用户名,给加上权限的,也就是说
//一般是我们自己进行条件设置,哪个用户可以操作什么
return userDetails;
}
重启项目,清除缓存,登录后,点击用户管理,可以发现,得到数据了,但是如果我们使用其他的用户重新登录
也就使得对应的信息对比刷新了,会发现,对应的权限并没有添加,所以得不到数据,至此权限设置操作成功
那么有个问题,如下:
/*
如果将http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");变成
http.authorizeRequests().antMatchers("/user/**").hasRole("A");或者
http.authorizeRequests().antMatchers("/user/**").hasRole("a");
可以吗,答:可以,只要你添加的权限集合中,包括了对应的权限即可
如authorities.add(new SimpleGrantedAuthority("ROLE_A"));
authorities.add(new SimpleGrantedAuthority("ROLE_a"));等等
即除了ROLE_是固定的,后面的可以随便写,只要对应了,当然有大小写之分的,即a不是A
所以说,对应的名称基本上是可以随便写的,但一般我们为了更方便的识别,尽量使用见名知意的单词
*/
从上面可以知道,我们可以手动的给对应的访问添加一个权限限制,只要对应的对比中有对应的权限,就可以操作该访问
使得决定一个用户是否可以操作访问,主要需要该用户被添加了什么权限
但当我们没有权限时,对应的错误页面是不好看的,可以通过设置,将对应的错误页面进行改变吗,代码如下:
在lagou包下,创建handle包,并在该包下创建MyAccessDeniedHandler类并实现AccessDeniedHandler接口:
package com.lagou.handle;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义权限不足的处理
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse
httpServletResponse, AccessDeniedException e) throws IOException,
ServletException {
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write("权限不足,联系管理员");
}
}
然后再配置类SecurityConfiguration里的configure(HttpSecurity http)方法里加上如下:
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
//这里就是操作权限失败后,进行操作的方法,实际上不只是权限,csrf除了登录的操作失败不会到这里来
//其他的失败会到这里来(如前面不防护报403错误的地方,就操作这个页面了,甚至优先于post和get的对比错误)
//即需要传递post,但设置了@GetMapping
记得注入如下:
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
重启项目,清除缓存,试着测试一下,发现是我们自己定义的页面值了
我们回到配置类SecurityConfiguration里的configure(HttpSecurity http)方法里加上如下:
//设置/product开头的请求,只要是本机,且有ADMIN或者PRODUCT的两个权限的其中一个就可以访问
http.authorizeRequests().antMatchers("/product/**")
.access("hasAnyRole('ADMIN','PRODUCT') and hasIpAddress('127.0.0.1')");
//我们可以发现,要操作多个权限,需要使用access来进行操作,and代表一起,当然还有or或等等
//也可以发现,一般都会加上ADMIN使得该权限也可以操作这个
我们进行重启项目,清除缓存,登录后,看看是否可以访问数据管理的商品管理,但是我们发现,访问失败
主要是因为对应的ip需要是127.0.0.1,我们可以将and修改成or进行访问,或者将localhost修改成127.0.0.1进行访问
那么为什么localhost没有在服务器变成127.0.0.1呢,一般情况下,在dns操作或者本机操作时
的确会变成127.0.0.1,但是请求的url却并没有变成127.0.0.1,也就是说,他提供对方地址
但url还是没有变化的(并没有参与真正的地址)
由于在服务器中,一般会将localhost变成0:0:0:0:0:0:0:1,所以需要进行改变成127.0.0.1(因为这是大多数电脑默认的,一般没有对应的配置就是0:0:0:0:0:0:0:1)
在Web 安全表达式中引用自定义Bean授权:
定义自定义授权类:
在service包里面的impl包下,创建MyAuthorizationService类:
package com.lagou.service.impl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查是否有对应的访问权限,虽然可以不检查而直接返回,但这是不好的习惯
* @param authentication
* @param request
* @param id
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request){
//得到对应的信息,即UserDetails的信息,该User是UserDetails的实现类
User user = (User) authentication.getPrincipal();
//获取用户的权限
Collection<GrantedAuthority> authorities = user.getAuthorities();
// 获取用户名
String username = user.getUsername();
// 如果用户名为admin,则不需要认证
if (username.equalsIgnoreCase("admin")) {
return true;
} else {
// 循环用户的权限,判断是否有ROLE_ADMIN权限,有则返回true
for (GrantedAuthority authority : authorities) {
String role = authority.getAuthority();
if ("ROLE_ADMIN".equals(role)) {
return true;
}
}
}
return false;
}
}
该类主要是用来确定权限的获取的
在配置类SecurityConfiguration的configure(HttpSecurity http)加上如下:
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
//会执行实例myAuthorizationService类的check方法,只要返回true,就可以访问
这时,我们重启项目,清除缓存,注释掉如下:
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
我们可以发现,他直接得到我们设置的信息,虽然该信息可以并不用操作,只要返回了true即可
这样就不需要直接进行比较了,而是使用类返回值进行比较(虽然可以直接的返回true)
有一个问题,需要说明:
/*
假设http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");,简称为直接的对比
和http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");,简称为类对比
一起,会怎么样
答:使用类操作的会进行覆盖,也就是说,他后进行操作,他的结果放行使得成立
所以这时候就算你这样写http.authorizeRequests().antMatchers("/user/**").hasRole("A");
没有对应的权限对比,也会可以访问
最后注意:该类需要扫描的,因为不扫描,又怎么会找到并执行方法呢
如果不加,一般得不到值,也就会报错
注意:对于上面路径的覆盖,有如下的解释
路径>**>*>,相同的则是类优先
所以说,实际上并不是覆盖,而是选择
比如类:/user/**,直接的对比:/user/**,则使用类
若类:/user/aa/**,直接的对比:/user/**,则使用直接的对比,因为路径>**
若类:/user/*,直接的对比:/user/*,则使用类
若类:/user/aa/*,直接的对比:/user/aa/*,则使用类
若类:/user/*,直接的对比:/user/**,则使用直接的对比,因为**>*
所以会有如上的覆盖(实际上是选择,即对应的操作并没有直接对比,或者执行方法等等)情况
那么以此类推,若类:/user/a,直接的对比:/user/a,则使用类,所以说,相同的类优先
回忆:**代表当前路径以及其子路径,*代表当前路径,当然有些使用*也会代表当前路径及其子路径
但在服务器里面,通常情况下*代表当前路径
一般情况下,在不代表路径时,*代表所有的意思
*/
携带路径变量:
改变对应的MyAuthorizationService类:
package com.lagou.service.impl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查是否有对应的访问权限,虽然可以不检查而直接返回,但这是不好的习惯
* @param authentication
* @param request
* @param id
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request,Integer id){
if (id > 10) {
return false;
}
return true;
}
}
将如下进行改变:
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
//修改成如下:
//使用自定义Bean授权,并携带路径参数
http.authorizeRequests().antMatchers("/user/delete/{id}").
access("@myAuthorizationService.check(authentication,request,#id)");
//#id得到路径里面的{id}中的id值,如/user/delete/1,那么#id就是1,到方法里面,也就是1
//使得只要对应路径的id的值大于10,就不能访问
//由于不是**或*的覆盖不了**或*的,所以我们需要注释掉如下:
//http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
//否则还是会删除的,除非他是/user/*,一个*,因为并没有覆盖到,需要对应路径
Method安全表达式:
针对方法级别的访问控制比较复杂,spring security提供了4种注解分别是:
@PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter
开启方法级别的注解配置:
在security配置类中添加注解:
//部分代码
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启注解支持
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
接下来找到UserController类,改变对应的方法:
/**
* 查询所有用户
*
* @return
*/
@RequestMapping("/findAll")
@PreAuthorize("hasRole('ADMIN')") //访问这个方法需要ADMIN权限,简称为指定
public String findAll(Model model) {
List<User> userList = userService.list();
model.addAttribute("userList", userList);
return "user_list";
}
//@ProAuthorize : 注解适合进入方法前的权限验证
/*
那么有个问题,假设有对应的http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
变成了http.authorizeRequests().antMatchers("/user/**").hasRole("ADMI");
则谁优先呢,答:直接的对比优先
如果配置类里的configure((HttpSecurity http)方法操作了
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMI");
那么选择这个,进行覆盖
当然覆盖与前面的是一样的,指定(注解指定)<直接<类
所以在测试时,记得注释掉对应的直接对比或者类的操作,否则可能就算有ADMIn权限,因为被覆盖了,使得权限还是不足
*/
在注释掉对应的直接对比或者类的操作时,我们进行登录,进行测试,只要不是ADMIN权限的用户(我们手动的进行添加操作的)
那么就不能访问该方法,甚至可以说是该路径,相当于在配置类里configure((HttpSecurity http)方法里配置如下:
http.authorizeRequests().antMatchers("/user/findAll").hasRole("ADMIN");
只是他是注解的形式的
继续在UserController类,改变对应的方法:
/**
* 用户修改页面跳转
*
* @return
*/
@RequestMapping("/update/{id}")
@PreAuthorize("#id<3") //只有id小于3的可以访问,而不是小于3的不能访问,因为返回true,就代表可以访问
//即针对参数的权限控制
//该id是对应的Integer id的值,而不是{id},因为Integer id的值他基本与{id}对应
//所以当如/update/1,那么该#id就是1
//如果Integer id是null,那么默认的#id<3这个判断是true,即默认是true
public String update(@PathVariable Integer id, Model model) {
User user = userService.getById(id);
model.addAttribute("user", user);
return "user_update";
}
继续进行测试,发现的确如此
那么到这里可以发现,实际上权限只是规则而已,为什么这样说呢
因为他是我们制定的规则,只有满足该规则才可访问,权限的设置,也就是满足规则的一种方式
而设置id<3也是一种满足规则的一种方式
继续在UserController类,改变对应的方法:
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
//returnObject:返回参数,这里也就是User
//对应的判断,返回true可以访问,这里就是,只能我自己查询自己的用户,而不能查询其他用户
@PostAuthorize("returnObject.username==authentication.principal.username")
public User getById(@PathVariable Integer id) {
//获取认证的信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication);
System.out.println(RememberMeAuthenticationToken.class.
isAssignableFrom(authentication.getClass()) == true);
//如果返回true,代表这个登录认证的信息,来源于自动登录
if(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass()) == true){
//一般来说,spring security在捕获异常时,会重定向到认证页面,一般并不会打印异常的信息
throw new RememberMeAuthenticationException("认证来源于RememberMe");
}
User user = userService.getById(id);
return user;
}
/*
@PostAuthorize:@PostAuthorize在方法执行后再进行权限验证,适合验证带有返回值的权限
Spring EL提供返回对象能够在表达式语言中获取到返回对象的 returnObject
returnObject:代表return返回的值
*/
继续进行测试,访问"/user/1"或者"/user/2",端口域名使用自己的
发现,只有前面一个"/user/1"可以,因为得到的数据中也是admin,所以可以访问
使得只能自己访问自己
继续在UserController类,改变对应的方法:
/**
* 用户删除-多选删除
*
* @return
*/
@GetMapping("/delByIds")
@PreFilter(filterTarget = "ids",value = "filterObject%2==0") //剔除参数为奇数的值
//规则并不是一定操作权限,也可以操作数据,这里就是操作对应的List<Integer> ids中剔除值为奇数的值
//实际上也就是保留filterObject%2==0返回true的值
//filterObject代表ids里面集合中的元素,自然是依次判断的
public String delByIds(@RequestParam(value = "id") List<Integer> ids) {
for (Integer id : ids) {
System.out.println(id);
}
return "redirect:/user/findAll";
}
//@PreFilter:可以用来对集合类型的参数进行过滤,将不符合条件的元素剔除集合
访问"/user/delByIds?id=5&id=6",看他的打印,会发现,他只打印了6,至此操作成功
继续在UserController类,改变对应的方法:
/**
* 查询所有用户-返回json数据
*
* @return
*/
@RequestMapping("/findAllTOJson")
@ResponseBody
@PostFilter("filterObject.id%2==0")
//剔除所有奇数的用户信息,我们发现,他操作的是返回值的剔除,而不是方法的剔除
//即后进行剔除,而不是@PreFilter这样的先进行剔除
public List<User> findAllTOJson() {
List<User> userList = userService.list();
System.out.println(userList);
return userList;
}
//@PostFilter:可以用来对集合类型的返回值进行过滤,将不符合条件的元素剔除集合
进行测试,访问,发现的确如此
总结:
/*
@ProAuthorize : 注解适合进入方法前的权限验证
@PostAuthorize:@PostAuthorize在方法执行后再进行权限验证,适合验证带有返回值的权限
对应判断得到的为true时,表示可以访问,当然也可以直接的设置true和false,但并不提倡
Spring EL提供返回对象能够在表达式语言中获取到返回对象的returnObject
returnObject:代表return返回的值
@PreFilter:可以用来对集合类型的参数进行过滤,将不符合条件的元素剔除集合,操作参数,先剔除
@PostFilter:可以用来对集合类型的返回值进行过滤,将不符合条件的元素剔除集合,操作返回值,后剔除
*/
为了更加的演示权限的操作,看如下
接下来我们再次操作MyAuthorizationService类,代码如下:
package com.lagou.service.impl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* 自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查是否有对应的访问权限,虽然可以不检查而直接返回,但这是不好的习惯
*
* @param authentication
* @param request
* @param id
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request) {
User principal = (User) authentication.getPrincipal();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
if ("admin".equalsIgnoreCase(principal.getUsername())) {
return true;
} else {
//获取路径
String requestURI = request.getRequestURI();
//当访问user时,需要ADMIN权限才可访问
if (requestURI.contains("/user")) {
for (GrantedAuthority grantedAuthority : authorities) {
if ("ROLE_ADMIN".equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
//当访问product时,需要PRODUCT权限才可访问
if (requestURI.contains("/product")) {
for (GrantedAuthority grantedAuthority : authorities) {
if ("ROLE_PRODUCT".equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
//实际上ADMIN是都可以访问的,只是这里为了突出分开,所以给出不同的结果
/*
就如前面写过的
for (GrantedAuthority authority : authorities) {
String role = authority.getAuthority();
if ("ROLE_ADMIN".equals(role)) {
return true;
}
}
直接这个循环即可,使得不同判断,只要你是ADMIN权限,就可以访问
*/
}
return false;
//当都不满足时,自然是访问不了的
}
}
配置类SecurityConfiguration的configure(HttpSecurity http)方法:
//修改如下:
//使用自定义Bean授权
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
启动测试,执行断点,可以测试admin和其他用户的操作,然后观看即可
一般其他用户的操作是没有权限的,基本只有admin有,因为这里是这样的设置
最后给出一个问题,如果出现如下,是什么情况:
/*
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMI");
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
当上面两个一起操作时,对应的是或者关系还是与关系,还是覆盖关系(后覆盖,和前覆盖)
后覆盖:写在后面的覆盖前面的
前覆盖:写在前面的覆盖后面的
答:是或者关系,相当于
http.authorizeRequests().antMatchers("/user/**")
// .access("hasAnyRole('ADMIN','ADMI')");
*/
至此,由于类的扩展性比较强,所以在一些情况下,我们一般操作类,来实现权限操作
如上面的自定义授权类MyAuthorizationService,当然也可以与数据库进行交互
基于数据库的RBAC数据模型的权限控制:
我们开发一个系统,必然面临权限控制的问题,不同的用户具有不同的访问、操作、数据权限
形成理论的权限控制模型有:
自主访问控制(DAC:Discretionary Access Control)
强制访问控制(MAC:Mandatory Access Control)
基于属性的权限验证(ABAC:Attribute-Based Access Control)等
最常被开发者使用也是相对易用、通用的就是RBAC权限模型(Role-Based Access Control)
RBAC权限模型简介:
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制
模型中有几个关键的术语:
用户:系统接口及访问的操作者
权限:能够访问某接口或者做某操作的授权资格
角色:具有一类相同操作权限的总称
RBAC权限模型核心授权逻辑如下:
某用户是什么角色?
某角色具有什么权限?
通过角色对应的权限推导出用户的权限(前面的72章博客也有类似的思想)
RBAC的演化进程:
用户与权限直接关联:
想到权限控制,人们最先想到的一定是用户与权限直接关联的模式
简单地说就是:某个用户具有某些权限,如上图
张三具有所有权限他可能是一个超级管理员
李四,王五具有添加商品和审核商品的权限有可能是一个普通业务员
这种模型能够清晰的表达用户与权限之间的关系,足够简单,但同时也存在问题:
现在用户是张三、李四,王五,以后随着人员增加,每一个用户都需要重新授权
即操作人员使得他的权限发生变更,需要对每个一个用户重新授予新的权限
特别的当我们不需要某个权限时,每个用户都需要进行解除关联,太麻烦了
用户与角色关联:
这样只需要维护角色和权限之间的关系就可以了,如果业务员的权限发生变更
只需要变动业务员角色和权限之前的关系进行维护就可以了,用户和权限就分离开来了,使得方便许多
而不用一个一个授权或者解除权限
但你可能会有疑惑:这不就是将用户操作变成了角色操作吗,同样的,角色需要一个一个授权或者解除权限
但也要知道,用户是变化的,而角色是不变的,也就是说,角色一般只有那几个,而用户是成千上万个,所以说是方便了许多
因为我们改变一个角色,就会使得对应的很多用户都进行改变,而不是我们手动改变这些很多的用户
换言之,就是将对应的权限变成一个集合(角色),我们只需要使得用户指向该集合(角色)即可,而不是一个一个的指向权限
最后要注意:操作角色时,对应的权限因为大范围的改变,可能会使得某些人的权限信息异常,比如他是特别的
他对应的权限是都不需要的,还没有重新指向,当然这是在某些情况下
实际上角色只会有那么几个,基本不会出现特例,就算出现了。及时发现并重新赋予角色即可,当然这是我们的问题
如下图:
基于RBAC设计权限表结构:
一个用户有一个或多个角色
一个角色包含多个用户
一个角色有多种权限
一个权限属于多个角色
实际上使用数据来进行操作也是利用规则,将表数据进行对比的规则等等
可以简单的理解,权限,就是我们在对应访问给加上某些数,对应的用户也加上某些数,如果这些数相同,就可以访问
基于Spring Security 实现RBAC权限管理:
动态查询数据库中用户对应的权限,找到对应的PermissionMapper接口,可以发现,已经写好了,所以并不需要操作
所以我们找到MyUserDetailsService类,修改如下:
package com.lagou.service.impl;
import com.lagou.domain.Permission;
import com.lagou.domain.User;
import com.lagou.mapper.PermissionMapper;
import com.lagou.service.PermissionService;
import com.lagou.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
*基于数据库完成认证
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
/**
* 根据用户名查询用户
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
//只有该一个接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库得到信息,具体如何得到的,并不需要知道
//因为我们只需要学习对应的spring security,而不是学习该项目
User byUsername = userService.findByUsername(username);
if(byUsername == null){
throw new UsernameNotFoundException("用户没有找到,"+username);
}
//权限的集合,前面我们并不需要操作权限,所以设置为空集合
Collection<GrantedAuthority> authorities = new ArrayList<>();
//基于数据库查询用户对应的权限
List<Permission> byUserId = permissionService.findByUserId(byUsername.getId());
for(Permission permission : byUserId){
authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
//上面使得添加对应用户的权限,由于数据库表的原因,那么用户只能操作对应的权限
//这也使得,不需要我们手动的进行规则操作了,数据库已经操作完毕了
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
username //用户名
,"{bcrypt}"+byUsername.getPassword() //密码
,true //用户是否启用,true代表启用,false代表未启用
,true //用户是否过期,true代表没有过期,false代表过期
,true //用户凭证是否过期,true代表没有过期,false代表过期
,true //用户是否锁定,true代表未锁定,false代表锁定
,authorities //权限的集合
);
//"{noop}"+byUsername.getPassword()代表不使用密码加密
//实际上是{noop}代表了不使用的意思,而该参数正好是密码的参数,所以是不使用密码加密
return userDetails;
}
public static void main(String[] args) {
//使用加密的类,进行加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//得到123456加密后的值
String code = bCryptPasswordEncoder.encode("123456");
System.out.println(code); //$2a$10$qbhLCxg43v7bv0oPk1dNz.vXCL.qT4.kEmMRdTJlh/CFY9v.OwAtq
}
}
接下来,我们将注解操作权限全部注释,原来的权限全部注释
在配置类SecurityConfiguration里的configure(HttpSecurity http)方法添加如下:
//查询数据库所有权限列表
List<Permission> list = permissionService.list();
for(Permission permission:list){
//添加请求权限
http.authorizeRequests().antMatchers(permission.getPermissionUrl()).
hasAnyAuthority(permission.getPermissionTag());
//hasAnyAuthority没有前缀ROLE_
}
//这样就使得,没有用户只能根据数据库来进行操作权限
重启项目,清除浏览器缓存,进行测试,发现,对应的用户的权限不同
基于页面端标签的权限控制:
在jsp页面或者thymeleaf模板页面中我们可以使用spring security提供的权限标签来进行权限控制
要想使用thymeleaf为SpringSecurity提供的标签属性,首先需要引入thymeleaf-extras-springsecurity依赖支持
在pom 文件中的引入springsecurity的标签依赖thymeleaf-extras-springsecurity5:
<!--添加thymeleaf为SpringSecurity提供的标签依赖,需要对应的spring security依赖存在,否则启动报错-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
在html文件里面申明使用方式:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
常用SpringSecurity的标签属性介绍:
/*
判断用户是否已经登陆认证,引号内的参数必须是isAuthenticated()
sec:authorize="isAuthenticated()" 若登录认证了,返回true,没有登录认证,则返回false
true则显示其标签的内容,false则不显示其标签的内容,虽然必须登录认证后才可以访问
所以这个只是为了以防万一的,虽然基本不会操作到不显示(因为基本要认证后才可访问该页面,也才可使得他进行操作)
但这时,也基本是认证的了
获得当前用户的用户名,引号内的参数必须是name
sec:authentication="name" 使得标签的内容为对应的用户名
判断当前用户是否拥有指定的权限,引号内的参数为权限的名称
sec:authorize="hasRole('role')"
若用户满足了该权限,则返回true,即显示标签的内容,否则返回false,即不显示该标签的内容
*/
SpringSecurity标签的使用:
在index.html里修改部分代码如下:
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name"></span>
<img src="images/y.jpg" class="radius-circle rotate-hover" height="50" alt=""/>
</div>
<div sec:authorize="hasAuthority('user:findAll')">
<h2><span class="icon-user"></span>系统管理</h2>
<ul style="display:block">
<li><a href="/user/findAll" target="right"><span class="icon-caret-right"></span>
用户管理</a></li>
<li><a href="javascript:void(0)" onclick="toCors()" target="right"><span
class="icon-caret-right"></span>跨域测试</a></li>
</ul>
</div>
<div sec:authorize="hasAuthority('product:findAll')">
<h2><span class="icon-pencil-square-o"></span>数据管理</h2>
<ul>
<li><a href="/product/findAll" target="right"><span class="icon-caret-right"></span>
商品管理</a></li>
</ul>
</div>
启动项目,清除缓存,登录不同的用户,发现,对应的显示是不同的,至此操作完毕
至此,实现了对应的页面数据显示的问题(根据权限来的)
源码分析:
在源码分析之前,最好结合自己调试的代码为主
过滤器链加载源码:
过滤器链加载流程分析:
在前面讲解的时候说springSecurity中主要功能是由过滤器链来完成的,那么spring boot是如何加载这个流程的呢?
过滤器链加载流程源码分析:
spring boot启动中会加载spring.factories文件,在文件中有对应针对Spring Security的过滤器链的配置信息
通过ctrl+n查找文件,输入spring.factories(属于spring-boot-autoconfigure-2.3.4.RELEASE.jar里面的)进行查找
也可以直接去External Libraries(一般称为外部依赖)里找到spring-boot-autoconfigure-2.3.4.RELEASE.jar,然后进入META-INF目录
然后就可以找到spring.factories文件
当我们进去spring-boot-autoconfigure-2.3.4.RELEASE.jar里面的这个spring.factories文件后
通过ctrl+f查找SecurityFilterAutoConfiguration即可
# 安全过滤器自动配置
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
SecurityFilterAutoConfiguration类(部分代码):
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableConfigurationProperties({SecurityProperties.class})
@ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class})
@AutoConfigureAfter({SecurityAutoConfiguration.class})
public class SecurityFilterAutoConfiguration {
//该类加载完后,一般会触发SecurityAutoConfiguration类
我们点击SecurityAutoConfiguration类看看(部分代码):
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({DefaultAuthenticationEventPublisher.class})
@EnableConfigurationProperties({SecurityProperties.class})
//一般有默认的用户名和密码,虽然上一级的也有这个注解,操作基本也是一样
//但基本这个会覆盖,所以就没有说明上一个的该注解了
@Import({SpringBootWebSecurityConfiguration.class,
WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class})
public class SecurityAutoConfiguration {
//注解一般起到辅助作用,如使用注解的值,或者操作值等等
上面的SecurityProperties一般操作对应的默认用户名和默认密码,我们点进去看看:
可以看到如下:
//部分代码,不同的版本,代码可能不同,但总体来说是一样的
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList();
private boolean passwordGenerated = true;
发现,的确有默认的用户名和密码,因为注解的作用,使得被加载,具体如何加载就不说明了
回到前面,我们查看对应的如下:
@Import({SpringBootWebSecurityConfiguration.class,
WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class})
点击WebSecurityEnablerConfiguration类进去,他一般是开启web安全启动配置:
上面点击进去后,部分内容如下:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnBean({WebSecurityConfigurerAdapter.class})
@ConditionalOnMissingBean(
name = {"springSecurityFilterChain"}
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@EnableWebSecurity //该注解开启security安全功能
public class WebSecurityEnablerConfiguration {
public WebSecurityEnablerConfiguration() {
}
}
接下来我们点击@EnableWebSecurity进去,部分代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}
//从上面看,主要的类是WebSecurityConfiguration.class,也就是安全的配置类
//该配置类里面就生成了过滤器链
//其中@EnableGlobalAuthentication注解里面的部分内容如下:
/*
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({AuthenticationConfiguration.class})
@Configuration
public @interface EnableGlobalAuthentication {
}
其中@Import({AuthenticationConfiguration.class})中的AuthenticationConfiguration进行了加载
他主要是配置认证信息
*/
接下来我们点击WebSecurityConfiguration类进去(主要的代码如下):
@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null &&
!this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter =
(WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(
new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
return (Filter)this.webSecurity.build();
}
他返回的就是过滤器链
我们可以看到,他的注解是@Bean,他的@Bean名称(name)是springSecurityFilterChain
也就是说,过滤器链也是在实例里面的,且key是springSecurityFilterChain
为了进行测试,我们可以在UserController类中操作如下代码:
@Autowired
@Qualifier("springSecurityFilterChain")
private Filter filter;
//进行注入,因为过滤器很多,我们需要精确的注入
/**
* 查询所有用户
*
* @return
*/
@RequestMapping("/findAll")
//@PreAuthorize("hasRole('ADMIN')") //访问这个方法需要ADMIN权限
public String findAll(Model model) {
List<User> userList = userService.list();
model.addAttribute("userList", userList);
System.out.println(filter); //这里进行打印
System.out.println(1);
return "user_list";
}
通过打印结果和启动中的过滤器的加载,可以发现
打印结果中对应的过滤器的对应内容与启动中的日志过滤器信息基本都是一致的,也就是说,的确是在实例中
所以我们在启动时,spring boot的确会加载spring.factories文件,然后操作对应的过滤器链
即启动日志的打印信息就是该过滤器链的具体内容信息(给出部分),之所以这样说,是因为打印的过滤器链信息还有其他信息
并不是只有真正的过滤器链的信息,只是日志中,给出了该信息而已,具体如何自己进行对比即可:
比如说我的:
/*
//日志信息
[
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@22b7ef2b, org.springframework.security.web.context.SecurityContextPersistenceFilter@7afe0e67, org.springframework.security.web.header.HeaderWriterFilter@23f60b7d, org.springframework.web.filter.CorsFilter@6d421fe, org.springframework.security.web.csrf.CsrfFilter@5ce3409b, org.springframework.security.web.authentication.logout.LogoutFilter@37d0d373, com.lagou.filter.ValidateCodeFilter@6f5d0190, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4af12a63, org.springframework.security.web.session.ConcurrentSessionFilter@77ccded4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@31228d83, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16bbaab3, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@72eed547, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3c17bd0b, org.springframework.security.web.session.SessionManagementFilter@4aa3fc9a, org.springframework.security.web.access.ExceptionTranslationFilter@5e37fb82, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6987a133
]
//打印信息
FilterChainProxy[Filter Chains: [[ Ant [pattern='/images/**'], []], [ Ant [pattern='/css/**'], []], [ Ant [pattern='/js/**'], []], [ Ant [pattern='/code/**'], []], [ any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@22b7ef2b, org.springframework.security.web.context.SecurityContextPersistenceFilter@7afe0e67, org.springframework.security.web.header.HeaderWriterFilter@23f60b7d, org.springframework.web.filter.CorsFilter@6d421fe, org.springframework.security.web.csrf.CsrfFilter@5ce3409b, org.springframework.security.web.authentication.logout.LogoutFilter@37d0d373, com.lagou.filter.ValidateCodeFilter@6f5d0190, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4af12a63, org.springframework.security.web.session.ConcurrentSessionFilter@77ccded4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@31228d83, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16bbaab3, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@72eed547, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3c17bd0b, org.springframework.security.web.session.SessionManagementFilter@4aa3fc9a, org.springframework.security.web.access.ExceptionTranslationFilter@5e37fb82, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6987a133
]]
]
]
*/
可以发现,只有部分一样,那个部分就是真正的过滤器链的信息
现在我们在对应的方法上打上断点,如下:
@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
//下面这一段代码打上断点,因为我们直接的进入容易到接口
//那么就不好直接的查看了,且就算你找到对应的类,但细节的信息,却不好观看
//所以我们使用断点来进行细节的查看并可以忽略找类的细节
//虽然对应的接口只有一个实现类,所以这里主要是观看细节信息
boolean hasConfigurers = this.webSecurityConfigurers != null &&
!this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter =
(WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(
new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
return (Filter)this.webSecurity.build();
}
接下来启动项目,清除缓存,会发现,在启动时,就调试到这里了,也更加说明了日志信息是对应的生成的过滤器链的信息
/*
不同的版本,可能代码显示不同,但大致一样
我们调试到
return (Filter)this.webSecurity.build();
进入:
会到这里:
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = this.doBuild();
return this.object;
} else {
throw new AlreadyBuiltException("This object has already been built");
}
}
我们进入this.object = this.doBuild();
后面说明进入时,一般都是进入对应的方法的,可能后面会所有省略,注意即可
然后到这里(没有调试之前,这里是看接口的(但该接口只有一个实现类,所以并不影响手动查找观看):
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
其中this.init();是初始化的方法,我们进去(也就是进入,而不是点击进去,即是调试的进去)
后面说的进去和进入都是调试的进去,(手动的)点击进去或者进入,都是我们去进入,而不是调试的进入
部分代码如下:
private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
我们再次的进去Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
出现如下:
private Collection<SecurityConfigurer<O, B>> getConfigurers() {
List<SecurityConfigurer<O, B>> result = new ArrayList();
Iterator var2 = this.configurers.values().iterator();
while(var2.hasNext()) {
List<SecurityConfigurer<O, B>> configs = (List)var2.next();
result.addAll(configs);
}
return result;
}
接下来就是为什么使用调试的原因了,我们看看对应的this.configurers的值
调试时可以查看到,基本直接的找不会给出来,所以这就是为什么使用调试的原因
可以发现,有个类的相关地址名称
你对比一下配置类,可以发现,就是你的配置类的相关地址名称
也就是说,那么可以说,他是将我们配置类的信息放在了result的list集合中,并返回
好像只有该一个信息,因为size=1
接下来继续调试,一直下一步,直到回到上一个方法
即init()方法
部分代码如下:
Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
SecurityConfigurer configurer;
while(var2.hasNext()) {
configurer = (SecurityConfigurer)var2.next();
configurer.init(this);
}
我们进入configurer.init(this);,第一个获得基本上就是我们写的配置类信息
因为基本上对应的size=1,只有他(这里是)
进入后,到如下
public void init(WebSecurity web) throws Exception {
HttpSecurity http = this.getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor =
(FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
我们进入 HttpSecurity http = this.getHttp();
protected final HttpSecurity getHttp() throws Exception {
if (this.http != null) {
return this.http;
} else {
AuthenticationEventPublisher eventPublisher = this.getAuthenticationEventPublisher();
this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = this.authenticationManager();
this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
Map<Class<?>, Object> sharedObjects = this.createSharedObjects();
这里进行了创建
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder,
sharedObjects);
if (!this.disableDefaults) {
((HttpSecurity)((DefaultLoginPageConfigurer)((HttpSecurity)((HttpSecurity)
((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)
((HttpSecurity)this.http.csrf().and()).addFilter(new
WebAsyncManagerIntegrationFilter()).exceptionHandling()
.and()).headers().and()).sessionManagement()
.and()).securityContext().and()).requestCache()
.and()).anonymous().and()).servletApi().and()).apply(new
DefaultLoginPageConfigurer())).and())
.logout();
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
Iterator var6 = defaultHttpConfigurers.iterator();
while(var6.hasNext()) {
AbstractHttpConfigurer configurer = (AbstractHttpConfigurer)var6.next();
this.http.apply(configurer);
}
}
this.configure(this.http);
return this.http;
}
}
其中我们创建的
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
看看对应的http的值,一般我们只需要看对应的两个值:
filters和configurers这两个值,这时他们的size=0,即现在还没有值,继续调试
在上面的代码中,我们可以看到,有csrf(),他代表开启csrf防护,所以的确是默认开启csrf防护的
即前面说过的默认开启csrf防护就是因为这里进行了实现,他操作一个过滤器,放在configurers里面
在15个默认的过滤器中,其中有个org.springframework.security.web.csrf.CsrfFilter,他是生成该过滤器的配置类
一般放在configurers里面的是对应的配置类,用来生成对应的过滤器的,也就是第4个
其中addFilter(new WebAsyncManagerIntegrationFilter())就是添加一个过滤器,放在filters里面
该过滤器也就是15个默认过滤器的第一个,即
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter:
而这个addFilter方法内容如下(可能是一行的,所以我们手动点击进去):
public HttpSecurity addFilter(Filter filter) {
Class<? extends Filter> filterClass = filter.getClass();
if (!this.comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException("The Filter class " + filterClass.getName() + " does
not have a registered order and cannot be added without a specified order. Consider
using addFilterBefore or addFilterAfter instead.");
} else {
this.filters.add(filter); 这里就进行了添加
return this;
}
}
所以的确是进行了添加
后面的exceptionHandling()也是添加对应的过滤器的配置类,用来创建15个默认过滤器的如下:
org.springframework.security.web.access.ExceptionTranslationFilter,也就是第14个
后面的就不依次说明,可以自己进行尝试
因为这样的说明并没有意义
我们直接的下一步,当调试完后,看看对应的值即可
发现filters和configurers这两个值分别是1和10
我们直接调试到this.configure(this.http);
那么这个this是谁呢,看看前面,我们是进入this.getHttp();里面的,那么这个this是谁呢
再看前面,我们是configurer.init(this);进入的,发现,this就是configurer,由于那么就可以知道
该this是对应的配置类信息,或者说,就是配置类
那么this.configure(this.http);,也就是调用我们写的配置类的该方法,看看是否有对应的方法,不难看出
就是我们前面多次提到的configure(HttpSecurity http)这个方法
所以对应的http再操作时,为什么会使得覆盖等的信息,这就是原因,所以他可以被我们进行配置
为了验证,我们进入,发现,的确是我们写的方法
为了验证对应的过滤器是否添加,我们进去
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
到这里
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) {
this.comparator.registerBefore(filter.getClass(), beforeFilter);
return this.addFilter(filter);
}
进入return this.addFilter(filter);,再前面我们知道他是添加过滤器的,所以可以看看方法
public HttpSecurity addFilter(Filter filter) {
Class<? extends Filter> filterClass = filter.getClass();
if (!this.comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException("The Filter class " + filterClass.getName() + " does
not have a registered order and cannot be added without a specified order. Consider
using addFilterBefore or addFilterAfter instead.");
} else {
this.filters.add(filter);
return this;
}
}
实际上是一样的,我们看看对应的this(这时是http),所以可以看到filters和configurers这两个值
发现filters多出一个过滤器了
调试到 如下
http.authorizeRequests().antMatchers(permission.getPermissionUrl())
.hasAnyAuthority(permission.getPermissionTag());
这里执行后,对应的配置类就会添加一个,从而生成对应的过滤器,一般是最后一个过滤器,即
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
当调试完 http.formLogin()这个后,也会多出一个过滤器配置,从而生成对应的过滤器
调试到 rememberMe()也会多出一个配置类,从而生成对应的过滤器
调试到http.csrf().disable();,若有的话,则会删除对应的配置类
调试到http.cors().configurationSource(corsConfigurationSource());,配置类加一个
在后面,他们这些配置类会变成对应的过滤器的
调试完后,回到
this.configure(this.http);
return this.http;
然后再次回到
public void init(WebSecurity web) throws Exception {
HttpSecurity http = this.getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor =
(FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
先看看web的值,可以发现有个securityFilterChainBuilders,执行后,从size=0会变成size=1
点开他,发现,多出了值,也就是之前的http,当然该http的配置类创建的过滤器名称并不是一定会与过滤器名称对应
或者说,操作的过滤器与他的名称并不一定类似,且可能不会创建
当然可能后续也会进行添加或者删除(虽然基本是默认的),这里要注意
所以他的信息名称也并不是一定与日志的过滤器链的信息名称对应(主要是名称可能不对应)
具体看后面过滤器配置了创建过滤器时,对应的创建的过滤器与其名称对比就知道了
比如:
DefaultLoginPageConfigurer不会创建过滤器(这里是)
FormLoginConfigurer创建UsernamePasswordAuthenticationFilter,后面是他的一些配置
类似的一般是如下:
CsrfConfigurer创建CsrfFilter,
RememberMeConfigurer创建RememberMeAuthenticationFilter,等等当然并没有规则,只是类似而已
当然这些并不需要注意
一直向下调试,回到这里
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
进入this.configure();
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
while(var2.hasNext()) {
SecurityConfigurer<O, B> configurer = (SecurityConfigurer)var2.next();
configurer.configure(this);
}
}
也是获取配置类信息,并进行操作,进入 configurer.configure(this);
发现到了我们配置类的如下方法
configure(WebSecurity web)
打开web,可以看到ignoredRequests,在对应的放行操作之前,size=0,操作后
根据放行参数的个数,来决定,比如我这里
web.ignoring().antMatchers("/images/**","/css/**","/js/**","/code/**");
那么对应的size=4,且对应的值是这些参数,从而操作放行或者说不被拦截
继续调试,回到
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
进入O result = this.performBuild();
部分代码如下:
int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList(chainSize);
Iterator var3 = this.ignoredRequests.iterator();
其中chainSize得到放行数和对应的http的个数,加起来是5
然后根据这个5,创建一个list集合
再看后面的部分代码
while(var3.hasNext()) {
RequestMatcher ignoredRequest = (RequestMatcher)var3.next();
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest, new Filter[0]));
}
其中执行四次(因为对应的长度是4),将对应的放行信息放入创建的集合中
在看后面代码:
var3 = this.securityFilterChainBuilders.iterator();
while(var3.hasNext()) {
SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder =
(SecurityBuilder)var3.next();
securityFilterChains.add(securityFilterChainBuilder.build());
}
这里只会执行一次,因为对应的长度只有1
我们得到的对应的值,就是http,进入
securityFilterChains.add(securityFilterChainBuilder.build());,也就是执行了http的build()方法
进入后,看如下:
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = this.doBuild();
return this.object;
} else {
throw new AlreadyBuiltException("This object has already been built");
}
}
是否有点点熟悉,在前面我们也进入过一个的方法,只是对应的this,不同了
之前的是WebSecurity类型,而现在是HttpSecurity类型
进入this.object = this.doBuild();
代码如下:
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
回到了这里,注意:对应的this由WebSecurity类型变成了HttpSecurity类型
进入this.init();
然后进入
Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
代码如下:
private Collection<SecurityConfigurer<O, B>> getConfigurers() {
List<SecurityConfigurer<O, B>> result = new ArrayList();
Iterator var2 = this.configurers.values().iterator();
while(var2.hasNext()) {
List<SecurityConfigurer<O, B>> configs = (List)var2.next();
result.addAll(configs);
}
return result;
}
之前操作WebSecurity类型时,对应的this.configurers是配置类的地址信息,或者说就是配置类,即size=1
而现在的size=14,我这里是,当然不同的设置,会使得不同的
如前面的是否开启防护,开启少一个,没有开启,则不会少等等
也就是对应的过滤器的配置类的数量
回到上一级:
Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
SecurityConfigurer configurer;
while(var2.hasNext()) {
configurer = (SecurityConfigurer)var2.next();
configurer.init(this);
}
进入configurer.init(this);,由于是14个,那么会执行14次
我们依次的进入,当然对应过滤器配置类做的事情就不依次介绍了
总体来说,就是之前的我们自定义的配置类进行的操作,过滤器配置类也进行一次操作
只是使用的是他们的init方法
这里我们直接跳过,回到上一级
protected final O doBuild() throws Exception {
synchronized(this.configurers) {
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
this.beforeInit();
this.init();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
this.beforeConfigure();
this.configure();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
O result = this.performBuild();
this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
进入this.configure();
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
Iterator var2 = configurers.iterator();
while(var2.hasNext()) {
SecurityConfigurer<O, B> configurer = (SecurityConfigurer)var2.next();
configurer.configure(this);
}
}
然后进入 configurer.configure(this);,这里执行了每个过滤器配置类的配置方法,这里并不是操作放行了
先看看第一个配置类的该方法(this是HttpSecurity类型,而不是WebSecurity类型)
我们很容易在对应的方法中可以看到,他首先创建对应的过滤器,不难发现
该过滤器也就是该配置类所需要生成的过滤器,并在最后进行了添加,即执行http.addFilter方法
而正是因为参数是HttpSecurity类型,所以也就添加到了http里面去了,而由于地址的原因
那么WebSecurity类型里面的http的值也是一样的,当然后面的过滤器配置类也是如此
所以过滤器配置类的确进行了创建过滤器
但是对应的顺序基本是依次放在后面的,也就是说,根据配置类的顺序来进行
其中 O result = this.performBuild();里面就是进行了排序,进入
protected DefaultSecurityFilterChain performBuild() {
this.filters.sort(this.comparator);
return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
}
this.filters.sort(this.comparator);这一步就是进行排序
至此我们可以对照排序前后与之前的15个默认过滤器,可以发现,排序之前是操作过滤器的添加顺序
而排序之后,就是前面的默认15个过滤器的顺序了(基本是一致的)
最后返回时,返回的是对应的http过滤器链的真正信息,从而保存到集合中去
至此,上面的操作正好将我们创建的集合都添加完毕,这就是为什么之前,需要将长度进行相加而创建集合的原因
而该集合,就使得放行和过滤器链进行统一操作了,然后一系列的操作后,我们的初始化就操作完毕
总结:
首先得到对应的过滤器和过滤器的配置类(自定义的配置类可以进行添加和删除)
然后得到对应放行,再次进行所有的配置类统一操作初始化,并排序
从而操作了过滤器链的新结果以及放行的新结果
放行基本不会变化,可能过滤器链会发现变化,基本是受我们的自定义配置的影响
然后过滤器链的信息就得到了,当然是包含了对应的过滤器链的信息,还有其他的信息
具体看前面注入的打印,而不是日志的打印(取的是注入打印的过滤器链信息的一部分)
实际上注入的信息是一个代理对应集合的类(调试时可以知道),也就是返回代理该集合的类
那么自然除了过滤器链信息,也包含了其他的信息,因为放行的也在原来的集合
至此过滤器链的生成大致说明完毕,即的确返回了过滤器链的信息
虽然打印结果还有其他信息,基本是放行(拦截)的信息
*/
过滤器链的生成源码已经分析完毕,接下来我们看看认证流程的源码
认证流程源码:
认证流程分析:
在整个过滤器链中,UsernamePasswordAuthenticationFilter是来处理整个用户认证的流程的
所以下面我们主要针对用户认证来看下源码是如何实现的?
认证流程源码跟踪:
ctrl+n输入UsernamePasswordAuthenticationFilter进行查看
他的父类AbstractAuthenticationProcessingFilter里面的doFilter方法,执行了this.attemptAuthentication方法
当父类指向子类时,this代表子类对象,所以对应的父类引用执行doFilter方法
那么this.attemptAuthentication方法相当于UsernamePasswordAuthenticationFilter类来执行,这里就是这样操作的
所以在UsernamePasswordAuthenticationFilter类方法里面是如下(部分代码如下):
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse
response) throws AuthenticationException {
//我们在下面一行这里打上断点
if (this.postOnly && !request.getMethod().equals("POST")) { //判断是否是post
throw new AuthenticationServiceException("Authentication method not supported: " +
request.getMethod());
} else {
String username = this.obtainUsername(request); //得到用户名
String password = this.obtainPassword(request); //得到密码
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
打上断点后,看如下的解释:
/*
重启项目,清除缓存
我们现在随便输入用户名和密码,进行认证,那么就会跳转到上面设置的断点
我们可以先看判断,若不是post,那么就会报错,而报错就会重定向到认证页面,可以自己试一下
得到用户名和密码的操作如下:
进入 String username = this.obtainUsername(request);
可以看到如下return request.getParameter(this.usernameParameter);
进入String password = this.obtainPassword(request);
可以看到如下return request.getParameter(this.passwordParameter);
不难发现,的确是获取我们输入的用户名和密码
进入UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
出现如下:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal; 赋值用户名
this.credentials = credentials; 赋值密码
this.setAuthenticated(false);
设置认证结果,false代表现在还是未认证的状态,true则代表已经认证的状态
}
回到上一级
找到 UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
其中this.getAuthenticationManager()得到的是AuthenticationManager接口的变量
但对象是实现他的类(后面有分为对应的实现他的父子类)
我们进入return this.getAuthenticationManager().authenticate(authRequest);
部分代码如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
其他的代码就不写上了
我们可以找到
Iterator var8 = this.getProviders().iterator();
其中进入this.getProviders()
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
看后面的图片,看其内容
*/
/*
我们在后面找到provider.supports(toTest)进入,这是一个判断
第一个
public boolean supports(Class<?> authentication) {
return AnonymousAuthenticationToken.class.isAssignableFrom(authentication);
}
其中authentication是UsernamePasswordAuthenticationToken的类型变量执行的getClass();
进行比较,对应的类的supports就是比较是否是UsernamePasswordAuthenticationToken.class
很明显不是
第二个
public boolean supports(Class<?> authentication) {
return RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
}
可以发现,他们都不是,也就是都是false,而不是true
所以会调试到这里
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
进入 result = parentResult = this.parent.authenticate(authentication);
我们发现还是当前的方法,只是对于的this,变了
一般是原来的指向的父类,也是实现了对应的AuthenticationManager接口
我们看看父类的this.getProviders()
进入后,有如下
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
对应的图片结果:
*/
当然了,无论是对于的子类和父类,否则创建的,所以对于的地址一般并不会相同(重启项目后)
/*
继续找到后面的provider.supports(toTest)进入,这是一个判断
因为this.getProviders()中的this不同,所以结果也就不同,因为继承,若操作子类,自然可以操作父类,前提没有覆盖
但通常会覆盖,所以这里会使用子类的版本,这时我们指向父类,那么我们使用父类的版本,自然对应的值也就不同的
无论是变量还是方法都基本如此,这是java基础内容
发现是如下
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
至此,返回true,因为是UsernamePasswordAuthenticationToken.class
往后面调试,可以到 result = provider.authenticate(authentication);
使用对应的provider操作authenticate方法
由于并不是对应的父类或者子类的执行,所以不是当前的方法了
部分的代码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported");
});
找到
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" :
authentication.getName();
可以发现,执行了UsernamePasswordAuthenticationToken的getPrincipal()方法
进入得
public Object getPrincipal() {
return this.principal;
}
在之前说明过,他被赋值了,这里我因为登录的是admin,所以该值就是admin,得到用户名
通过authentication.getName();得到,实际上该authentication.getName();底层也是操作getPrincipal()得到
继续调试,可以找到
尝试从缓存中获取
UserDetails user = this.userCache.getUserFromCache(username);
他代表是否登录过,很明显,现在我们并没有登录(没有缓存),所以user是null
登陆过的话,这里应该是有值的,后面的检查自然也有值,且会使得登录成功
通常都是能够登录成功的,因为我们通常没有修改值的操作
继续调试,可以到如下:
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
他一般是用来检查我们的username的
我们进入后,有如下
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken
authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null,
which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
我们进去UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
在这之前,我们可以看看this.getUserDetailsService()的值,也就是父类的值
我们可以发现,值是我们自己编写的MyUserDetailsService类,也就是对比的数据
所以说他就是调用我们的MyUserDetailsService类的loadUserByUsername方法
由于前面说过,username一般是需要我们自己来进行判断的,所以该方法里面就使用了我们的自己判断
所以UserDetails loadedUser得到的就是对比的密码数据,换言之,就是对应的对比数据
继续调试,回到上一级
可以知道,上面说的是否登录的数据,被赋值了
我们调试到如下:
this.preAuthenticationChecks.check(user);
很明显,这是检查,一般是认证前(对比前)检查user状态(user也就是UserDetails的对比信息)
我们进入
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is
locked");
throw new
LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} else if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is
disabled");
throw new DisabledException(
AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is
expired");
throw new AccountExpiredException(
AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
可以发现,对应的检查都完毕,但并不是密码的检查,继续调试
找到
this.additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken)authentication);
这一步非常关键,因为是密码的对比,但却没有用户名的对比
所以也更加确定了对应的用户名需要我们自己来进行判断
所以在前面操作时,对应用户名的地方
基本是可以随便写的(没有因为判断发生错误的情况下,使得重定向,带后缀的,错误的后缀,前面说明过)
我们进入,有如下
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
其中 String presentedPassword = authentication.getCredentials().toString();获取密码
在后面的if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {中
我们进去matches方法,看看他做了什么
到如下
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return this.getPasswordEncoder().matches(rawPassword, encodedPassword);
}
继续进去matches方法
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
} else {
String id = this.extractId(prefixEncodedPassword); id代表加密的方式
得到对应的加密方式对象
PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword,
prefixEncodedPassword);
} else {
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
}
}
继续进去return delegate.matches(rawPassword, encodedPassword);这里
注意,由于delegate的值是根据对应的是否加密规则(方式)生成的
所以使用noop或者使用bcrypt操作的matches方法不同
其中rawPassword是密码,encodedPassword是对比的密码(即数据库的密码)
如果使用noop,那么进入如下:
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
很明显是直接的比较
而使用bcrypt,那么进入如下:
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
进入return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
这里就是真正的检查,我们输入的密码与数据库的密码进行加密的对比
至于如何的对比,就不说明了,无非基本就是三种情况
输入的加密后对比,数据库的解密后对比,根据他们两个来生成对应的数据库的一样的密码(算法导致)对比
通常情况下,是加密后对比,即输入的加密后进行对比
我们直接下一步,直到回到之前的
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
发现返回是是true,那么取反就是false
至此,只要是对比成功的,那么就不会报错,也就不会重定向(加后缀的)
直接继续调试(下一步),会到如下:
this.postAuthenticationChecks.check(user);
进入后
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account
credentials have expired");
throw new CredentialsExpiredException(
AbstractUserDetailsAuthenticationProvider.this.messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials
have expired"));
}
}
也是一个检查,但这是认证后(相当于认证后了,因为密码已经对比了,后面的基本会使得认证)
所以也称为认证后,或者对比后
检查user状态,前面的一个类似的是认证前的检查
即this.preAuthenticationChecks.check(user);
而不是this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
进入到 return this.createSuccessAuthentication(principalToReturn, authentication, user);
protected Authentication createSuccessAuthentication(Object principal, Authentication
authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null &&
this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
其中,使用父类的方法时,this还是自己的而不是父类的,如这里使用super,但this却是子类的,而不是父类的
因为是在子类里进行的操作,也就是说,相当于任然是子类调用(虽然看起来是父类的调用)
但他的调用却也是自己(因为是一体了)
只是super用来指定是否操作父类版本的而已,实际上还是相当于this(可以操作父类的this)
因为一般this是操作本类的
}
进入return super.createSuccessAuthentication(principal, authentication, user);
protected Authentication createSuccessAuthentication(Object principal, Authentication
authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new
UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
this.authoritiesMapper.mapAuthorities(user.getAuthorities()),相当于返回参数里的值,所以也就是
user.getAuthorities()得到权限信息
我们看到他又创建了UsernamePasswordAuthenticationToken,只是与之前不同的是,这里多了一个参数
我们进入看看
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities); 添加的权限信息,即对比信息中的权限信息
this.principal = principal; 得到的对比信息
this.credentials = credentials;
密码(原来的没有认证的UsernamePasswordAuthenticationToken得到的)
super.setAuthenticated(true);
}
与之前的对比,可以发现,super.setAuthenticated(true);是true,代表已经认证了
继续调试
直到回到 result = provider.authenticate(authentication);
再次调试
可以发现这个
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
他使得我们认证的UsernamePasswordAuthenticationToken的密码设置为null,因为我们已经认证了
那么为了防止数据的泄露,就设置为null(虽然总体来说进行了两次赋值),但结果都是赋值null,影响不大
再次调试,后面会进行返回
至此得到了一个新的UsernamePasswordAuthenticationToken,最后会进行返回
继续往后面走,回到 result = parentResult = this.parent.authenticate(authentication);
也就是说,最终的新的UsernamePasswordAuthenticationToken,还是回到了之前子类里面的那个地方
继续下一步,发现到了这里
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);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
也就是说,我们传递一个没有认证的UsernamePasswordAuthenticationToken得到一个认证的
UsernamePasswordAuthenticationToken
这就相当于我们输入用户名和密码后,认证成功
我们继续下一步,会到如下:
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
父类的方法,当前过滤器的父类,因为前面说过,是父类在执行该方法的,我们进去后
发现,this是子类的引用,那么也就说明前面的解释并没有错误
至此authResult得到的也就是认证的UsernamePasswordAuthenticationToken
当然对应的信息设置(true),是设置他的父类的,这里我们直接简称为
认证的UsernamePasswordAuthenticationToken
而false简称为没有认证的UsernamePasswordAuthenticationToken
继续下一步,到这里
session策略验证,这里并不需要查看
this.sessionStrategy.onAuthentication(authResult, request, response);
继续下一步,到这里
成功身份验证的操作
this.successfulAuthentication(request, response, chain, authResult);
我们进入后,有如下
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse
response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult,
this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
这里有个关键的代码,SecurityContextHolder.getContext().setAuthentication(authResult);
传入认证的UsernamePasswordAuthenticationToken
将认证信息给SecurityContextHolder,所以我们才能得到对应的信息,如对比信息
再次下一步:
this.rememberMeServices.loginSuccess(request, response, authResult);
他一般是操作"记住我"的,或者说有没有"记住我",这里我们并没有操作,我们继续下一步
找到如下
this.successHandler.onAuthenticationSuccess(request, response, authResult);
其中this.successHandler就是我们写的MyAuthenticationService类
在没有写对应配置之前,会使用默认的,前面说过了
在写了之后,这里是写的,即要知道,在过滤器链中我们早就进行了设置
所以而正是因为this是对应的UsernamePasswordAuthenticationFilter
所以我们在执行完http.formLogin()这个后
添加的创建该UsernamePasswordAuthenticationFilter过滤器的配置类后
其中后面的设置也就是为了以后创建过滤器而操作的
至此,会进入我们写的MyAuthenticationService类的onAuthenticationSuccess方法
至此认证流程源码解释完毕,以后我们重新登录时,一般会将得到的返回值看看是否有认证成功,而进行是否重新认证
当然认证失败后,一般是根据异常使得的,而出现了异常,那么对应的基本就不会是true了,所以会操作失败的方法
那么失败的源码分析也就不说明了,总不能所有的代码都进行说明吧,这是不可能的(因为代码太多)
只说明主要部分
总结:首先经历一系列的操作后,使得密码判断,且成功后,给出对应的已经认证的对象
最后保存,然后调用登录成功方法
*/
记住我流程源码:
在整个过滤器链中,RememberMeAuthenticationFilter是来处理记住我用户认证的流程的
所以下面我们主要针对记住我看下源码是如何实现的?
记住我流程分析:
记住我流程源码跟踪:
/*
我们通过登录认证的源码中,可以知道有个"记住我"的操作,接下来我们回到如下:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+
authResult);
}
//在下面这一行打上断开,进行测试
SecurityContextHolder.getContext().setAuthentication(authResult);
如果没有开启记住我,那么这个方法什么都没有做,因为this不同了
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult,
this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
上面打上断点后,我们重启项目,清除缓存,登录认证,并点击记住我
接下来我们不跳过this.rememberMeServices.loginSuccess(request, response, authResult);这里了,直接进入
如下:
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
其中 if (!this.rememberMeRequested(request, this.parameter)) {
进行判断是否携带remember-me(默认是这个),但是若你在前面配置类设置了别名,那么该值就是你设置的
因为在添加的过滤器配置类时,也进行了设置,那么在创建对应的过滤器时
会操作该设置的别名,在调试时,可以知道
我们进入if (!this.rememberMeRequested(request, this.parameter)) {
到如下:
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
request的对象的方法在其多个父类中的一个中,大概是因为注解或者其他的原因,使得this变成对应的父类调用
String paramValue = request.getParameter(parameter);
if (paramValue != null && (paramValue.equalsIgnoreCase("true") ||
paramValue.equalsIgnoreCase("on") ||
paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set
parameter '" + parameter + "')");
}
return false;
}
}
}
很明显,这里是直接从前端获得对应名称的值并进行判断
注意是判断,因为对应的对象并不是操作得到值的
只是底层会得到值,并进行判断
String paramValue = request.getParameter(parameter);
其中底层会根据该参数名称来进行从前端获取值
所以这就是为什么当我们设置后,对应的前端的name也要一致的原因
因为不一致,那么通过该参数来得到值时,一般返回的是null
若是一样的,在取出前端返回的值,前面我们设置的是true,当成返回值,从后面可以看到
只要是true,no,1,yes,则会返回true,使得后续进行执行
也说明了对应的值的确是多样的(可以回到前面"记住我"的说明,看看是否一样进行确认)
这时可以进行测试,看看对应的名称是否需要一致或者不需要,虽然前面说明过
注意一下:在有互踢的情况下,且到达最大会话数量时
一般操作不了记住我,因为这时,会默认重定向到认证页面(没有后缀)
测试后,当然你也可以不用测试
继续调试(下一步)
回到上一级
调试到如下:
this.onLoginSuccess(request, response, successfulAuthentication);
我们进入后,有如下
注意:当你是持久化的化,那么就是这个方法,因为对应的this的结果会根据持久化来进行改变
所以持久化和不持久化的操作,该方法不同,因为this不同,换言之
就是之前的this.rememberMeServices.loginSuccess(request, response, authResult);
中的this.rememberMeServices不同,可能底层进行了判断
有操作对应的方法,即这个.tokenRepository(getpersistentTokenRepository())
加上这个即操作持久化,前面说明过了
这里根据持久化为主,因为大多数都是操作持久化的
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
获取对应的名称
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
生成一个cookie信息
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username,
this.generateSeriesData(), this.generateTokenData(), new Date());
try {
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
我们进入 this.tokenRepository.createNewToken(persistentToken);
public void createNewToken(PersistentRememberMeToken token) {
this.getJdbcTemplate().update(this.insertTokenSql, new Object[]{token.getUsername(),
token.getSeries(), token.getTokenValue(), token.getDate()});
}
很明显,对应的this.insertTokenSql是sql语句信息(点击进去看看就可以知道)
而后面的是对应的用户名,以及前面说的
series:登录序列号,随机生成策略,用户输入用户名和密码登录时
该值重新生成,使用remember-me功能,该值保持不变
和
token:随机生成策略,每次访问都会重新生成
和
expiryTime:token过期时间
实际上这里的this,就是我们持久化方法的返回值
调试时可以知道,与方法的返回类型一样,该值的生成在启动时,就生成了,可能是操作对应的过滤器链时进行调用生成的
实际上就是一个值,而不是类似值
后面就是加到数据库里面的信息,这就是信息的来源
至于为什么会是返回值,这里并不说明了,因为总不能所有都要说明吧
接下来打开数据库,现在我们直接下一步,执行完后,到 this.addCookie(persistentToken, request, response);
再看数据库,会发现多出一条数据,即的确是生成的信息
再看 this.addCookie(persistentToken, request, response); ,根据名称,很容易发现,是给浏览器的cookie
那么对应的操作是否与添加到数据库的信息是一样的呢,我们进入后,有如下
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,
HttpServletResponse response) {
this.setCookie(new String[]{token.getSeries(), token.getTokenValue()},
this.getTokenValiditySeconds(), request, response);
}
传递了series:登录序列号,随机生成策略,用户输入用户名和密码登录时
该值重新生成,使用remember-me功能,该值保持不变
和
token:随机生成策略,每次访问都会重新生成
其中this.getTokenValiditySeconds()是基本固定的数据,1209600
是否看这个有点眼熟,没错,就是之前配置类里面的设置的失效时间
通过修改再次测试,可以发现,值改变了,实际上他的改变是过滤器配置类操作过滤器时,进行的设置
可以点开那个配置里,就可以知道了
进入setCookie方法
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request,
HttpServletResponse response) {
String cookieValue = this.encodeCookie(tokens); 这里通过series和token一起生成了cookie的值
以后的解析也就是这个,然后再根据数据库的用户名和他们再次解析的密码进行对比,然后认证,当然这是后话了
this.cookieName一般默认是remember-me,与设置的名称没有关系,所以基本是remember-me不变
Cookie cookie = new Cookie(this.cookieName, cookieValue);
cookie.setMaxAge(maxAge); 设置过期时间
cookie.setPath(this.getCookiePath(request));
if (this.cookieDomain != null) {
cookie.setDomain(this.cookieDomain);
}
if (maxAge < 1) {
cookie.setVersion(1);
}
if (this.useSecureCookie == null) {
cookie.setSecure(request.isSecure());
} else {
cookie.setSecure(this.useSecureCookie);
}
cookie.setHttpOnly(true);
response.addCookie(cookie); 这里就是将cookie给浏览器了
}
那么继续下一步,我们知道流程已经走完,我们直接的点击下一个断点(不是下一个,具体可以看34章博客)
使得不操作调试,但可以继续调试(虽然下一个也可,但通常需要一直到底才行,所以基本操作不了,可能也会退出)
记住之前的String cookieValue = this.encodeCookie(tokens);这个值
这时我们看看浏览器的cookie,发现的确有对应的cookie了,且名称是remember-me,值也是这个值
接下来我们退出浏览器,看看他是如何操作
我们到RememberMeAuthenticationFilter类里面,找到如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws
IOException, ServletException {
在这里打上断点
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder populated with remember-me token:
'" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(),
this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
return;
}
} catch (AuthenticationException var8) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me
token, as AuthenticationManager rejected Authentication returned by
RememberMeServices: '" +
rememberMeAuth + "'; invalidating remember-me token", var8);
}
this.rememberMeServices.loginFail(request, response);
this.onUnsuccessfulAuthentication(request, response, var8);
}
}
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with remember-me token, as it
already contained: '" + SecurityContextHolder.getContext().getAuthentication() +
"'");
}
chain.doFilter(request, response);
}
}
重新访问,那么会到上面的断点
一直下一步,会到
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
我们进入后
部分如下:
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response)
{
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
this.logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
this.cancelCookie(request, response);
return null;
} else {
UserDetails user = null;
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
return this.createSuccessfulAuthentication(request, user);
} catch (CookieTheftException var6) {
this.cancelCookie(request, response);
throw var6;
} catch (UsernameNotFoundException var7) {
this.logger.debug("Remember-me login was valid but corresponding user not
found.", var7);
} catch (InvalidCookieException var8) {
this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
} catch (AccountStatusException var9) {
this.logger.debug("Invalid UserDetails: " + var9.getMessage());
} catch (RememberMeAuthenticationException var10) {
this.logger.debug(var10.getMessage());
}
this.cancelCookie(request, response);
return null;
}
}
}
其中 String rememberMeCookie = this.extractRememberMeCookie(request);会从浏览器获得cookie
继续调试,会到String[] cookieTokens = this.decodeCookie(rememberMeCookie);
这里就进行了解密,也可以顺便对比一下数据库的数据,发现的确一样的
我们再次下一步,进入 user = this.processAutoLoginCookie(cookieTokens, request, response);
会看到这两个
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
得到对应的信息
再次调试,进入
PersistentRememberMeToken token =
this.tokenRepository.getTokenForSeries(presentedSeries);
找到如下
return (PersistentRememberMeToken)this.getJdbcTemplate().queryForObject(this.tokensBySeriesSql,
(rs, rowNum) -> {
return new PersistentRememberMeToken(rs.getString(1), rs.getString(2),
rs.getString(3), rs.getTimestamp(4));
}, new Object[]{seriesId});
解释:就是从数据库里查找对应的序列号,也就是presentedSeries,即series
至此很显然,我们的数据库有值,可以查询出来
继续下一步,会到
PersistentRememberMeToken token =
this.tokenRepository.getTokenForSeries(presentedSeries);
看看token的值,会发现,是我们查询的值
继续调试
直到这里
这里重新生成一个token信息,我们也会发现,只有token和时间会变化
PersistentRememberMeToken newToken = new
PersistentRememberMeToken(token.getUsername(), token.getSeries(),
this.generateTokenData(), new Date());
执行后,可以看看对应的newToken值
try {
再次操作配置类的数据返回值,操作数据库语句,进行更新
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
再次的发送cookie,注意:你可能会认为,对应的生成的cookie不同,实际上是相同的
因为单独的话,的确不同,但是结合序列号来说,则会是相同的
大概是算法的原因,所以前面说过,token不唯一
好处是:理论上不同的token可以登录多个地方,在安全保证的情况下,我们提高了扩展性
this.addCookie(newToken, request, response);
继续调试,可以到
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
注意,并不是之前的我们的认证操作,因为this不同
我们进入后
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (this.delegate != null) {
return this.delegate.loadUserByUsername(username);
} else {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
Iterator var3 = this.delegateBuilders.iterator();
while(var3.hasNext()) {
AuthenticationManagerBuilder delegateBuilder =
(AuthenticationManagerBuilder)var3.next();
this.delegate = delegateBuilder.getDefaultUserDetailsService();
这个this.delegate得到的就是对比信息,即我们的MyUserDetailsService
if (this.delegate != null) {
break;
}
}
if (this.delegate == null) {
throw new IllegalStateException("UserDetailsService is required.");
}
this.delegateBuilders = null;
}
}
return this.delegate.loadUserByUsername(username);
所以这里就是返回我们的MyUserDetailsService类中方法的返回值,也就是认证的信息了
}
}
所以之前的return this.getUserDetailsService().loadUserByUsername(token.getUsername());
也就是通过用户名来得到对应的对比信息
继续调试,回到user = this.processAutoLoginCookie(cookieTokens, request, response);
下一步,找到this.userDetailsChecker.check(user);,进入后
发现是对应的检查,好像与之前的一样,只是之前的是分开检查的,分为认证前和认证后,现在是一次性检查
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new
LockedException(this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User
account is locked"));
} else if (!user.isEnabled()) {
throw new
DisabledException(this.messages.getMessage("AccountStatusUserDetailsChecker.disabled",
"User is disabled"));
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(this.messages.getMessage(
"AccountStatusUserDetailsChecker.expired", "User account has expired"));
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(this.messages.getMessage(
"AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
}
}
发现的确如此
至此,那么我们得到之前的对比信息了
继续下一步,我们到 return this.createSuccessfulAuthentication(request, user);
传递了对比信息(可以简称为对比类)
进入后
protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails
user) {
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
这个与前面的认证一样,并不需要在意
return auth;
}
进入 RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.key, user,
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
参数分别是,固定的生成key值,对比信息,权限信息
public RememberMeAuthenticationToken(String key, Object principal, Collection<? extends
GrantedAuthority> authorities) {
super(authorities); 设置权限
if (key != null && !"".equals(key) && principal != null && !"".equals(principal)) {
this.keyHash = key.hashCode(); 这里不是密码,而是随机的key值
this.principal = principal; 对比信息
this.setAuthenticated(true);
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
总体来说,我们可以看到对应的this.setAuthenticated(true);,不难知道,他操作了对应的认证,由于对应的设置是在
AbstractAuthenticationToken类里面
我们知道他的子类中有RememberMeAuthenticationToken和UsernamePasswordAuthenticationToken
很明显,由于前面的UsernamePasswordAuthenticationToken操作的是AbstractAuthenticationToken类的值
使得认证,那么这里也操作,即同样的也是认证,至此,认证成功
那么继续往下走,直到回到
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
所以我们得到的rememberMeAuth就是前面的RememberMeAuthenticationToken
继续下一步,到
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
我们看看该值this.authenticationManager,发现他的类型是AuthenticationManager
这个类型我们在前面的认证流程看到过,操作父子类时
我们进入后
回到了熟悉的地方
但是这里有个地方不同了
进入 if (provider.supports(toTest)) {
因为现在的toTest是RememberMeAuthenticationToken的getClass()
而不是UsernamePasswordAuthenticationToken.getClass()
所以不会执行父类的操作,那么是一样的对象吗,答:是的,是一样的对象,可能是都赋值了
所以第一次的判断,就执行往后走
因为如下:
第一个
public boolean supports(Class<?> authentication) {
return AnonymousAuthenticationToken.class.isAssignableFrom(authentication);
}
第二个
public boolean supports(Class<?> authentication) {
return RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
}
可以发现,第二个是,所以说所以第一次的判断,就执行往后走
继续调试
到 result = provider.authenticate(authentication);
那么很明显,与认证的provider值不同,所以方法一般也是不同的,我们进入后
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!this.supports(authentication.getClass())) {
return null;
} else if (this.key.hashCode() !=
((RememberMeAuthenticationToken)authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage(
"RememberMeAuthenticationProvider.incorrectKey", "The presented
RememberMeAuthenticationToken does not contain the expected key"));
} else {
return authentication;
}
}
直接调试完,这里一般是检查,返回参数,即认证的RememberMeAuthenticationToken
继续走,返回的任然是RememberMeAuthenticationToken,回到
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
也就是说,我们只是进行检查,实际上因为我们已经是认证成功的,所以我们并不需要有创建认证的操作,直接返回即可
但却也要检查一下,认证时,操作密码为null时,一般也检查了
注意:其中的
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
并不是操作密码为null,而是其他操作,这里就不做说明,提醒一下,因为result不同了
继续调试,到SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
发现也进行了保存
我们继续调试,会发现有个
this.onSuccessfulAuthentication(request, response, rememberMeAuth);
他并没有做什么,继续下一步,发现,进行放行了,即到chain.doFilter(request, response);
至此,代码解析完成
总结:与认证一样,需要先使得认证,然后保存,既然认证了,那么自然可登录
只是获取方式不同,前面认证操作是通过
UsernamePasswordAuthenticationToken
而这里是RememberMeAuthenticationToken,但都是操作父类的值为true
使得变成认证,也就是说,只要你让父类的参数为true,就是认证成功
而中间的操作就是一系列的判断等等操作,或者说true就是认证的开关
而中间的就是防止不能随便的打开开关
但是你会发现,并没有与对比进行比对,实际上在更加底层的操作中进行的
这里就不说明了,只给出部分代码
当然,上面的解析,只是说明重要父部分,而很多的细节,并没有说明到
比如,如果删除浏览器cookie会怎么样,删除数据库的对应信息会怎么样等等操作
当然,也不可能所有的细节都说出来,那么肯定不是一朝一夕的都说明的
实际上前面的学习中的文字部分,就有说明,只是没有源码补充而已
*/
csrf流程源码:
在整个过滤器链中,CsrfFilter是起到csrf防护的,所以下面我们主要针对"记住我"看下源码是如何实现的?
因为并没有提交对应的csrf信息,那么"记住我"是怎么操作的呢,所以以记住我为例子,通过实践:实际上与普通的一样
记住我和csrf是分开的,因为csrf只是针对请求,而不是认证登录,所以说,是否操作记住我,对调试结果并没有影响
所以这里的针对可以点击或者不点击,结果都是一样的
csrf流程分析:
csrf流程源码跟踪:
在这之前,我们首先将过期时间调大,因为根据顺序
这个csrf是先操作的(虽然前面的登录认证也在记住我之前,当然需要操作到才可,调试时)
所以我们在操作源码时万一验证码过期了,会影响操作
回到ValidateCodeController类,进行修改,将60修改成600,当然,你可以更大
要跟踪源码,自然需要对应的类,即CsrfFilter:
/*
使用ctrl+n查找,然后再该类里面找到如下:
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) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
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);
}
}
}
首先我们访问初始页面,即登录页面,那么就会到上面的断点
下一步,到CsrfToken csrfToken = this.tokenRepository.loadToken(request);
一般来说,只要当页面取值时,才会进行获取this.tokenRepository
但是若页面并没有取值,那么也会获取,只是,会推迟(当然也会按照顺序)
这样使得session节省开支,因为不会立即占用session
进入后
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
再次进入return this.delegate.loadToken(request);
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session == null ? null : (CsrfToken)session.getAttribute(this.sessionAttributeName);
}
由于是第一次,那么对应的session肯定是null,所以也返回null
即回到CsrfToken csrfToken = this.tokenRepository.loadToken(request);
即csrfToken值是null
继续调试,到 csrfToken = this.tokenRepository.generateToken(request);
我们进入
public CsrfToken generateToken(HttpServletRequest request) {
return this.wrap(request, this.delegate.generateToken(request));
这个this.delegate的值一般是HttpSessionCsrfTokenRepository对象
}
我们再次进入return this.wrap(request, this.delegate.generateToken(request));
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}
对应的this.headerName值是:X-CSRF-TOKEN
this.parameterName值是:_csrf
后面的
this.createNewToken()是操作UUID的
private String createNewToken() {
return UUID.randomUUID().toString();
}
回到上一级,进入return this.wrap(request, this.delegate.generateToken(request));中的wrap方法
private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
HttpServletResponse response = this.getResponse(request);
return new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
}
进入如下:
return new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request,
HttpServletResponse response, CsrfToken delegate) {
this.tokenRepository = tokenRepository;
this.request = request;
this.response = response;
this.delegate = delegate;
}
这里进行构造,继续下一步:
直到回到 csrfToken = this.tokenRepository.generateToken(request);
即csrfToken值变成了
new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
我们继续下一步:
到如下this.tokenRepository.saveToken(csrfToken, request, response);
进入后
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse
response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
因为token不是null,所以跳过
继续下一步,我们可以看到如下:
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
其中
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
将对应的csrfToken值,进行保存,我们看看看对应的
CsrfToken.class.getName()值:CsrfToken类的地址
csrfToken.getParameterName()值:_csrf
他们都是String类型,这是自然的,因为参数就是操作String类型的
继续调试
到 if (!this.requireCsrfProtectionMatcher.matches(request)) {
他就是来判断是否是post请求的
我们进入
public boolean matches(HttpServletRequest request) {
Iterator var2 = this.requestMatchers.iterator();
RequestMatcher matcher;
do {
if (!var2.hasNext()) {
this.logger.debug("All requestMatchers returned true");
return true;
}
matcher = (RequestMatcher)var2.next();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Trying to match using " + matcher);
}
} while(matcher.matches(request));
this.logger.debug("Did not match");
return false;
}
我们调试到} while(matcher.matches(request));
进入后
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
看看this.allowedMethods值,如图所示:
*/
我们可以发现,他并没有post,由于现在的测试是get,也就是说,由于取反,使得原来的true变成了false,那么结束循环
当然只要你不是其中的请求即可,不会结束循环,虽然也有其他请求,但这里以post请求为例子
/*
回到这里
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
由于返回false,取反,变成true,所以会执行 filterChain.doFilter(request, response);
也就是放行了,至此,我们访问成功,到达登录页面
现在,在页面上,我们也知道
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
他操作了_csrf.parameterName和_csrf.token,回顾88章博客spring boot中的Thymeleaf
对应的对象.变量,操作的是变量首字母大写的方法,无论是否有该变量,即只操作方法
那么接下来我们找到csrfToken值,也就是
new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response,
token);
也就是SaveOnAccessCsrfToken类的信息
可以看到这两个方法
public String getParameterName() {
return this.delegate.getParameterName();
}
public String getToken() {
this.saveTokenIfNecessary();
return this.delegate.getToken();
}
很明显,对应的this.delegate值操作的是前面的token,也就是
new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
对应的this.headerName值是:X-CSRF-TOKEN
this.parameterName值是:_csrf
后面的
this.createNewToken()是操作UUID的
private String createNewToken() {
return UUID.randomUUID().toString();
}
DefaultCsrfToken类的代码:
public DefaultCsrfToken(String headerName, String parameterName, String token) {
Assert.hasLength(headerName, "headerName cannot be null or empty");
Assert.hasLength(parameterName, "parameterName cannot be null or empty");
Assert.hasLength(token, "token cannot be null or empty");
this.headerName = headerName;
this.parameterName = parameterName;
this.token = token;
}
public String getHeaderName() {
return this.headerName;
}
public String getParameterName() {
return this.parameterName;
}
public String getToken() {
return this.token;
}
明显知道得到了
this.parameterName值是:_csrf,即
和
UUID.randomUUID().toString();
即,调用的方法,最终的返回是this.delegate操作的
_csrf.parameterName值是:_csrf,也就是this.parameterName,也就是
_csrf.token值是:UUID.randomUUID().toString(),也就是this.token,也就是
我们找到他
public String getToken() {
这里打上断点调试 this.saveTokenIfNecessary();
return this.delegate.getToken();
}
进入 this.saveTokenIfNecessary();
private void saveTokenIfNecessary() {
if (this.tokenRepository != null) {
synchronized(this) {
if (this.tokenRepository != null) {
this.tokenRepository.saveToken(this.delegate, this.request, this.response);
this.tokenRepository = null;
this.request = null;
this.response = null;
}
}
}
}
进入this.tokenRepository.saveToken(this.delegate, this.request, this.response);进入
到达this.tokenRepository的子类HttpSessionCsrfTokenRepository
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse
response) {
HttpSession session;
if (token == null) {
session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
} else {
session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
我们明显的发现,他将token存在session里面了,也就是说
他不只是将对应的token给request里面,也放在session里面,只是session的是对应的this.delegate
且是在取值时进行存放
注意:在页面还没调试完时,会到这个调试
且后面可能需要多次的下一个断点进行跳过,跳过即可,可能其他程序造成的结果
接下来,我们发送登录请求(之前的都进行下一个断点跳过)
至此,我们又回到了(部分代码)
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) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
接下来,我们进入 CsrfToken csrfToken = this.tokenRepository.loadToken(request);
public CsrfToken loadToken(HttpServletRequest request) {
return this.delegate.loadToken(request);
}
进入return this.delegate.loadToken(request);
public CsrfToken loadToken(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session == null ? null : (CsrfToken)session.getAttribute(this.sessionAttributeName);
}
在前面,由于我们是第一次,所以会返回null,但现在我们已经有对应的session了,所以返回对应的数据
一直调试到如下
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
很明显,由于是post,那么这里就会返回true,取反,不放行
调试到如下:
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
获取了页面的csrfToken.getParameterName()值,也就是_csrf的值
在页面是,对应的_csrf值,就是生成的token值
继续调试,到
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " +
UrlUtils.buildFullRequestUrl(request));
}
很明显,是比较session和前端的token值,是否相同,当然,这是相同的,所以取反,则为false
而正是因为从session取值,所以csrfToken.getToken()并不会操作对应的保存session的方法,即
public String getToken() {
this.saveTokenIfNecessary();
return this.delegate.getToken();
}
而是
public String getToken() {
return this.token;
}
因为session保存的是DefaultCsrfToken类对象,而request保存的是SaveOnAccessCsrfToken类对象
但是SaveOnAccessCsrfToken对象
最终的返回值却还是DefaultCsrfToken的对象的getToken,只是多出了一个this.saveTokenIfNecessary();
进行将DefaultCsrfToken类对象保存到session里面的方法,即会发现,一个页面若没有获取csrf的token值时
则会在操作post时,帮我们进行保存,因为这时是request的token
简单来说,就是页面没有操作保存的,我们post帮页面干
继续下一步,那么到 filterChain.doFilter(request, response);,即放行了
那么,csrf认证完毕,但为了更加的追踪,我们找到之前的
session策略验证,之前这里并不需要查看,现在我们来看一看
this.sessionStrategy.onAuthentication(authResult, request, response);
进入后
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
SessionAuthenticationStrategy delegate;
for(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext();
delegate.onAuthentication(authentication, request, response)) {
delegate = (SessionAuthenticationStrategy)var4.next();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Delegating to " + delegate);
}
}
}
其中this.delegateStrategies值有两个,在开启csrf时,就是两个,否则是一个
如图所示:
*/
开启后:
开启前:
发现,的确多出一个值,这就是为什么之前,不进行查看的原因
因为对应的源码解析,并不需要说明,现在的源码解析,需要进行说明一下
/*
到这里, for(Iterator var4 = this.delegateStrategies.iterator(); var4.hasNext();
delegate.onAuthentication(authentication, request, response)) {
我们进入 delegate.onAuthentication(authentication, request, response)
注意是第二个循环值的进入
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) throws SessionAuthenticationException {
boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
if (containsToken) {
this.csrfTokenRepository.saveToken((CsrfToken)null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
}
}
注意:随着时间的推移,就算是相同的版本,但对应的代码可能是改变的,因为总会有变化
但操作的结果还是一样的,虽然代码过程变了,比如1+1=2,3-1=2,结果都是2,但过程变了
之所以说明这一点,是因为,你操作我这个版本时,可能代码不同,但仔细的观察,实际上结果是一样的,比如说这样
如图所示:
*/
代码不同,但结果相同
/*
我们进入(说进入的都是调试到,然后进入,注意即可)
this.csrfTokenRepository.saveToken((CsrfToken)null, request, response);
如下:
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (token == null) {
this.delegate.saveToken(token, request, response);
}
}
再次进入 this.delegate.saveToken(token, request, response);
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response)
{
HttpSession session;
if (token == null) {
session = request.getSession(false);
if (session != null) {
session.removeAttribute(this.sessionAttributeName);
}
} else {
session = request.getSession();
session.setAttribute(this.sessionAttributeName, token);
}
}
我们发现,他得到我们的session,然后并移除掉,因为我们已经用完了,而之所以使用session
是为了可以得到,因为request需要设置,但得到后,那么也就不需要session对应值了
我们回到这里
this.csrfTokenRepository.saveToken((CsrfToken)null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
很明显,他又生成了一个newToken,放在reuqest里面使得,当我们再次得到对应的数据时
就会保存到session,也就是说,他是为了给下一个页面做准备,实际上每次的访问,在没有session的情况下
都会有新的token出现,然后存在request里面,给页面做准备,当页面有使用时
会进行保存到session,使得一致性,当然,每次的使用都会进行删除,节省空间
这就是为什么,认证后,对应的token的值发生了改变的原因,因为session没有了,所以需要重新的生成
主要代码如下:
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
那么解析到这里,现在说明一个,放行的原因,即不防护,如前面的代码
http.csrf().ignoringAntMatchers("/user/saveOrUpdate");
我们继续回到CsrfFilter类,在这一行打上断点
request.setAttribute(HttpServletResponse.class.getName(), response);
继续调试
到添加用户这里,如图:
*/
因为他操作的是没有token的csrf,但却放行的路径
/*
点击提交,查看调试
部分代码如下:
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) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
很明显,由于session删除的,且没有使用对应的csrf值,那么自然的是生成一个新的csrf的token
但也要注意:若对应的页面有获得该值,那么自然也会存放在session里面
比如这里的退出登录,而由于我们又没有操作,那么自然session会存在,这里注意一下即可
因为在以后的操作中,会发现,对应的token与与某一个页面的token是一样的,这就是原因
而不一样的,自然是退出登录或者认证导致的,只要没有使得删除session的操作
那么以后再得到值时,基本token是一样的,当然,由于元素的原因,基本不会变
这里一般会使得有操作查看源代码访问的,之前说明过了
但是退出登录自然也会删除session(虽然这里并没有解析源代码说明,但与登录认证的操作类似)
可能对应的退出登录会触发两次调试
一个是退出,一个的指向,然后到访问
当然这里我们注意即可
而由于他又是post请求,那么会到如下代码:
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);
}
}
if (!this.requireCsrfProtectionMatcher.matches(request)) {
一般的post请求会使得这个判断结果是false,返回true,取反得到false
但是如果你加了不防护,也就是http.csrf().ignoringAntMatchers("/user/saveOrUpdate");
那么无论是什么请求,都返回false,取反得true,即必定会放行
之前的我们的认证时操作的post请求是有对应的值的,但这里
actualToken = request.getParameter(csrfToken.getParameterName());
得不到值,也就是null,这也使得 if (!csrfToken.getToken().equals(actualToken)) {
在进行判断时,取反为true,也就是不会到放行了,且是必定的
因为无论你的csrfToken有没有是session得到的,对该结果都是一样
不会发行,只是若没有操作session,那么这里会进行保存对应的token到session里面
简单来说,就是页面没有操作保存的,我们post帮页面干,当然,若没有操作成功
自然该session会一直存在,操作成功,则会删除,就如登录和退出登录一样
只要成功了就删除session,否则不会删除
但这里若是放行,即他并没有操作保存,但也可以称为删除,虽然并不是
继续调试
到如下:
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new
InvalidCsrfTokenException(csrfToken, actualToken));
}
很明显会到 this.accessDeniedHandler.handle(request, response, new
MissingCsrfTokenException(actualToken));
在没有设置权限不足的页面下,他就是默认的错误页面,也就是403错误,前面说过了
在设置了http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
指定方法,则操作该方法,前面说过了
也就是说this.accessDeniedHandler值
可以被http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
进行设置,否则使用默认的
我们进入http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
public ExceptionHandlingConfigurer<H> accessDeniedHandler(AccessDeniedHandler
accessDeniedHandler) {
this.accessDeniedHandler = accessDeniedHandler;
return this;
}
发现的确如此
至此,我们的csrf防护解析完毕
*/
授权流程源码:
在整个过滤器链中,FilterSecurityInterceptor是来处理整个用户授权流程的
也是距离用户API最后一 个非常重要的过滤器链,所以下面我们主要针对用户授权来看下源码是如何实现的?
授权流程分析
AffirmativeBased(基于肯定)的逻辑是:一票通过权
ConsensusBased(基于共识)的逻辑是:赞成票多于反对票则表示通过,反对票多于赞成票则将抛出AccessDeniedException
UnanimousBased(基于一致)的逻辑:一票否决权
三个逻辑是进行设置的,一般默认的中间的AffirmativeBased,一般来说,在低版本下,是多个逻辑一起操作,而高版本只操作一个
因为一票通过和一票否定,他们基本是不能一起操作的,如果可以一起,那么也就违背了他们的初衷
一票的意思,也就是有一个必定不能一票
上面的异常,我们发现,不只是操作了csrf也操作的权限,实际上他也只是一个处理方式而已,可能以后会给出一个新的处理方式
使得该异常只操作权限,因为需要对应,新的异常,操作csrf异常处理,但好像现在并没有这种方式(可以百度试试)
授权流程源码跟踪(FilterSecurityInterceptor):
/*
现在,我们先重启项目,清除缓存,登录后,在如下地方打上断点
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException, ServletException {
在下面一行这里打上断点
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
我们可以看看fi的值,可以发现,他后面显示url的值(也就是可以看到/user/findAll)
在他的toString里面可以看到显示
在对应对象后面的值(是该变量的对象),显示的就是其toString方法的返回值,注意即可
接下来,我们点击用户管理,那么会到
FilterInvocation fi = new FilterInvocation(request, response, chain);
创建了一个对象,我们往下走
进入this.invoke(fi);
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if (fi.getRequest() != null && fi.getRequest().getAttribute("
__spring_security_filterSecurityInterceptor_filterApplied") != null &&
this.observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null && this.observeOncePerRequest) {
fi.getRequest().setAttribute("
__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
调试到上面的 InterceptorStatusToken token = super.beforeInvocation(fi);
进入后
找到如下:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
boolean debug = this.logger.isDebugEnabled();
if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " +
object.getClass().getName() + " but AbstractSecurityInterceptor only configured to
support secure objects of type: " + this.getSecureObjectClass());
} else {
Collection<ConfigAttribute> attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
if (attributes != null && !attributes.isEmpty()) {
if (debug) {
到这里
Collection<ConfigAttribute> attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
我们进入后
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
看看他的值,如图所示:
*/
数据库只有8个,但这里却有10个,其余两个中,只有一个是默认的,一个是操作认证页面的,一个是当匹配不到时的值
我们会为每个用户,给出访问认证页面的权限,或者说,他就是放行的,因为操作了antMatchers(“/toLoginPage”).permitAll()
使得放行,不只是操作了csrf防护,也操作了授权,因为他是全部的放行,而另外一个也就是默认的
由于这里我操作的权限是用户权限,但他还是全部的权限,那么可以说明他就是我们配置类的操作的总添加权限信息
即:
/*
//查询数据库所有权限列表
List<Permission> list = permissionService.list();
for(Permission permission:list){
//添加请求权限
http.authorizeRequests().antMatchers(permission.getPermissionUrl())
.hasAnyAuthority(permission.getPermissionTag());
}
这里的权限规则操作
那么对应的 Collection<ConfigAttribute> attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
中this.obtainSecurityMetadataSource()得到的就是保存了我们设置的权限信息
我们继续进入getAttributes(object);
object就是之前创建的 FilterInvocation fi = new FilterInvocation(request, response, chain);
进入后
public Collection<ConfigAttribute> getAttributes(Object object) {
HttpServletRequest request = ((FilterInvocation)object).getRequest();
Iterator var3 = this.requestMap.entrySet().iterator();
Entry entry;
do {
if (!var3.hasNext()) {
return null;
}
entry = (Entry)var3.next();
} while(!((RequestMatcher)entry.getKey()).matches(request));
return (Collection)entry.getValue();
}
我们可以发现,他进行了循环取出,并操作了
} while(!((RequestMatcher)entry.getKey()).matches(request));
我们进入matches方法,看看他做了什么
public boolean matches(HttpServletRequest request) {
if (this.httpMethod != null && StringUtils.hasText(request.getMethod()) && this.httpMethod !=
valueOf(request.getMethod())) {
if (logger.isDebugEnabled()) {
logger.debug("Request '" + request.getMethod() + " " + this.getRequestPath(request) +
"' doesn't match '" + this.httpMethod + " " + this.pattern + "'");
}
return false;
} else if (this.pattern.equals("/**")) {
if (logger.isDebugEnabled()) {
logger.debug("Request '" + this.getRequestPath(request) + "' matched by universal
pattern '/**'");
}
return true;
} else {
String url = this.getRequestPath(request);
if (logger.isDebugEnabled()) {
logger.debug("Checking match of request : '" + url + "'; against '" + this.pattern +
"'");
}
return this.matcher.matches(url);
}
}
我们可以找到 String url = this.getRequestPath(request);
返回了request对应的请求
由于request是之前FilterInvocation fi = new FilterInvocation(request, response, chain);得到的
所以实际上该url就是对应的我们访问的请求
然后调试到return this.matcher.matches(url);
因为this是对应的循环的,而操作this.matcher,就是得到循环的url,与上面的我们访问的url进行对比
那么返回true,由于 } while(!((RequestMatcher)entry.getKey()).matches(request));
进行了取反,即返回false,跳出循环
调试到 return (Collection)entry.getValue();
也就是得到对应的循环的key对应的value
也就是如下图所示:
*/
/*
也就是说,我们得到了配置类中,添加的权限信息结果(匹配当前访问的权限信息)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
即给attributes赋值了
这样也就是得到了,要访问这个路径的权限信息(即系统权限),配置类里面操作的规则权限,称为系统权限
使得后面当你有该权限时,那么就可以访问
继续调试
到这里 Authentication authenticated = this.authenticateIfRequired();
我们进入this.authenticateIfRequired();
如下:
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Previously Authenticated: " + authentication);
}
return authentication;
} else {
authentication = this.authenticationManager.authenticate(authentication);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Successfully Authenticated: " + authentication);
}
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
}
在认证解析中,我们直到有这个 SecurityContextHolder.getContext().setAuthentication(authResult);
将对应的认证成功(设置为true的)的 UsernamePasswordAuthenticationToken
这里Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
再次获得该信息,我们直到,他里面有对应的权限信息的,这里就进行操作
而该权限信息就是当前用户的信息,后端操作数据库的代码中进行的
继续调试,发现,但会该认证的UsernamePasswordAuthenticationToken
也就是说Authentication authenticated = this.authenticateIfRequired();中
authenticated得到认证成功的UsernamePasswordAuthenticationToken
继续下一步,到这里
this.accessDecisionManager.decide(authenticated, object, attributes);
我们看看 this.accessDecisionManager这个值(类型是AccessDecisionManager)
如图所示:
*/
发现,的确是默认的AffirmativeBased(基于肯定)的逻辑(一票通过权),是类型AccessDecisionManager的子类
其中只要是得到当前类的值,一般在后面会出现眼镜符号显示,注意即可
/*
我们进入 this.accessDecisionManager.decide(authenticated, object, attributes);里面的decide方法
传递了参数,分别是:
认证的信息,创建的FilterInvocation类信息,匹配FilterInvocation类地址的权限信息(这个是规定)
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute>
configAttributes) throws AccessDeniedException {
int deny = 0;
Iterator var5 = this.getDecisionVoters().iterator();
while(var5.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
int result = voter.vote(authentication, object, configAttributes);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Voter: " + voter + ", returned: " + result);
}
switch(result) {
case -1:
++deny;
break;
case 1:
return;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
我们进入Iterator var5 = this.getDecisionVoters().iterator();
public List<AccessDecisionVoter<?>> getDecisionVoters() {
return this.decisionVoters;
}
返回一个投票者
继续调试到如下:
int result = voter.vote(authentication, object, configAttributes);
里面的代码中就不说明了,其中有个关键字,assert
一般作用是,只要他后面的值是true,继续下一步,否则抛出异常,当然,这里并不需要理会
返回的结果中,result值的不同,代表不同的投票结果
0:投票者表示弃权
-1:投票者表示不通过
1:投票者表示不通过
我们也发现,他传递了三个参数,在前面说过了,即整体来说
是看看我们用户的权限,是否与匹配的权限(通过将该权限称为系统权限,即配置类里面操作的规则权限,称为系统权限)
是否有相同的,相同,那么一般是返回1,这就是匹配权限的原因,很明显这里是返回1,因为是匹配(匹配成功)的
继续调试
到这里
case 1:
return;
}
进行了返回,即结束的该方法
至此,该 this.accessDecisionManager.decide(authenticated, object, attributes);操作结束
即认证成功,由于没有报错,那么自然的不会到规定的异常页面,即使得放行
虽然说,报错会使得重定向到认证页面(加后缀),但那是操作普通的报错的
即大多数的报错是这样(security在认证时报错,因为对应值是null,使得重定向,基本是这样)
但有些报错是操作自己的(不在认证时不会),注意即可,即重定向报错,基本是操作认证时的
我们点击下一个断点,然后看页面,发现,显示的内容
上面是正常的用户,能够匹配上的,那么如果匹配不上呢,解释如下:
先将页面上操作权限管理的标签去掉,显示对应的内容
<div sec:authorize="hasAuthority('product:findAll')">
去掉sec:authorize="hasAuthority('product:findAll')"
在页面操作权限不会匹配的访问
点击商品管理(这里使用zhaoyang测试,操作的就是用户管理,所以应该不能匹配)
在这之前,我们任然在这里打上断点
public void doFilter(ServletRequest request, ServletResponse response, FilterChain
chain) throws IOException, ServletException {
这里打上断点 FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
在后面的循环中,如图所示:
*/
操作的是"/product/findAll"
/*
我们直接到,投票者,那么,看看匹配结果
找到(即调试到,简称为找到) this.accessDecisionManager.decide(authenticated, object, attributes);
进入后
到这里
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute>
configAttributes) throws AccessDeniedException {
int deny = 0;
Iterator var5 = this.getDecisionVoters().iterator();
while(var5.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
int result = voter.vote(authentication, object, configAttributes);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Voter: " + voter + ", returned: " + result);
}
switch(result) {
case -1:
++deny;
break;
case 1:
return;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
这时对应的result是-1了,也就是不通过,之所以匹配不成功,是因为在用户的权限里面
没有对应的系统权限,所以匹配不成功
而之前的匹配成功,是因为我们的用户zhaoyang权限如下:
*/
所以前面的"/user/findAll"可以匹配成功,而"/product/findAll"不会匹配成功
/*
那么到如下:
switch(result) {
case -1:
++deny;
break;
case 1:
return;
}
}
很明显,这里操作break,即结束循环,到这里
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
不难直到,会执行
throw new AccessDeniedException(this.messages.getMessage("
AbstractAccessDecisionManager.accessDenied", "Access is denied"));
即抛出异常,那么我们继续调试,到如下:
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var7) {
this.publishEvent(new AuthorizationFailureEvent(object, attributes,
authenticated, var7));
throw var7;
}
很显然,操作了这些代码:
this.publishEvent(new AuthorizationFailureEvent(object, attributes,
authenticated, var7));
throw var7; 这里又进行抛出异常
继续调试
到如下:
} catch (Exception var10) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
这里得到AuthenticationException(认证)的异常,很明显得不到
RuntimeException ase =
(AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(
AuthenticationException.class, causeChain);
if (ase == null) {
再次得到,看看是否是AccessDeniedException(授权)的异常,很明显是的,即得到返回值
ase = (AccessDeniedException)this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
最后调试到如下:
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because
the response is already committed.", var10);
}
this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
我们进入 this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse
response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
this.logger.debug("Authentication exception occurred; redirecting to authentication
entry
point", exception);
this.sendStartAuthentication(request, response, chain,
(AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!this.authenticationTrustResolver.isAnonymous(authentication) &&
!this.authenticationTrustResolver.isRememberMe(authentication)) {
this.logger.debug("Access is denied (user is not anonymous); delegating to
AccessDeniedHandler", exception);
this.accessDeniedHandler.handle(request, response,
(AccessDeniedException)exception);
} else {
this.logger.debug("Access is denied (user is " +
(this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not
fully authenticated") + ");
redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, new
InsufficientAuthenticationException(this.messages.getMessage("
ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
}
}
调试到如下
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
获取了对应的认证成功的信息
继续往下走,到这里
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
不难知道,这就是异常操作的地方,即我们前面的默认,若设置了话,则操作设置的
至此,我们进行到下一个断点使得结束,查看页面,发现,的确如此,即后面都进行了放行
像这样结束的,并给看页面的,那么最终后面基本都是放行,所以就不说明了
为什么说放行呢,实际上过滤器在没有放行时,也是直接与servlet,一样,操作返回的responst
所以说也是一种放行
那么上面的操作中,都是我们的访问与数据库得到的系统权限的访问,都会匹配成功,在与我们用户的权限操作匹配
如果,我们的访问与数据库得到的系统权限的访问匹配失败,会怎么样,在前面说明过
没有在设置规则的情况下,基本是不操作权限的,这里我们结合代码来观察
将前端的访问和对应的后端的"/product/findAll"修改成"/product/findAl"
继续调试,在如下
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
这里打上断点 FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
不多说,直接到循环那里看看
如图:
*/