0.背景
很多东西使用都会,但是知识点都比较零散,通过查阅资料,学习 总结下知识点。
此 案例 总结了常用到的 SpringSecurity 的知识点。
1.搭建 Demo
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。相关文档 🚪=》https://docs.spring.io/spring-security/site/docs/current/reference/html5/
1.1 maven 依赖 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>spring.spring.security</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>war</packaging>
<dependencies>
<!--因为maven 具有依赖传递的特性,所以 我们 依赖 taglibs 后 会自动引用 web,具体可以在maven 依赖中查看-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.5.2</version>
</dependency>
<!--同理 同传递依赖到 core 包-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.5.2</version>
<!--
原来版本 5.1.5.RELEASE 升级 至 5.5.2
⚠️,升级案例版本后 必须排除 spring-web,因为 引用spring-mvc 的依赖和此 有冲突
Caused by: java.lang.IllegalArgumentException: 找到多个名为spring_web的片段。这是不合法的相对排序。有关详细信息,请参阅Servlet规范的第8.2.2 2c节。考虑使用绝对排序。-->
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!--jsr250 规范-->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
1.2 learing_security.sql
/*
Navicat Premium Data Transfer
Source Server : local_maria
Source Server Type : MariaDB
Source Server Version : 100604
Source Host : localhost:3306
Source Schema : learing_security
Target Server Type : MariaDB
Target Server Version : 100604
File Encoding : 65001
Date: 30/08/2021 22:10:44
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for persistent_logins
-- ----------------------------
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of persistent_logins
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_NAME` varchar(30) DEFAULT NULL COMMENT '菜单名称',
`permission_url` varchar(100) DEFAULT NULL COMMENT '菜单地址',
`parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES (1, 'ROLE_USER', '测试角色');
INSERT INTO `sys_role` VALUES (6, 'ROLE_ADMIN', '超级管理员');
INSERT INTO `sys_role` VALUES (7, 'ROLE_PRODUCT', '产品');
INSERT INTO `sys_role` VALUES (8, 'ROLE_ORDER', '订单');
COMMIT;
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`,`PID`),
KEY `FK_Reference_12` (`PID`),
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`password` varchar(120) NOT NULL COMMENT '密码',
`status` int(1) DEFAULT 1 COMMENT '1开启0关闭',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 'test', '$2a$10$0AFPibMeFbsCdufbt6ZDweaCAebm8VY1CPCLJJKGmfrIjzX2NM4L.', 1);
INSERT INTO `sys_user` VALUES (2, 'zpr', '$2a$10$0AFPibMeFbsCdufbt6ZDweaCAebm8VY1CPCLJJKGmfrIjzX2NM4L.', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`,`RID`),
KEY `FK_Reference_10` (`RID`),
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_user_role` VALUES (1, 6);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
1.3 配置 spring 容器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///learing_security"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.learingsecurity.dao"/>
</bean>
<context:component-scan base-package="com.learingsecurity.service"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven/>
<!--导入 spring-security 的配置文件-->
<import resource="classpath:spring-security.xml"/>
</beans>
1.4 配置 spring-mvc
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
>
<context:component-scan base-package="com.learingsecurity.controller"/>
<mvc:annotation-driven/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<!--
开启权限控制注解支持
jsr250-annotations="enabled" 表示 启用 jsr250-api 的注解,需要jsr250-api 的包
pre-post-annotations="enabled" 表示启用 spring 的注解支持
secured-annotations="enabled" 表示启用 springSecurity 的安全注解支持
⚠️:一般情况下,只需要 启用一种即可
注解在 springmvc 中使用则需要把 开启的定义在 mvc的容器中
注解在 Spring 中使用 则需要把 开启的定义在 spring 的容器中
否则不会产生效果
-->
<security:global-method-security
jsr250-annotations="enabled"
pre-post-annotations="enabled"
secured-annotations="enabled"/>
</beans>
1.5 配置 spring-security
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:sucurity="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"
>
<!--放行静态资源 css,images等文件-->
<!--⚠️ 在加此配置启动过,那么浏览器可以会有缓存,导致 不加此代码 也能正常显示页面,需要清除缓存在试试-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/favicon.ico" security="none"/>
<!-- 启动自动配置,并支持El 表达式 ,spring Security 自动配置对应的组件-->
<sucurity:http auto-config="true" use-expressions="true">
<!--⚠️注意 此配置一定要在 下面 /** 之前-->
<!--对登陆页面放行,login.jsp 页面还是经过 security-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--使用 spring el 表达式 指定项目资源只有 指定角色才能访问-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')" />
<!--配置认证配置
login-page: 配置 登陆页面
login-processing-url: 配置后台登陆请求地址,/login 是security 默认提供的
default-target-url: 登陆成功默认跳转页面,如果在如 直接访问订单页面没有登陆 跳转了 登陆页面,登陆成功后会回到订单页面。
authentication-failure-url: 登陆失败跳转页面
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
/>
<!--配置 注销操作
logout-url: 配置注销登陆 后台地址,security 默认提供
logout-success-url:注销成功 跳转 页面
invalidate-session: 清除🆑 session 会话信息
delete-cookies: 删除cookies
-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"
invalidate-session="true"
delete-cookies="true"
/>
<!--记住我功能
在数据库中存储 token,配置数据源 引用来自 Spring 容器 ,需要在数据库建对应的表
设置 cookies 过期时间为 60s
自定义 remember-me 的参数名称,默认为remember-me
-->
<security:remember-me
data-source-ref="dataSource"
token-validity-seconds="60"
remember-me-parameter="abc"
/>
<!--去掉 csrf 拦截过滤器,不启用,,,关闭csrf 后不安全,,警告⚠️⚠️⚠️-->
<!-- 如果不禁用 且 在页面中没有配置 csrf token 的话,会报 403 权限拒绝❌-->
<!--为了测试方便,先把 csrf 关闭,因为没有在系统所有post请求的位置都添加 csrf 的标签-->
<security:csrf disabled="true"/>
</sucurity:http>
<!--注册加密对象-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<!--引用加密对象-->
<security:password-encoder ref="passwordEncoder"/>
<!-- <security:user-service >-->
<!--改用 数据的用户信息-->
<!-- <security:user name="user" password="{noop}user" authorities="ROLE_USER" />-->
<!-- <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />-->
<!-- </security:user-service>-->
</security:authentication-provider>
</security:authentication-manager>
</beans>
1.6 web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Archetype Created Web Application</display-name>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--配置过滤链-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<!--过滤所有访问路径-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
1.7 项目结构截图
1.8 结果展示
登陆页面,实现 SpringSecurity 的方式登陆并实现 记住我 功能
权限不足的判断,并自定义了 403 页面。
通过 security 提供的taglibs 标签对 菜单进行显示控制。
post 表单 提供 csrf 跨站请求伪造 防范。
2.相关知识点
2.1 SpringSecurity 依赖模块
简单了解下 SpringSecurity 有哪些模块。
- spring-security-core
- 本模块包含核心身份验证和访问控制类和接口、远程支持和基本预置API。它是任何使用Spring Security的应用程序所必需的。它支持独立应用程序、远程客户端、方法(服务层)安全性和JDBC用户预置。
- spring-security-remoting
- 本模块提供与Spring Remoting的集成。除非您正在编写使用Spring Remoting的远程客户端,否则您不需要此客户端。
- spring-security-web
- 如果您需要Spring Security Web身份验证服务和基于URL的访问控制,则需要它。
- spring-security-config
- 本模块包含安全命名空间解析代码和Java配置代码。如果您使用Spring Security XML命名空间进行配置或Spring Security的Java配置支持,则需要它。
- spring-security-ldap
- 本模块提供LDAP身份验证和预置代码。如果您需要使用LDAP身份验证或管理LDAP用户条目,这是必需的。
- spring-security-oAuth2-core
- 包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口。它是使用OAuth 2.0或OpenID Connect Core 1.0的应用程序所必需的,例如客户端、资源服务器和授权服务器。
- spring-security-oAuth2-client
- 包含Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持。它是使用OAuth 2.0登录或OAuth客户端支持的应用程序所必需的。
- spring-security-oAuth2-jose
- 包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。JOSE框架旨在为当事人之间安全转移索赔提供一种方法。
- spring-security-oAuth2-resource-server
- 包含Spring Security对OAuth 2.0资源服务器的支持。它用于通过OAuth 2.0承载令牌保护应用编程接口。
- spring-security-acl
- 本模块包含一个专门的域对象ACL实现。它用于将安全性应用于应用程序中的特定域对象实例。
- spring-security-cas
- 本模块包含Spring Security的CAS客户端集成。如果您想将 Spring Security Web 身份验证与 CAS 单点登录服务器一起使用,则应使用它。
- spring-security-openid
- 本模块包含OpenID Web身份验证支持。它用于针对外部OpenID服务器验证用户。
- spring-security-test
- 本模块支持使用Spring Security进行测试。
- spring-security-taglibs
- 提供Spring Security的JSP标签实现。
2.2 过滤链
SpringSecurity 的安全🔐控制是通过 Servlet Filter 来实现。 client 的请求 经过一层一层的 filter 最终达到 处理请求的 Servlet
在 web.xml 中配置了 DelegatingFilterProxy 这是一个Spring Web 提供的 过滤器委托代理,我们在 filter-name 中指定了 被代理的 bean name 是 springSecurityFilterChain ,这是固定写法,不能写错。
initFilterBean 的初始化Filter Bean方法会把 web.xml 配置的 bean name 拿到,并根据它从WebApplicationContext 上下文中获取对应的 bean FilterChainProxy
public class FilterChainProxy extends GenericFilterBean {
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
//标记过滤器是否已经执行过
private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
//注意⚠️看这里 过滤器链 可以有多个
private List<SecurityFilterChain> filterChains;
//省略了 不关心的代码
//进行内部过滤
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//根据请求获取对应 过滤链中的 filter 列表
List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
if (filters != null && filters.size() != 0) {
//创建一个虚拟的过滤器链
FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
} else {
//这里是 没有获取到对应过滤器的情况,直接放行,比如我们的静态资源,在配置中配置了放行,就不会有filter 拦截
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
}
}
}
下面可以看到获取到springSecurity 提供的默认的 过滤器列表,对filers 做了一层封装 是 FilterChainProxy 的子类 VirtualFilterChain 然后执行 doFilter 方法 。
ps: 默认应该有15个,因为我把 csrf 关闭了
private static class VirtualFilterChain implements FilterChain {
//原生的过滤器链
private final FilterChain originalChain;
//过滤器列表
private final List<Filter> additionalFilters;
private final FirewalledRequest firewalledRequest;
//过滤器集合 size
private final int size;
//过滤器集合执行到什么位置了,主要用于记录位置
private int currentPosition;
//上面通过此构造 把 过滤器链和 默认的过滤器传入。
private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) {
this.currentPosition = 0;
this.originalChain = chain;
this.additionalFilters = additionalFilters;
//默认 肯定不为0
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
//表示过滤器链已经执行完成了
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain");
}
this.firewalledRequest.reset();
//调用originalChain.doFilter 进入原生过滤链
this.originalChain.doFilter(request, response);
} else {
++this.currentPosition;
Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
}
//执行当前过滤器,这里,因为当前 VirtualFilterChain 实现了 FilterChain
//这里又指定了 过滤链 是 this,所以 当前的 filter list 会递归进入此方法。
nextFilter.doFilter(request, response, this);
}
}
}
梳理一下流程:
- web.xml 配置 DelegatingFilterProxy 并指定 代理的 bean name 为 springSecurityFilterChain
- 打断点可得-----通过 第一步的bean name 得到 FilterChainProxy 过滤器链代理
- 执行 FilterChainProxy 的 doFilterInternal 方法 根据请求获得 相关的 Filter
- 传入获得的 Filter list 使用 FilteChainProxy 的内部类 VirtualFilterChain 的 doFilter 执行真正的 filter 的 doFilter 操作。
ps:过滤器链可以有多个。
2.3 认证流程
SpringSecurity 的表单登陆 是基于 UsernamePasswordAuthencationFilter 完成。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //指定前端传参 public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { //指定 登陆请求 地址和请求方式 super(new AntPathRequestMatcher("/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = this.obtainUsername(request); String password = this.obtainPassword(request); //删除多余代码 //把前端传的账号密码 封装成 UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); //获取认证管理器,调用认证方法 return this.getAuthenticationManager().authenticate(authRequest); }}
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private static final Log logger = LogFactory.getLog(ProviderManager.class); //提供 认证的 提供商 private List<AuthenticationProvider> providers; public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); Iterator var9 = this.getProviders().iterator(); while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); //根据封装的 authentication 类型来找到对应的 认证提供商 //这里我们封装的 authentication 是 UsernamePasswordAuthcationFilter if (provider.supports(toTest)) { try { result = provider.authenticate(authentication); } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } //删除了多余的代码 return result; }
认证提供商是 AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //认证方法 public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); //调用 retrieveUser 方法认证,但 当前类 并没有实现 此方法,默认的实现类是 DaoAuthenticationProvider user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); return this.createSuccessAuthentication(principalToReturn, authentication, user); } }
实际上调用的是 DaoAuthenticationProvider 的 retrieveUser 方法实现认证
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; private PasswordEncoder passwordEncoder; private volatile String userNotFoundEncodedPassword; private UserDetailsService userDetailsService; private UserDetailsPasswordService userDetailsPasswordService; protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { //看到 loadUserByUsername 如果用过 SpringSecurity 的朋友 就很熟悉了 //这一步是要把 我们自己的用户系统接入 SpringSecurity,所有我们需要去 实现 UserDetailsService 并实现 //loadUserByUsername(username) 方法 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); } } }
@Service@Transactionalpublic class UserServiceImpl implements UserService { @Autowired private UserDao userDao; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userDao.findByName(username); if(null == sysUser){ //返回空,表示认证失败。 return null; } List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>(); //动态获取权限 List<SysRole> roles = sysUser.getRoles(); for (SysRole role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } authorities.add(new SimpleGrantedAuthority("ROLE_USER")); //{noop} 表示密码是明文,加了 passwordEncoder 就不需要了// return new User(sysUser.getUsername(),"{noop}"+sysUser.getPassword(),authorities); //enable:账号是否启用,这里使用 enable 参数判断账号是否启用 //accountNonExpired:账号没有过期? //credentialsNonExpired:密码没有过期? //accountNonLocked:账号没有锁定? //需要 把自定义的 SysUser 转换成 SpringSecurity 的 User 对象 return new User(sysUser.getUsername(), sysUser.getPassword(), sysUser.getStatus()==1, true, true, true, authorities); }}
ok 到这一步,已经实现了根据 传入的用户名查询出 用户信息,下面看看 如何对返回的用户信息处理
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = this.determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //这里返回用户信息 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { } } try { //这里对 用户是否锁定/是否账号过期/是否密码过期 等进行检查 this.preAuthenticationChecks.check(user); //此方法 对比 密码 是否正确 this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } //此方法会把 认证完的用户信息 重新 封装成 一个 UsernamePasswordAuthenticationToken 对象 返回 return this.createSuccessAuthentication(principalToReturn, authentication, user); } //封装 UsernamePasswordAuthenticationToken 对象 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()); this.logger.debug("Authenticated user"); return result; } }
在返回到 ProviderManager 最终 在 AbstractAuthenticationProcessingFilter 的下面的方法,做 成功响应 和 失败响应。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { //处理返回 重新封装回来的 UsernamePasswordAutehcationFilter Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 认证成功响应 successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); //认证失败响应 unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { //认证失败响应 unsuccessfulAuthentication(request, response, ex); }}protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //成功 把 用户信息 放入 Security 的上下文中 SecurityContextHolder.getContext().setAuthentication(authResult); //记住我功能的相关处理 this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { //观察者模式,发布事件消息 this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } //使用successHandler 触发 成功的处理策略 this.successHandler.onAuthenticationSuccess(request, response, authResult); }
@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { //ExceptionTranslationFilter 在认证开始前把 request 缓存起来 //获取请求缓存,可以跳转回 用户登陆前 想访问的 url SavedRequest savedRequest = this.requestCache.getRequest(request, response); if (savedRequest == null) { super.onAuthenticationSuccess(request, response, authentication); return; } String targetUrlParameter = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) { this.requestCache.removeRequest(request, response); super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); // Use the DefaultSavedRequest URL String targetUrl = savedRequest.getRedirectUrl(); //跳转 指定的成功页面 getRedirectStrategy().sendRedirect(request, response, targetUrl);}
流程梳理:
- UsernamePasswordAuthenticationFilter 拦截到 /login 请求,执行 attemptAuthentication 方法把账号密码封装成 UsernamePasswordAuthenticationToken 对象 传入 ProviderManager authenticate 方法
- ProviderManager authenticate 方法中 通过循环比对的方式 找到合适的 认证提供商--AbstractUserDetailsAuthenticationProvider
- AbstractUserDetailsAuthenticationProvider 的 认证方法调用 DaoAuthenticationProvider 的 retrieveUser 方法
- DaoAuthenticationProvider最终会调用 UserDetailsService(需要我们来实现,整合用户体系) 来返回符合条件的用户
- AbstractUserDetailsAuthenticationProvider 中会对比密码,并把 认证成功的对象 重新封装成 UsernamePasswordAuthenticationToken对象
- 最终 重新封装的 UsernamePasswordAuthenticationToken 会返回到 UsernamePasswordAuthenticationFilter,做出成功或失败的响应操作。
2.4 记住我功能实现
在 spring-security.xml 的配置文件中 配置一下,数据源,过期时间,字段名称(默认为 remember-me)
<security:remember-me data-source-ref="dataSource" token-validity-seconds="60" remember-me-parameter="abc"/>
登陆页面
<!-- /.login-logo --><div class="login-box-body"> <p class="login-box-msg">登录系统</p> <form action="${pageContext.request.contextPath}/login" method="post"> <!--必须在 from 表单内,通过taglib 标签 会自动获取 后台产生的 csrf 的token--> <security:csrfInput/> <div class="form-group has-feedback"> <input type="text" name="username" class="form-control" placeholder="用户名"> <span class="glyphicon glyphicon-envelope form-control-feedback"></span> </div> <div class="form-group has-feedback"> <input type="password" name="password" class="form-control" placeholder="密码"> <span class="glyphicon glyphicon-lock form-control-feedback"></span> </div> <div class="row"> <div class="col-xs-8"> <div class="checkbox icheck"> <!--此处参数名 必须为 remember-me,源码中写死, 如果想修改也不是没有办法可以在 配置文件中更改,具体在配置文件中详细讲解--> <label><input type="checkbox" name="abc" value="true"> 记住 下次自动登录</label> </div> </div> <!-- /.col --> <div class="col-xs-4"> <button type="submit" class="btn btn-primary btn-block btn-flat">登录</button> </div> <!-- /.col --> </div> </form> <a href="#">忘记密码</a><br></div>
勾选记住我登陆后,后台会产生一个 cookie,数据库 persistent_logins 表会产生一条新纪录,因为我们配置了数据源,如果不配置数据源 将不会写入数据库,表结构和名称是固定的
2.5 csrf 跨站请求伪造 防护机制
什么是跨站请求伪造=》 https://baike.baidu.com/item/跨站请求伪造/13777878?fr=aladdin
SpringSecurity 默认开启csrf 验证,所有页面请求的地方都需要 填写 csrf token,如果没有 将返回403 权限拒绝❌,下面在源码中 我们可以看到。
当使用了 jsp 时,SpringSecurity 默认提供了 jsp 的标签库
首先需要先引入标签库
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!--导入 taglib 标签库--><%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>数据 - AdminLTE2定制版 | Log in</title><meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport"><link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css"><link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css"><link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css"><link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css"><link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css"></head><body class="hold-transition login-page"> <div class="login-box"> <div class="login-logo"> <a href="#"><b>ITCAST</b>后台管理系统</a> </div> <!-- /.login-logo --> <div class="login-box-body"> <p class="login-box-msg">登录系统</p> <form action="${pageContext.request.contextPath}/login" method="post"> <!--必须在 from 表单内,通过taglib 标签 会自动获取 后台产生的 csrf 的token--> <security:csrfInput/> <div class="form-group has-feedback"> <input type="text" name="username" class="form-control" placeholder="用户名"> <span class="glyphicon glyphicon-envelope form-control-feedback"></span> </div> <div class="form-group has-feedback"> <input type="password" name="password" class="form-control" placeholder="密码"> <span class="glyphicon glyphicon-lock form-control-feedback"></span> </div> <div class="row"> <div class="col-xs-8"> <div class="checkbox icheck"> <!--此处参数名 必须为 remember-me,源码中写死, 如果想修改也不是没有办法可以在 配置文件中更改,具体在配置文件中详细讲解--> <label><input type="checkbox" name="abc" value="true"> 记住 下次自动登录</label> </div> </div> <!-- /.col --> <div class="col-xs-4"> <button type="submit" class="btn btn-primary btn-block btn-flat">登录</button> </div> <!-- /.col --> </div> </form> <a href="#">忘记密码</a><br> </div> <!-- /.login-box-body --> </div> <!-- /.login-box --> <!-- jQuery 2.2.3 --> <!-- Bootstrap 3.3.6 --> <!-- iCheck --> <script src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script> <script src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script> <script src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script> <script> $(function() { $('input').iCheck({ checkboxClass : 'icheckbox_square-blue', radioClass : 'iradio_square-blue', increaseArea : '20%' // optional }); }); </script></body></html>
csrf 功能 通过 CsrfFilter 提供,可以通过配置文件 配置
<security:csrf disabled="true"/>
public final class CsrfFilter extends OncePerRequestFilter { //默认的 匹配器 private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); @Override public boolean matches(HttpServletRequest request) { return !this.allowedMethods.contains(request.getMethod()); } @Override public String toString() { return "CsrfNotRequired " + this.allowedMethods; } } @Override 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); //post,put,delete 等触发修改的请求 都将被拦截 if (!this.requireCsrfProtectionMatcher.matches(request)) { if (this.logger.isTraceEnabled()) { this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher); } //"GET", "HEAD", "TRACE", "OPTIONS" 直接放行 filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug( LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request))); AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken); //csrf token 传的有问题,将返回权限拒绝错误🙅 this.accessDeniedHandler.handle(request, response, exception); return; } filterChain.doFilter(request, response); }
2.6 SpringSecurity taglibs jsp 标签库
文档 🚪=》https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/htmlsingle/#taglibs
它为访问安全信息和在JSP中应用安全约束提供了基本支持。
使用需要导入 maven依赖
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.5.2</version></dependency>
在 jsp 中导入 taglib 标签库
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
授权校验
<security:authorize access="hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')"> <li id="system-setting"><a href="${pageContext.request.contextPath}/product/findAll"> <i class="fa fa-circle-o"></i> 产品管理 </a></li></security:authorize>
csrf token 支持
<!--必须在 from 表单内,通过taglib 标签 会自动获取 后台产生的 csrf 的token--><security:csrfInput/>
页面上显示用户信息
<%--<security:authentication property="principal.username" />--%><security:authentication property="name" />
2.7 自定义登陆界面
默认SpringSecurity 提供了 默认的登陆界面
我们可以通过配置的方式 指定我们自己的 登陆页面 和 注销操作
<!--配置认证配置 login-page: 配置 登陆页面 login-processing-url: 配置后台登陆请求地址,/login 是security 默认提供的 default-target-url: 登陆成功默认跳转页面,如果在如 直接访问订单页面没有登陆 跳转了 登陆页面,登陆成功后会回到订单页面。 authentication-failure-url: 登陆失败跳转页面--><security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/index.jsp" authentication-failure-url="/failer.jsp"/><!--配置 注销操作 logout-url: 配置注销登陆 后台地址,security 默认提供 logout-success-url:注销成功 跳转 页面 invalidate-session: 清除🆑 session 会话信息 delete-cookies: 删除cookies--><security:logout logout-url="/logout" logout-success-url="/login.jsp" invalidate-session="true" delete-cookies="true"/>
2.8 处理全局异常
每当 SpringSecurity 出现 状态吗异常 都是原生的丑陋界面
受不了,用户更受不了,所以需要定义 自己的错误页面。
配置:
<!--省略其它配置--> <!--403异常处理--><security:access-denied-handler error-page="/403.jsp"/>
定义 Spring-mvc 全局异常类,根据异常跳转对应页面。
package com.learingsecurity.controller.advice;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.acls.model.NotFoundException;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;/** * @className ControllerExceptionAdvice * @Desc TODO 全局异常类 * @Author 张埔枘 * @Date 2021/8/28 9:11 下午 * @Version 1.0 */@ControllerAdvicepublic class ControllerExceptionAdvice { /** * 拦截 403 异常 跳转到 403 页面 * @return */ @ExceptionHandler(AccessDeniedException.class) public String exception403(){ return "forward:/403.jsp"; } }
2.9 加密方式
我们常用的是 BCryptPasswordEncoder 类,采用动态盐的方式加强密码的安全性。
使用需要把它放入 spring 的容器中
<!--注册加密对象--><bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
3.0 放行静态资源
在开发早期,单体项目时,前端代码和后端代码是在一起的。
那么SpringSecurity 对接口拦截的同时,也可能会对 静态资源拦截。所以需要 处理一下,不拦截静态资源
<!--放行静态资源 css,images等文件--><!--⚠️ 在加此配置启动过,那么浏览器可以会有缓存,导致 不加此代码 也能正常显示页面,需要清除缓存在试试--><security:http pattern="/css/**" security="none"/><security:http pattern="/img/**" security="none"/><security:http pattern="/plugins/**" security="none"/><security:http pattern="/failer.jsp" security="none"/><security:http pattern="/favicon.ico" security="none"/>
源码:https://gitee.com/zhang-purui/spring-spring-security 欢迎 Start 😄😄😄
评论区