目 录CONTENT

文章目录

Spring 整合SpringSecurity

小张的探险日记
2021-09-07 / 0 评论 / 0 点赞 / 910 阅读 / 43,813 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2021-09-07,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

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 项目结构截图

image-20210831005149278

1.8 结果展示

​ 登陆页面,实现 SpringSecurity 的方式登陆并实现 记住我 功能

image-20210831005826651

​ 权限不足的判断,并自定义了 403 页面。

通过 security 提供的taglibs 标签对 菜单进行显示控制。

post 表单 提供 csrf 跨站请求伪造 防范。

image-20210831005942505

2.相关知识点

2.1 SpringSecurity 依赖模块

​ 简单了解下 SpringSecurity 有哪些模块。

  1. spring-security-core
    • 本模块包含核心身份验证和访问控制类和接口、远程支持和基本预置API。它是任何使用Spring Security的应用程序所必需的。它支持独立应用程序、远程客户端、方法(服务层)安全性和JDBC用户预置。
  2. spring-security-remoting
    • 本模块提供与Spring Remoting的集成。除非您正在编写使用Spring Remoting的远程客户端,否则您不需要此客户端。
  3. spring-security-web
    • 如果您需要Spring Security Web身份验证服务和基于URL的访问控制,则需要它。
  4. spring-security-config
    • 本模块包含安全命名空间解析代码和Java配置代码。如果您使用Spring Security XML命名空间进行配置或Spring Security的Java配置支持,则需要它。
  5. spring-security-ldap
    • 本模块提供LDAP身份验证和预置代码。如果您需要使用LDAP身份验证或管理LDAP用户条目,这是必需的。
  6. spring-security-oAuth2-core
    • 包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口。它是使用OAuth 2.0或OpenID Connect Core 1.0的应用程序所必需的,例如客户端、资源服务器和授权服务器。
  7. spring-security-oAuth2-client
    • 包含Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持。它是使用OAuth 2.0登录或OAuth客户端支持的应用程序所必需的。
  8. spring-security-oAuth2-jose
    • 包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。JOSE框架旨在为当事人之间安全转移索赔提供一种方法。
  9. spring-security-oAuth2-resource-server
    • 包含Spring Security对OAuth 2.0资源服务器的支持。它用于通过OAuth 2.0承载令牌保护应用编程接口。
  10. spring-security-acl
    • 本模块包含一个专门的域对象ACL实现。它用于将安全性应用于应用程序中的特定域对象实例。
  11. spring-security-cas
    • 本模块包含Spring Security的CAS客户端集成。如果您想将 Spring Security Web 身份验证与 CAS 单点登录服务器一起使用,则应使用它。
  12. spring-security-openid
    • 本模块包含OpenID Web身份验证支持。它用于针对外部OpenID服务器验证用户。
  13. spring-security-test
    • 本模块支持使用Spring Security进行测试。
  14. spring-security-taglibs
    • 提供Spring Security的JSP标签实现。

2.2 过滤链

​ SpringSecurity 的安全🔐控制是通过 Servlet Filter 来实现。 client 的请求 经过一层一层的 filter 最终达到 处理请求的 Servlet

过滤器链

image-20210831114117031

在 web.xml 中配置了 DelegatingFilterProxy 这是一个Spring Web 提供的 过滤器委托代理,我们在 filter-name 中指定了 被代理的 bean name 是 springSecurityFilterChain ,这是固定写法,不能写错。

image-20210831114450005

截屏2021-08-31 上午11.48.44

​ 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 关闭了

截屏2021-08-31 上午11.57.10

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);
        }

    }
}

梳理一下流程:

  1. web.xml 配置 DelegatingFilterProxy 并指定 代理的 bean name 为 springSecurityFilterChain
  2. 打断点可得-----通过 第一步的bean name 得到 FilterChainProxy 过滤器链代理
  3. 执行 FilterChainProxy 的 doFilterInternal 方法 根据请求获得 相关的 Filter
  4. 传入获得的 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);}

流程梳理:

  1. UsernamePasswordAuthenticationFilter 拦截到 /login 请求,执行 attemptAuthentication 方法把账号密码封装成 UsernamePasswordAuthenticationToken 对象 传入 ProviderManager authenticate 方法
  2. ProviderManager authenticate 方法中 通过循环比对的方式 找到合适的 认证提供商--AbstractUserDetailsAuthenticationProvider
  3. AbstractUserDetailsAuthenticationProvider 的 认证方法调用 DaoAuthenticationProvider 的 retrieveUser 方法
  4. DaoAuthenticationProvider最终会调用 UserDetailsService(需要我们来实现,整合用户体系) 来返回符合条件的用户
  5. AbstractUserDetailsAuthenticationProvider 中会对比密码,并把 认证成功的对象 重新封装成 UsernamePasswordAuthenticationToken对象
  6. 最终 重新封装的 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 表会产生一条新纪录,因为我们配置了数据源,如果不配置数据源 将不会写入数据库,表结构和名称是固定的

image-20210831224110462

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"%>

使用:security:csrfInput/

<%@ 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 出现 状态吗异常 都是原生的丑陋界面

受不了,用户更受不了,所以需要定义 自己的错误页面。

image-20210831234358003

​ 配置:

<!--省略其它配置--> <!--403异常处理--><security:access-denied-handler error-page="/403.jsp"/>

image-20210831235602880

定义 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 加密方式

​ 文档 🚪 =》 https://docs.spring.io/spring-security/site/docs/current/reference/html5/#authentication-password-storage

我们常用的是 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 😄😄😄

0

评论区