Spring Security 基础篇配置文件版学习

配套资料,免费下载
链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
提取码:kbue
复制这段内容后打开百度网盘手机App,操作更方便哦

第一章 Spring Security的概述

1.1、框架概述

Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 的两大核心功能就是认证(authentication)和授权(authorization)。

1.2、常用术语

  • 认证 :你是什么人。
  • 授权 :你能做什么。
  • 用户 :主要包括用户名称、用户密码和当前用户所拥有的角色信息,可用于实现认证操作。
  • 角色 :主要包括角色名称、角色描述和当前角色所拥有的权限信息,可用于实现授权操作。

1.3、常用单词

  • 认证 :authentication
  • 授权 :authorization
  • 用户 :user
  • 角色 :role
  • 登录 :login
  • 注销 :logout

1.4、环境准备

打开基础代码:

请在配套资料中,找到ssm专用基础代码,使用Idea打开ssm-security,配置好tomcat 8.5,这只是一个很普通的ssm项目,如果你有ssm项目的基础,相信你一定能看得懂,我们从左侧的菜单栏可以看到有四个部分,其中“产品管理”、“订单管理”虽然可以进行添加和查询所有,但是,这两个功能并没有和数据库交互,为了防止污染数据库表,让大家看起来很乱,所以我就使用map结构在service层进行了数据模拟;“用户管理”和“角色管理”我已经实现了最基础的增加和查询所有的功能,因为这并不是我们学习的重点,所以一些基本的配置和页面编写我就帮大家完成了。

我们正好趁此机会看着下边的页面,以这个项目为基础来进行学习Spring Security,最终实现的效果就是:

  • zhangsan:作为产品采购员,只能访问产品管理模块
  • lisi:作为财务管理员,只能访问订单管理模块
  • wangwu:作为系统管理员,可以访问所有模块,并可以对zhangsan和lisi进行访问权限管理

image-20210116142642925

修改数据连接:

因为这个基础代码是可以在我的电脑上运行的,我安装了mysql 5.5idea 2020.1以及tomcat 8.5,当你打开了基础项目的时候,并不一定能运行,请你检查运行环境是否和我保持一致,其中最重要的就是mysql,请最好不要安装mysql 8.0,如果你有一定的基础并且完全有能力克服此问题,你可以忽略我所说的以上这些。接下来,我们需要修改数据库的连接,这一步很重要,找到applicationContext.xml文件,根据自己的实际情况修改密码,其余的就不用动了。

<!--数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>
    <property name="username" value="root"/>
    <property name="password" value="密码"/>
</bean>

导入基础数据:

请在配套资料中,找到ssm专用基础代码,将test.sql运行到自己的mysql去,为了防止破坏大家的数据库,我在这里采用公共的test数据库来进行数据保存。

一切准备就绪:

当以上所有工作都准备完成以后,意味着,你可以启动项目并进行简单的查看了,启动后在浏览器输入http://localhost:8080/进行访问,切记在配置tomcat的时候,一定要把那个网站默认的前缀去掉,也就是下边这张图的红色框框部分。

image-20210116144130401

第二章 Spring Security的基本使用

2.1、导入所需依赖

Spring Security 5.4.2是基于Spring Framework 5.2.11.RELEASE构建的,但通常应与任何较新版本的Spring Framework 5.x一起使用。

<!--Spring Security-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.4.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.4.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.4.2</version>
</dependency>

2.2、创建配置文件

我们需要创建一个配置文件(spring-security.xml)来统一管理Spring Security的配置信息,然后将这个配置文件引入到核心配置applicationContext.xml中,以保证在启动的时候可以被Spring正常加载到。

spring-security.xml:

<?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: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/security
                           http://www.springframework.org/schema/security/spring-security.xsd">

</beans>

applicationContext.xml:

<!--导入Spring Security配置文件-->
<import resource="spring-security.xml"/>

2.3、配置过滤器链

Spring Security是Spring采用AOP思想,基于Servlet过滤器实现的安全框架。我们可以这么理解:当客户端发送一次请求,这个请求就应该被Spring Security安全框架给拦截住,然后经过Spring Security的过滤器链的层层过滤,最终才能到达我们目标方法,只要在这其中有一个过滤器不满足,就不能到达我们的目标方法,以此起到一个安全保护的目的,下图显示了单个HTTP请求的处理程序的典型分层。

filterchain

那我们的Spring Security的过滤器链中的过滤器都有哪些呢?分别都起到了什么作用,我在这里以基础环境下启动为例,给大家先拿过来15个过滤器并介绍,那么,Spring Security一共就这么多过滤器吗?答案是否定的,随着spring-security.xml配置的添加,还会出现新的过滤器。那么,是不是Spring Security每次都会加载这些过滤器呢?答案也是否定的!随着spring-security.xml配置的修改,有些过滤器可能会被去掉。在这里先说,只是让大家先有个印象,而且这些过滤器见名之意并不难理解,这非常有助于我们开展接下来的工作。

  1. org.springframework.security.web.context.SecurityContextPersistenceFilter

SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager。

  1. org.springframework.security.web.header.HeaderWriterFilter

    向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制。

  2. org.springframework.security.web.csrf.CsrfFilter

csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息, 如果不包含,则报错,起到防止csrf攻击的效果。

  1. org.springframework.security.web.authentication.logout.LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。

  1. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

    认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

  2. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

  3. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

如果没有在配置文件中指定退出页面,则由该过滤器生成一个默认退出页面。

  1. org.springframework.security.web.authentication.www.BasicAuthenticationFilter

    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

  2. org.springframework.security.web.savedrequest.RequestCacheAwareFilter

    通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest。

  3. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

    针对ServletRequest进行了一次包装,使得request具有更加丰富的API。

  4. org.springframework.security.web.authentication.AnonymousAuthenticationFilter

    当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。Spring Security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

  5. org.springframework.security.web.session.SessionManagementFilter

    SecurityContextRepository限制同一用户开启多个会话的数量。

  6. org.springframework.security.web.access.ExceptionTranslationFilter

    异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常。

  7. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

    获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

我们对过滤器链有了一定的认识,接下来,我们在web.xml中配置一下过滤器链,以下这段配置,相当于过滤器链的入口,并且过滤器的名称不可随便起。

<!--Spring Security过滤器链,注意过滤器名称必须叫springSecurityFilterChain-->
<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>

2.4、配置登录用户

我们要在spring-security.xml文件中配置一下,当前有哪些用户可以访问我们的权限管理系统,需要定义用户的名称、用户的密码、用户的角色。并且这三样东西都是我们自定义的,用户的名称和密码是在登录的时候需要验证的信息,当验证通过以后,就会通过角色来判断当前这个用户可以访问权限管理系统的哪些资源,默认情况下,我们是需要使用Spring Security拦截所有的请求。

<security:http auto-config="true" use-expressions="true">
    <!--使用Spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
</security:http>

<!--配置用户、密码以及所对应的角色信息,name、password、authorities中的内容都是自己定义的,{noop}代表密码不使用加密密码-->
<security:authentication-manager>
    <security:authentication-provider>
        <security:user-service>
            <security:user name="user" password="{noop}123456" authorities="ROLE_USER"/>
            <security:user name="admin" password="{noop}123456" authorities="ROLE_ADMIN"/>
        </security:user-service>
    </security:authentication-provider>
</security:authentication-manager>

如果一切正常,当你启动应用的时候,你应该会来到Spring Security的默认登录页面,这个页面是由框架自动生成的,目的就是用来进行登录的,由于这个页面中使用了bootstrap的cdn链接,第一次访问可能会很慢,以后就好很多了,不得不说其实他的这个页面设计的还是不错的,很精简的风格,如下效果:

image-20210116174959228

2.5、开放内嵌框架

当你使用用户user密码123456登录的时候,默认就会进入到权限管理系统的后台首页,但是当你点击各个功能模块的时候,会发现localhost拒绝了我们的连接请求。其实这个问题还是挺常见的一个问题,项目中如果用到iframe嵌入网页,然后用到Spring Security,请求就会被拦截,如果你打开F12开发者控制台,你可能就会发现这样一句报错:Refused to display 'http://localhost:8080/user/add' in a frame because it set 'X-Frame-Options' to 'deny'.

image-20210116175251143

Spring Security下,X-Frame-Options默认为DENY,非Spring Security环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何Frame页面,设置含义如下:

  • DENY:浏览器拒绝当前页面加载任何frame页面

  • SAMEORIGIN:frame页面的地址只能为同源域名下的页面

  • ALLOW-FROM:origin为允许frame加载的页面地址

既然清楚了问题的来源,那我们也就好解决这个问题了,有两种解决办法,第一种就是我们关掉Spring Security对frame的拦截;另外一种就是将X-Frame-Options设置为SAMEORIGIN,也就是只能是我们同域名下的请求访问,当然了,这种拦截机制肯定是为了保证系统的安全性,如果关掉了,有点太可惜了,我在这里给出两种解决方案的配置,但是我会采用第二种,而不是第一种的关闭。

第一种:

<security:http auto-config="true" use-expressions="true">
    <!--使用Spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
    <!--解决浏览器拒绝当前页面加载任何Frame页面-->
    <security:headers>
        <security:frame-options disabled="true"/>
    </security:headers>
</security:http>

第二种:

<security:http auto-config="true" use-expressions="true">
    <!--使用Spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
    <!--解决浏览器拒绝当前页面加载任何Frame页面-->
    <security:headers>
        <security:frame-options policy="SAMEORIGIN"/>
    </security:headers>
</security:http>

2.6、退出当前登录

如果想要注销,只要在浏览器地址访问:http://localhost:8080/logout就可以了,为了功能完整,请你打开main.jsp,第16行,修改注销地址为以下这段代码:

<ul class="navbar-nav px-3">
    <li class="nav-item text-nowrap">
        <a class="btn btn-danger btn-sm" href="${pageContext.request.contextPath}/logout">注销</a>
    </li>
</ul>

2.7、指定登录页面

虽然默认的登录页面还不错,往往项目中的静态页面已经是前端开发好的,包括登录页面,我们想要使用自己的登录界面该怎么办?我们不妨转换一下思维,使用自带的页面,我们先打开源码,看看他是怎么写的,按照他的这个模式,我们模仿着写到自己的登录界面中不就好了,为了节约大家的时间,我就在下边贴出来了关键部分,你也可以自己打开尝试,如下所示:

image-20210116182619671

我们会发现,他的这个登录页面没有什么特别的,就是一个form表单,里边有两个文本框,一个是账号,一个是密码,还有最下边多了一个特殊的hidden隐藏域,这个隐藏域他是为了防止csrf跨站破坏的,这个值每一次启动项目都不一样,是一个动态值,他是为了标识当前请求一定是我们自己的请求,而不是别的网站仿造的请求,我们的所有请求都需要携带上这个标签上边的value值,我们也称这个值为token值,我们需要借助Spring Security的标签库来动态生成这个隐藏域,直接拷贝是不管用的,我照着默认登录界面自己重写了一个登录页面,放在WEB-INF/views下,名字叫login.jsp,完整的代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html>
<head>
    <title>自定义登录页</title>
    <link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css">
    <link rel="stylesheet" href="${pageContext.request.contextPath}/css/main.css">
</head>
<body>
<div class="container mt-4">
    <form action="${pageContext.request.contextPath}/login" method="post">
        <div class="form-group">
            <label for="username">用户:</label>
            <input type="text" class="form-control" id="username" name="username" placeholder="请输入用户" required>
        </div>
        <div class="form-group">
            <label for="password">密码:</label>
            <input type="text" class="form-control" id="password" name="password" placeholder="请输入密码" required>
        </div>
        <div class="form-group form-check">
            <input type="checkbox" class="form-check-input" id="autoLogin">
            <label class="form-check-label" for="autoLogin">自动登录</label>
        </div>
        <button type="submit" class="btn btn-primary">登录</button>
        <%--隐藏域,用于防止csrf攻击--%>
        <security:csrfInput/>
    </form>
</div>
<script src="${pageContext.request.contextPath}/js/jquery-3.5.1.min.js"></script>
<script src="${pageContext.request.contextPath}/js/bootstrap.bundle.min.js"></script>
</body>
</html>

我们编写好自己的登录页面,还得需要告诉Spring Security你登录的时候不要使用你自己的登录界面了使用我的,我们只需要在spring-security.xml

中编写如下配置即可,然后重新启动项目,我们来看一看效果,是不是可以了。

<security:http auto-config="true" use-expressions="true">
    <!--需要放行去登录界面的请求,否则你根本看不到登录页面,这段配置必须放在pattern="/**"之前,否则不能生效-->
    <security:intercept-url pattern="/toLogin" access="permitAll()"/>
    <!--使用Spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
    <!--解决浏览器拒绝当前页面加载任何Frame页面-->
    <security:headers>
        <security:frame-options policy="SAMEORIGIN"/>
    </security:headers>
    <!--指定自定义的认证页面-->
    <!--
        login-page:自定义的登录页面的地址,由于login.jsp页面在WEB-INF/views下,不能直接访问,需要使用控制器方法来进行跳转
        login-processing-url:处理登录的请求,这个是固定的,这个/login地址是Spring Security内部已经规定好的
        default-target-url:登录成功以后,默认跳转到哪个界面,由于main.jsp也在WEB-INF/views下,不能直接访问,需要使用控制器方法来进行跳转
        username-parameter:登录表单中用户所对应的name的值,默认就是username,可以不用配置
        password-parameter:登录表单中密码所对应的name的值,默认就是password,可以不用配置
        authentication-failure-url:认证失败以后需要跳转的地址,这里还没有写认证错误页面,暂时出错还跳转回登录页面
    -->
    <security:form-login login-page="/toLogin"
                         login-processing-url="/login"
                         default-target-url="/main"
                         username-parameter="username"
                         password-parameter="password"
                         authentication-failure-url="/toLogin"/>
</security:http>
@Controller
public class MainController {
    /**
     * WEB-INF/views/目录下的所有页面,默认在浏览器中不能被访问到,需要使用控制器方法跳转
     * 通过http://localhost:8080/main来访问WEB-INF/views/main.jsp
     * @return
     */
    @RequestMapping("/main")
    public String main() {
        return "main";
    }

    /**
     * WEB-INF/views/目录下的所有页面,默认在浏览器中不能被访问到,需要使用控制器方法跳转
     * 通过http://localhost:8080/toLogin来访问WEB-INF/views/login.jsp
     * @return
     */
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
}

2.8、开放静态资源

最终我们启动后,发现确实来到了我们自己定义的登录页面了,说明我们之前的配置没有任何问题,但是,好像干干巴巴,啥样式都没有,这是为什么呢?如果你能看到样式,你清理一下浏览器缓存或者CTRL+F5强制刷新一下,就看不到了,至于原因,我们不难想到,刚才我们只是对跳转到登录页的请求进行了放行,而Spring Security默认是拦截所有请求,那肯定也包括静态资源css、js、img之类的,因此,静态资源是应该要被放行的,静态资源是不需要进行保护的,我们需要在spring-security.xml配置如下代码来放行静态资源。

<!--直接释放无需经过SpringSecurity过滤器的静态资源-->
<!--放的位置:必须要放到<security:http auto-config="true" use-expressions="true">上边-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/js/**" security="none"/>
<security:http pattern="/favicon.ico" security="none"/>

image-20210116195022625

2.9、指定退出页面

当你现在想要退出登录,点击右上角咱们之前配置好的注销,你就会神奇的发现,好像不能退出了,这是因为,默认退出会直接跳转到/login自动生成的认证页面,现在,认证页面也就是登录页面,已经改成我们自己的登录页面了,你只要指定了登录页面了,那默认的登录页面自然就不会创建了,因此当你退出的时候也就会报404找不到异常。

image-20210116195759468

而我们想要解决这个问题,其实很简单,我们给退出指定一个退出页面,只需要加入以下这段配置,很类似我们配置登录页的时候的代码:

<security:http auto-config="true" use-expressions="true">
    <!--需要放行去登录界面的请求,否则你根本看不到登录页面,这段配置必须放在pattern="/**"之前,否则不能生效-->
    <security:intercept-url pattern="/toLogin" access="permitAll()"/>
    <!--使用Spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
    <!--解决浏览器拒绝当前页面加载任何Frame页面-->
    <security:headers>
        <security:frame-options policy="SAMEORIGIN"/>
    </security:headers>
    <!--指定自定义的认证页面-->
    <!--
        login-page:自定义的登录页面的地址,由于login.jsp页面在WEB-INF/views下,不能直接访问,需要使用控制器方法来进行跳转
        login-processing-url:处理登录的请求,这个是固定的,这个/login地址是Spring Security内部已经规定好的
        default-target-url:登录成功以后,默认跳转到哪个界面,由于main.jsp也在WEB-INF/views下,不能直接访问,需要使用控制器方法来进行跳转
        username-parameter:登录表单中用户所对应的name的值,默认就是username,可以不用配置
        password-parameter:登录表单中密码所对应的name的值,默认就是password,可以不用配置
        authentication-failure-url:认证失败以后需要跳转的地址,这里还没有写认证错误页面,暂时出错还跳转回登录页面
    -->
    <security:form-login login-page="/toLogin"
                         login-processing-url="/login"
                         default-target-url="/main"
                         username-parameter="username"
                         password-parameter="password"
                         authentication-failure-url="/toLogin"/>

    <!--指定退出登录后跳转的页面-->
    <!--
        logout-url:处理注销(退出)的请求,这个是固定的,这个/logout地址是Spring Security内部已经规定好的
        logout-success-url:退出成功后需要跳转的页面,我们默认跳转到登录页去
    -->
    <security:logout logout-url="/logout"
                     logout-success-url="/toLogin"/>
</security:http>

即使加上了指定退出页的配置,当你登录后,点击注销,还是报404找不到资源,如果大家是跟着一步一步走来的,那就应该见过下边这个页面,当你在地址栏也好,还是a标签中也好,只要请求路径是:http://localhost:8080/logout,你就会看见下边这个是否确认注销的页面,你输入的刚才那个请求并不是真正退出,他还会问你是不是要退出,只有当你点击了这个Log Out,才是真正退出,你看他的源码,他是向/logout发送了一个post请求,并且还携带了csrf这个隐藏域,那我们是不是就可以仿照他这种形式,修改一下我们自己的退出功能呢。

image-20210116202243002

第一步:找到main.jsp添加Spring Security标签库

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

第二步:把之前的a标签的get请求,换成form的post请求,并加上隐藏域csrf

<ul class="navbar-nav px-3">
    <li class="nav-item text-nowrap">
        <form action="${pageContext.request.contextPath}/logout" method="post">
            <security:csrfInput/>
            <input class="btn btn-danger btn-sm" type="submit" value="退出">
        </form>
    </li>
</ul>

第三章 Spring Security的高级使用

3.1、深入跨站请求伪造

3.1、CSRF的概念

CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

3.2、CSRF的原理

假设:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,用户C为Web A网站的合法用户。

  1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
  2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
  3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
  4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
  5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

3.3、CSRF的防御

目前防御 CSRF 攻击主要有三种策略:验证 HTTP Referer 字段;在请求地址中添加 token 并验证(Spring Security采用);在 HTTP 头中自定义属性并验证。

(1)验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

然而,这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。

(2)在请求地址中添加 token 并验证

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 <input type="hidden" name="csrftoken" value="tokenvalue"/>,这样就把 token 以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。

该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上 token,黑客可以在自己的网站上得到这个 token,并马上就可以发动 CSRF 攻击。为了避免这一点,系统可以在添加 token 的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加 token,如果是通向外网则不加。不过,即使这个 csrftoken 不以参数的形式附加在请求之中,黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。

在Spring Security中,“GET”,“HEAD”,“TRACE”,"OPTIONS"四类请求可以直接通过,并不会被CsrfFilter过滤器过滤,会被直接放行,但是对于其他过滤器该过滤的还是会过滤的,除去上面四类,包括POST都要被验证携带token才能通过。

(3)在 HTTP 头中自定义属性并验证

这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

然而这种方法的局限性非常大。XMLHttpRequest 请求通常用于 Ajax 方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。另外,对于没有进行 CSRF 防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为 XMLHttpRequest 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

3.4、form表单如何添加token

如果您使用的是JSP,则可以使用Spring的表单标签库,只需要引入标签库,并在对应的表单添加上对应的标签即可。

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<security:csrfInput/>

image-20210116230211806

因此,当开启了 csrf 的时候,整个系统中的所有表单都需要加上 <security:csrfInput/> ,否则在进行表单提交的时候,会报一个 403 权限异常

image-20210117113839314

现在你的任务就是,给系统中的所有表单都加上 <security:csrfInput/> ,当你完成以后,才可以进行下一小节,否则后边操作可能会出现意想不到的问题。

3.5、ajax请求如何添加token

如果您使用的是JSP,则可以使用Spring的表单标签库,只需要引入标签库,并在head标签内添加上<security:csrfMetaTags/>标签即可。

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<security:csrfMetaTags/>

image-20210116230113648

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

3.6、文件上传避免 CSRF 拦截

请将MultipartFilter在Spring Security过滤器之前指定。MultipartFilter在Spring Security过滤器之前指定,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序所处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。为了确保MultipartFilter在具有XML配置的Spring Security过滤器之前指定,用户可以确保的<filter-mapping>元素MultipartFilter放在web.xml中的springSecurityFilterChain之前,如下所示:

<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

3.7、如何关闭 CSRF 防御机制

<security:http auto-config="true" use-expressions="true">
    ...
    ...
    <!--禁用csrf防护机制-->
    <security:csrf disabled="true"/>
</security:http>

3.2、完成网站自动登录

如果我想要关闭浏览器,下次再打开浏览器,权限管理系统会自动根据我上次的登录状态进行登录,这就是登录常用的“自动登录功能”,要想实现自动登录功能,我们需要实现两处关键配置就能使用了,具体操作如下:

打开 login.jsp 修改自动登录的name为remember-me,这是一个默认名称,可以修改,但是一般我们就叫这个名

<div class="form-group form-check">
    <input type="checkbox" class="form-check-input" id="autoLogin" name="remember-me">
    <label class="form-check-label" for="autoLogin">自动登录</label>
</div>

打开 spring-security.xml 开启自动登录功能

<security:http auto-config="true" use-expressions="true">
    ...
    ...
    <!--开启remember-me过滤器-->
    <!--
        remember-me-parameter:指定自动登录表单的name的值,默认是remember-me,可取值有:true、on、yes、1,一般用true
        token-validity-seconds:设置token存储时间为3600秒,也就是1个小时后会失效
    -->
    <security:remember-me
            remember-me-parameter="remember-me"
            token-validity-seconds="3600"/>
</security:http>

打开 http://localhost:8080/ ,登录以后,我们在关闭浏览器,然后重新打开 http://localhost:8080/ ,发现仍然可以访问,并且这时候不需要登录,他是怎么做到的呢?其实,在登录成功以后会往当前网站的cookie中写入一个自动登录的token值,当我们下次启动的时候,只要这个cookie没有消失,Spring Security就能拿到这个cookie的中保存的token的值,然后帮我们自动登录认证。

image-20210117101018607

3.3、保存凭据到数据库

自动登录功能方便是大家看得见的,但是安全性却令人担忧。因为cookie毕竟是保存在客户端的,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。 此外,Spring Security还提供了remember-me的另一种相对更安全的实现机制:在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到数据库表中验证,如果通过,自动登录才算通过。这样,自动登录功能的安全性就有了保证,因此,我们需要在数据库中创建一张用于保存自动登录信息的表,这张表是固定的,包括名称、字段等信息,都不能修改,否则会认识失败。

CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

接下来,我们需要配置一下,告诉Spring Security使用哪一个dataSource来操作这个表

<security:http auto-config="true" use-expressions="true">
    ...
    ...
    <!--开启remember-me过滤器-->
    <!--
        data-source-ref="dataSource":指定数据库连接池
        remember-me-parameter:指定自动登录表单的name的值,默认是remember-me,可取值有:true、on、yes、1,一般用true
        token-validity-seconds:设置token存储时间为3600秒,也就是1个小时后会失效
    -->
    <security:remember-me
            data-source-ref="dataSource"
            remember-me-parameter="remember-me"
            token-validity-seconds="3600"/>
</security:http>

接下来,我们重新进行测试,发现也是可行的,并且这里给出了浏览器和数据库的截图信息:

image-20210117102128942

image-20210117102206126

3.4、展示当前登录用户

登录成功以后,如何显示出来当前登录成功的用户名呢?我们这里给出两种常用方法,他们都必须使用Spring Security的标签库,具体代码如下:

第一种:打开 main.jsp 修改第13行

<span class="hidden-xs">
	<security:authentication property="principal.username" />
</span>

第二种:打开 main.jsp 修改第13行

<span class="hidden-xs">
	<security:authentication property="name" />
</span>

使用第一种方法修改完成后,代码如下:

<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">
    权限管理系统,欢迎:
    <span class="hidden-xs">
        <security:authentication property="principal.username" />
    </span>
</a>

image-20210117102944141

3.5、对接数据库中数据

我们现在已经在 spring-security.xml 写死并配置好了两个用户(user、admin)以及他们所对应的角色,但是,在真实的企业开发中,这些信息显然是不能保存在配置文件中的,因为要动态添加删除用户以及角色,我们就需要使用数据库来保存,现在 Spring Security 默认是走的配置文件中的账户和密码,我们如何对接数据库中的数据呢?

第一步:实现自己的 SysUserDetailsService 接口继承 UserDetailsService

public interface SysUserDetailsService extends UserDetailsService {
    
}

第二步:实现自己的 SysUserDetailsService 接口的 loadUserByUsername 方法,方法传入一个字符串,代表当前登录的用户名

@Service
@Transactional
public class SysUserDetailsServiceImpl implements SysUserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错
        SysUser sysUser = sysUserMapper.findUserByUsername(username);
        //如果没有查询到这个用户,说明数据库中不存在此用户,认证失败,返回null
        if (sysUser == null) {
            return null;
        }

        //获取该用户所对应的所有角色,当查询用户的时候级联查询其所关联的所有角色,用户与角色是多对多关系
        //如果这个用户没有所对应的角色,也就是一个空集合,那么在登录的时候会报 403 没有权限异常,切记这点
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        List<SysRole> sysRoles = sysUser.getSysRoles();
        for (SysRole sysRole : sysRoles) {
            authorities.add(new SimpleGrantedAuthority(sysRole.getName()));
        }

        //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证
        //org.springframework.security.core.userdetails.User实现了UserDetails对象,是SpringSecurity内置认证对象
        return new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);
    }
}

第三步:修改配置文件 spring-security.xml 中的认证提供者换成咱们自己定义的,然后重新启动系统使用数据库中的账户登录即可。

<!--配置用户、密码以及所对应的角色信息,name、password、authorities中的内容都是自己定义的,{noop}代表密码不使用加密密码-->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="sysUserDetailsServiceImpl">
        <!--<security:user-service>-->
            <!--<security:user name="user" password="{noop}123456" authorities="ROLE_USER"/>-->
            <!--<security:user name="admin" password="{noop}123456" authorities="ROLE_ADMIN"/>-->
        <!--</security:user-service>-->
    </security:authentication-provider>
</security:authentication-manager>

第四步:使用数据库所提供的账户进行登录测试。

image-20210117114114859

3.6、用户密码进行加密

第一步:配置加密对象

<!--加密对象-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<!--配置用户、密码以及所对应的角色信息,name、password、authorities中的内容都是自己定义的,{noop}代表密码不使用加密密码-->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="sysUserDetailsServiceImpl">
        <!--<security:user-service>-->
            <!--<security:user name="user" password="{noop}123456" authorities="ROLE_USER"/>-->
            <!--<security:user name="admin" password="{noop}123456" authorities="ROLE_ADMIN"/>-->
        <!--</security:user-service>-->        
        <!--指定认证使用的加密对象,如果加密对象的id=passwordEncoder,下边这句你都可以不用配置,默认引用就是passwordEncoder-->
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

第二步:保存用户的时候,给用户的密码进行加密,修改SysUserServiceImpl

@Autowired
private BCryptPasswordEncoder passwordEncoder;

@Override
public void save(SysUser sysUser) {
    sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
    sysUserMapper.save(sysUser);
}

第三步:去掉 SysUserDetailsServiceImpl 中的{noop}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    ...
    ...
    //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证
    //org.springframework.security.core.userdetails.User实现了UserDetails对象,是SpringSecurity内置认证对象
    return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}

第四步:手动修改数据库中的密码为加密后的密码,我们现在需要知道123456加密后的密文,需要手动生成,注意啊,每一次生成都不一样,但是都可以用

public class CreatePwd {
    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String encode = bCryptPasswordEncoder.encode("123456");
        System.out.println(encode);
    }
}

image-20210117130824084

第五步:重新启动权限管理系统,分别使用zhangsan、lisi、wangwu进行登录测试,发现都可以正常进行登录,我在创建表的时候默认就给他们分配了权限。

zhangsan:用户权限和产品权限

image-20210117132034187

lisi:用户权限和订单权限

image-20210117132047464

wangwu:所有权限

image-20210117132102343

为什么这三个账户都能登录成功,以下几个方面很重要:

  1. Spring Security已经配置了加密登录,我们手动把数据库中的123456改成了密文,登录的时候才可以保证认证成功

  2. 认证成功了可不一定能访问我们系统的资源,必须拥有相对应的角色,虽然角色是我们自己定义的,但是请你不要忘记,我们一开始,使用死的配置进行配置用户的时候,那个时候,只有拥有ROLE_USERROLE_ADMIN这样的用户才能访问系统资源,这就是为什么zhangsan和lisi必须要有ROLE_USER角色了

    <!--使用Spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
    <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
    

3.7、动态展示功能菜单

3.7.1、页面菜单动态展示

细心的你应该发现了,无论是zhangsan、lisi、wangwu中的哪一个人登录进去,左侧的菜单都是下边这个样子,完全没有实现我们的效果

image-20210117132943767

我们可以使用Spring Security提供的标签库来动态判断,只有拥有指定角色的人,才可以访问我们指定的功能模块,具体做法如下,找到main.jsp进行修改:

<ul class="nav flex-column">
    <security:authorize access="hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')">
        <li class="nav-item border-bottom">
            <p><a href="#">产品管理</a></p>
            <ul>
                <li><a href="/product/add" target="container">添加产品</a></li>
                <li><a href="/product/findAll" target="container">产品列表</a></li>
            </ul>
        </li>
    </security:authorize>

    <security:authorize access="hasAnyRole('ROLE_ADMIN','ROLE_ORDER')">
        <li class="nav-item border-bottom">
            <p><a href="#">订单管理</a></p>
            <ul>
                <li><a href="/order/add" target="container">添加订单</a></li>
                <li><a href="/order/findAll" target="container">订单列表</a></li>
            </ul>
        </li>
    </security:authorize>

    <%--只有拥有系统管理员角色的用户才能够添加用户、修改角色--%>
    <security:authorize access="hasAnyRole('ROLE_ADMIN')">
        <li class="nav-item border-bottom">
            <p><a href="#">用户管理</a></p>
            <ul>
                <li><a href="/user/add" target="container">添加用户</a></li>
                <li><a href="/user/findAll" target="container">用户列表</a></li>
            </ul>
        </li>
        <li class="nav-item border-bottom">
            <p><a href="#">角色管理</a></p>
            <ul>
                <li><a href="/role/add" target="container">添加角色</a></li>
                <li><a href="/role/findAll" target="container">角色列表</a></li>
            </ul>
        </li>
    </security:authorize>
</ul>

我们保存以后,重新启动权限管理系统,再次分别登录zhangsan、lisi、wangwu,看看左侧菜单栏发生了什么变化

zhangsan:

image-20210117133651412

lisi:

image-20210117133706875

wangwu:

image-20210117133724167

3.7.2、业务代码动态拦截

我们发现虽然界面上效果好像可以了,但是,难道就真的可以了吗?还有没有什么纰漏,我们假设一种场景,一个程序员,它使用zhangsan的账户登录系统后,闲来无事,他呢,自己又懂技术,想试试,在地址栏直接输入李四的订单页面,看看能不能进去,结果发现,进去了,这就是纰漏。

image-20210117134032756

我们上一步所实现的只是表面你所看到的,也就是视图上实现了不同用户可以看到不同的菜单,但是在控制器层并没有拦截住,这就是导致问题的根本原因,一般我们的解决办法就是在业务层(控制器层也可以,但是不推荐),给相对应的方法或者相应的类添加角色判断注解,只有拥有相应角色的用户才能访问该方法或者该类,在Spring Security中,一共支持三种注解都可以做到这个效果,而这三种注解的开启都是一个标签上进行开启,我接下来会把三个注解都打开,只使用第一种注解,其余两种会给大家注释掉,要记住,打开的哪个注解,就用哪个注解来限制访问,必须配套使用。这里演示三类注解,实际开发中,用一类即可!

spring-security.xml 添加以下配置

<!--
开启权限控制注解支持,以下三个配置只用开启一个就行了,全部开启也不会报错
    jsr250-annotations="enabled"表示支持jsr250-api的注解,需要jsr250-api的jar包
    pre-post-annotations="enabled"表示支持spring表达式注解
    secured-annotations="enabled"这才是SpringSecurity提供的注解
-->
<security:global-method-security jsr250-annotations="enabled"
                                 pre-post-annotations="enabled"
                                 secured-annotations="enabled"/>

修改OrderServiceImpl:我们就以这个类为例进行讲解,其余剩下的所有的实现都需要标注,可以在方法上标注注解,也可以在类上标注注解

@RolesAllowed({"ROLE_ADMIN", "ROLE_ORDER"})//JSR-250注解
//@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_ORDER')")//spring表达式注解
//@Secured({"ROLE_ADMIN","ROLE_ORDER"})//SpringSecurity注解
@Override
public void save(Order Order) {
    int size = orderMap.size();
    int id = ++size;
    Order.setId(id);
    orderMap.put(id, Order);
}

@RolesAllowed({"ROLE_ADMIN","ROLE_ORDER"})//SpringSecurity注解
@Override
public List<Order> findAll() {
    Collection<Order> Orders = orderMap.values();
    return new ArrayList<>(Orders);
}

完成以后,重新启动权限管理系统,然后登录zhangsan,你再次输入lisi的添加订单地址,看看还能不能访问,你会发现,添加订单界面还在,但是当你点击提交挺订单的时候,就会 403 权限不足,如果你连界面都不想展示出来,请你在控制层上标注相对应的注解即可。

image-20210117180847081

需要注意的是,如果添加到服务层,那相应的开启配置自然是配置到spring-security.xml,如果是添加到控制器层,那相应的开启配置需要添加到spring-mvc.xml中,这一点也一定要切记。

3.8、权限不足异常处理

大家也发现了,每次权限不足都出现403页面,这个错误页面是tomcat自己生成的,非常的难看,很不友好,当出现403异常以后,如何跳转到我们自定义的页面,接下来,我将提供三种形式来解决,第一种是tomcat提供的解决方式,第二种是Spring Security提供的解决方式,第三种是Spring MVC提供的解决方式,在解决问题之前,我们先定义自己的403没有权限的页面,以及通过控制器方法跳转到403.jsp,以上这几种情况还可以配置404、500等错误页面的跳转,如有需要也可以自行配置。

在 views 目录中创建 error 目录,在 error 目录中创建 403.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>没有权限</title>
</head>
<body>
<h3>403,没有权限</h3>
</body>
</html>

MainController 中添加跳转方法,代码如下:

@RequestMapping("/to403")
public String to403() {
    return "error/403";
}

以下几种方法任选其一使用即可,不必全部配置,推荐使用第三种Spring MVC提供的异常处理机制。

第一种:web.xml 中配置以下代码即可

<error-page>
    <error-code>403</error-code>
    <location>/to403</location>
</error-page>

image-20210117182105797

第二种:spring-security.xml中配置一下代码即可

<security:http auto-config="true" use-expressions="true">
    ...
    ...
    <!--403异常处理-->
    <security:access-denied-handler error-page="/to403"/>
</security:http>

image-20210117182105797

第三种:com.caochenlei.controller 中创建一个包 advice ,然后创建 ExceptionAdvice

@ControllerAdvice
public class ExceptionAdvice {
    //别导错类了:org.springframework.security.access.AccessDeniedException
    //只有出现AccessDeniedException异常才调转403.jsp页面
    @ExceptionHandler(AccessDeniedException.class)
    public String exceptionAdvice() {
        return "forward:/to403";
    }
}

image-20210117182105797

3.9、保证当前登录人数

有时候我们为了安全,也可以设置同一个账户,只能同时有一个人在线,我们只需要简单的配置就能实现。

第一种:同一个账户,只能有一个人访问,这个账户登录之后,如果不退出,此账户在其他地方均不可登录。

<security:http auto-config="true" use-expressions="true">
    ...
    ...
    <security:session-management>
        <!--
             max-sessions:最大会话数是1,也就是一个账户最多只能允许一个人登录
             error-if-maximum-exceeded:属性为true的话,如果帐号已经登录,在其它地方这个帐号就登录不了了
         -->
        <security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>
    </security:session-management>
</security:http>

第二种:同一个账户,只能有一个人访问,这个账户登录之后,如果不退出,此账户在其他地方登录,均被提示不能登录并跳转

expired.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>温馨提示</title>
</head>
<body>
<h2>账户已经在别的地方登录</h2>
<h2><a href="/toLogin">是否继续登录</a></h2>
</body>
</html>

MainController

@RequestMapping("/toExpired")
public String toExpired() {
    return "expired";
}

spring-security.xml

<!--
    会话管理器
        invalid-session-url:必须要有的属性,否则不会踢掉原来的登录,被踢掉的用户再发出请求会转到/toExpired页面
-->
    <security:session-management invalid-session-url="/toExpired">
        <!--
max-sessions:最大会话数是1,也就是一个账户最多只能允许一个人登录
error-if-maximum-exceeded:属性默认是false,表示同一账号,先登录的,会被后登录者强制下线,为true时,表示一旦有用户登录,其他用户将无法登录
expired-url:不设置的话会提示:This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
expired-url:设置的话就会跳转到哪个页面
        -->
        <security:concurrency-control max-sessions="1" error-if-maximum-exceeded="false" expired-url="/toExpired"/>
    </security:session-management>
</security:http>

原文地址:https://caochenlei.blog.csdn.net

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


本篇文章为大家展示了如何解决Spring Cloud 服务冲突问题,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。一、背景...
本篇内容主要讲解“spring cloud服务的注册与发现怎么实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“spri...
本篇内容介绍了“Dubbo怎么实现Spring Cloud服务治理 ”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处...
本篇内容主要讲解“SpringCloud相关面试题有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“SpringCloud相...
如何分析Spring Cloud Ribbon、Spring Cloud Feign以及断路器,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希
这篇文章主要讲解了“springcloud微服务的组成部分有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“s...
这篇文章主要讲解了“SpringCloud的OpenFeign项目怎么创建”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习...
本篇内容主要讲解“spring cloud oauth3整合JWT后获取用户信息不全怎么办”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带...
怎样解析微服务架构SpringCloud,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。...
这篇文章主要介绍spring cloud中API网关的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一、服务网关简介1、外观模式客户端...
本篇内容介绍了“Spring Cloud微服务的相关问题有哪些”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处...
本文小编为大家详细介绍“spring cloud config整合gitlab如何搭建分布式的配置中心”,内容详细,步骤清晰,细节处理妥当,希望这篇“spring cloud config整合gi...
config的用途?将cloud中各微服务的配置文件外部集中化,实行统一管理。尤其在一个服务负载时,配置的集中管理将显得非常方便。 spring cloud config 的使用* co...
前言·课程简介 最近挺多童鞋在公众号(itmuch_com)上催更Spring Cloud系列教程,故有此系列。以下是几点说明/规划/答疑: 问:为什么基于Finchley,而非Greenw...
【前面的话】书接上文,本文的某些知识依赖我的上一篇文章:SpringCloud之Eureka,如果没有看过可以先移步去看一下。另外在微服务架构中,业务都会被拆分成一个...
Spring Cloud Config Server提供了微服务获取配置的功能,这些配置文件(application.yml或者application.properties)通常维护在git或者数据库中,而且支持通过...
前言 随着服务化编程思想的不断流行,越来越多的公司、企业、开发人员使用微服务技术。目前流行的两大微服务技术:dubbo\SpringCloud。这篇文章不会去对dubbo和S...
1. 微服务简介1.1 什么是微服务架构微服务架构是系统架构上的一种设计风格将大系统拆分成N个小型服务这些小型服务都在各自的线程中运行小服务间通过HTTP协议进
最近,开源社区发生了一件大事,那个全国 Java 开发者使用最广的开源服务框架 Dubbo 低调重启维护,并且 3 个月连续发布了 4 个维护版本。我上次在写放弃Dubbo...
【前面的话】SpringCloud为开发人员提供了快速构建分布式系统的一些工具,包括配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式...