• 学习一下 Spring Security


    一、Spring Security

    1、什么是 Spring Security?

    (1)基本认识
      Spring Security 是基于 Spring 框架,用于解决 Web 应用安全性的 一种方案,是一款优秀的权限管理框架。
      Web 应用的安全一般关注 用户认证(authentication) 以及 用户授权(authorization) 这两个部分。简单的理解就是 Web 应用 如何确定 你是谁 以及 你能干什么。

    【官网地址:】
        https://spring.io/projects/spring-security

    (2)用户认证(authentication)
      用户认证就是 验证某个用户是否为系统中的合法主体,也即该用户是否能登陆系统,通常根据用户名以及密码进行确认。
      简单的理解就是 使 Web 应用确定 你是谁。

    (3)用户授权(authorization)
      用户授权就是 验证某个用户是否有执行某个操作的权限。
      简单的理解就是 使 Web 应用确定 你能干什么。

    (4)记住几个点

    【@EnableWebSecurity】
        用于开启 WebSecurity 模式。有时不需要也可以实现相应的功能。
        
    【@EnableGlobalMethodSecurity】
        用于开启注解。常见参数为:prePostEnabled、securedEnabled。
    
    【WebSecurityConfigurerAdapter】
        用于自定义 Security 策略。
    
    【AuthenticationManagerBuilder】
        用于自定义 认证策略。

    2、Spring Security 与 Shiro 简单比较一下?

    (1)Spring Security
      基于 Spring 框架开发,可以与 Spring 无缝整合。
      属于重量级的权限控制框架(依赖其他组件、引入各种依赖),提供了全面的权限控制。

    (2)Shiro
      Apache 的轻量级权限控制框架,不与任何框架捆绑。
      使用起来比 Spring Security 简单。

    (3)使用
      一般来说,使用 Shiro 可以解决大部分项目的问题,且容易操作。
      而 SpringBoot 提供了自动化配置方案,通过较少的配置就可以使用 Spring Security。
      所以常见组合通常为: SSM + Shiro 或者 SpringBoot / SpringCloud + Spring Security。

    3、Spring Security 初体验(SpringBoot + Spring Security)

    (1)步骤

    【步骤:】
        Step1:新建一个 SpringBoot 项目。
        Step2:引入 Web 依赖、Spring Security 依赖。
        Step3:新建一个 controller 进行测试。
    注:
        此处仅导入依赖,未进行任何配置,所以显示的都是默认效果。
    
    【效果:】
    当 Spring Security 依赖存在时,访问 controller 时会默认跳转到登陆页面。
    默认用户名为:user
    密码在控制台上可以看到(随机生成)。

    (2)新建一个 SpringBoot 项目,并添加 Web、Spring Security 等依赖。

     

    【依赖:】
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    (3)新建一个 controller,并简单测试一下 Spring Security。

    【controller:】
    package com.lyh.demo.springsecurity.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RequestMapping("test")
    @RestController
    public class TestController {
    
        @GetMapping("/hello")
        public String hello() {
            return "hello spring security";
        }
    }

    如下图所示,未添加 SpringSecurity 依赖时,访问 controller 没有限制。
    而 添加上依赖后,访问 controller 会首先跳转到登录页面,成功登录后才允许访问。

    4、Spring Security 再次体验(SSM + Spring Security)

    (1)步骤
      使用 SSM 时,需要进行一些繁琐的配置,没有 SpringBoot 用起来舒服,
      此处简单配置一下,后面介绍仍然以 SpringBoot 为主。

    【步骤:】
        Step1:创建一个 maven 工程 或者 web 工程(能使用 SpringMVC 即可),可参考:https://www.cnblogs.com/l-y-h/p/12030104.html
        Step2:配置 SpringSecurity,并测试。

    (2)新建 maven 工程,导入相关依赖
      此处使用 tomcat 8 版本启动项目,tomcat 7 启动后在登录时可能会报错。

    【依赖】
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-core</artifactId>
      <version>5.3.4.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>5.3.4.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>5.3.4.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.8.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    【注意:(tomcat7 版本可能会报如下的错误,更换 tomcat 8 以上版本即可)】
    java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.changeSessionId()Ljava/lang/String;

    (3)配置基本的 web 环境(Spring 以及 SpringMVC)

    【web.xml】
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">
    
      <!-- step1: 配置全局的参数,启动Spring容器 -->
      <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- 若没有提供值,默认会去找/WEB-INF/applicationContext.xml。 -->
        <param-value>classpath:applicationContext.xml</param-value>
      </context-param>
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
    
      <!-- step2: 配置SpringMVC的前端控制器,用于拦截所有的请求  -->
      <servlet>
        <servlet-name>springmvcDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <!-- 若没有提供值,默认会去找WEB-INF/*-servlet.xml。 -->
          <param-value>classpath:dispatcher-servlet.xml</param-value>
        </init-param>
        <!-- 启动优先级,数值越小优先级越大 -->
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>springmvcDispatcherServlet</servlet-name>
        <!-- 将DispatcherServlet请求映射配置为"/",则Spring MVC将捕获Web容器所有的请求,包括静态资源的请求 -->
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    
      <!-- step3: characterEncodingFilter字符编码过滤器,放在所有过滤器的前面 -->
      <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
          <!--要使用的字符集,一般我们使用UTF-8(保险起见UTF-8最好)-->
          <param-name>encoding</param-name>
          <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
          <!--是否强制设置request的编码为encoding,默认false,不建议更改-->
          <param-name>forceRequestEncoding</param-name>
          <param-value>false</param-value>
        </init-param>
        <init-param>
          <!--是否强制设置response的编码为encoding,建议设置为true-->
          <param-name>forceResponseEncoding</param-name>
          <param-value>true</param-value>
        </init-param>
      </filter>
      <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <!--这里不能留空或者直接写 ' / ' ,否则可能不起作用-->
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
      <!-- step4: 配置过滤器,将post请求转为delete,put -->
      <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
      </filter>
      <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    </web-app>
    
    【applicationContext.xml】
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
    
        <!-- step1: 配置包扫描方式。扫描所有包,但是排除Controller层 -->
        <context:component-scan base-package="com.lyh.demo">
            <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        </context:component-scan>
    </beans>
    
    【dispatcher-servlet.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:context="http://www.springframework.org/schema/context"
           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/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <!-- step1: 配置Controller扫描方式 -->
        <!-- 使用组件扫描的方式可以一次扫描多个Controller,只需指定包路径即可 -->
        <context:component-scan base-package="com.lyh.demo" use-default-filters="false">
            <!-- 一般在SpringMVC的配置里,只扫描Controller层,Spring配置中扫描所有包,但是排除Controller层。
            context:include-filter要注意,如果base-package扫描的不是最终包,那么其他包还是会扫描、加载,如果在SpringMVC的配置中这么做,会导致Spring不能处理事务,
            所以此时需要在<context:component-scan>标签上,增加use-default-filters="false",就是真的只扫描context:include-filter包括的内容-->
            <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
        </context:component-scan>
    
        <!-- step2: 配置视图解析器 -->
        <bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/"/><!--设置JSP文件的目录位置-->
            <property name="suffix" value=".jsp"/>
        </bean>
    
        <!-- step3: 标准配置 -->
        <!-- 将springmvc不能处理的请求交给 spring 容器处理 -->
        <mvc:default-servlet-handler/>
        <!-- 简化注解配置,并提供更高级的功能 -->
        <mvc:annotation-driven />
    </beans>

    (4)配置 SpringSecurity,并新建一个 controller 进行测试

    【web.xml 中配置核心过滤器链 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>
    
    
    【新建一个 spring-security.xml 用于进行 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:security="http://www.springframework.org/schema/security"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
           http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
    
        <!--
            配置 Spring-Security.
            auto-config="true" 表示使用框架默认提供的登录界面
            use-expressions="true" 表示使用 Spring 的 EL 表达式
        -->
        <security:http auto-config="true" use-expressions="true">
            <!--
                配置拦截请求。
                pattern="/**" 表示拦截所有请求
                access="hasAnyRole('ROLE_USER')" 表示只有角色为 ROLE_USER 的用户才能访问并登陆系统
            -->
            <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        </security:http>
    
        <!--
            配置用户信息(用户管理)
            密码默认是加密的,若不想密码加密,则可以在 密码前面添加 {noop}
        -->
        <security:authentication-manager>
            <security:authentication-provider>
                <security:user-service>
                    <security:user name="tom" password="{noop}123456" authorities="ROLE_USER" />
                    <security:user name="jarry" password="{noop}123456" authorities="ROLE_ADMIN" />
                    <security:user name="jack" password="123456" authorities="ROLE_USER" />
                </security:user-service>
            </security:authentication-provider>
        </security:authentication-manager>
    </beans>
    
    
    【在 web.xml 中导入 spring-security.xml 文件(与导入 applicationContext.xml 类似,也可以在 applicationContext.xml 中通过 <import> 标签引入 spring-security.xml)】
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <!-- 若没有提供值,默认会去找/WEB-INF/applicationContext.xml。 -->
      <param-value>
        classpath:applicationContext.xml
        classpath:spring-security.xml
      </param-value>
    </context-param>
    
    【完整 web.xml 如下:】
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">
    
      <!-- step1: 配置全局的参数,启动Spring容器 -->
      <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- 若没有提供值,默认会去找/WEB-INF/applicationContext.xml。 -->
        <param-value>
          classpath:applicationContext.xml
          classpath:spring-security.xml
        </param-value>
      </context-param>
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
    
      <!-- step2: 配置SpringMVC的前端控制器,用于拦截所有的请求  -->
      <servlet>
        <servlet-name>springmvcDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <!-- 若没有提供值,默认会去找WEB-INF/*-servlet.xml。 -->
          <param-value>classpath:dispatcher-servlet.xml</param-value>
        </init-param>
        <!-- 启动优先级,数值越小优先级越大 -->
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>springmvcDispatcherServlet</servlet-name>
        <!-- 将DispatcherServlet请求映射配置为"/",则Spring MVC将捕获Web容器所有的请求,包括静态资源的请求 -->
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    
      <!-- step3: characterEncodingFilter字符编码过滤器,放在所有过滤器的前面 -->
      <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
          <!--要使用的字符集,一般我们使用UTF-8(保险起见UTF-8最好)-->
          <param-name>encoding</param-name>
          <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
          <!--是否强制设置request的编码为encoding,默认false,不建议更改-->
          <param-name>forceRequestEncoding</param-name>
          <param-value>false</param-value>
        </init-param>
        <init-param>
          <!--是否强制设置response的编码为encoding,建议设置为true-->
          <param-name>forceResponseEncoding</param-name>
          <param-value>true</param-value>
        </init-param>
      </filter>
      <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <!--这里不能留空或者直接写 ' / ' ,否则可能不起作用-->
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
      <!-- step4: 配置过滤器,将post请求转为delete,put -->
      <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
      </filter>
      <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
      <!-- Step5:配置 SpringSecurity 核心过滤器链 -->
      <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>
    
    【新建一个 TestController.java 进行测试】
    package com.lyh.demo.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RequestMapping("test")
    @RestController
    public class TestController {
    
        @GetMapping("/hello")
        public String hello() {
            return "hello spring security";
        }
    }

    如下图所示,未配置 springSecurityFilterChain 时,等同于普通的系统登录。
    配置 springSecurityFilterChain 后,在 spring-security.xml 中可以看到,
    配置了如下内容:
      拦截所有请求,并只允许拥有 ROLE_USER 这个角色的用户才可以登录。
      设置了三个用户,tom 为 ROLE_USER 角色,且密码未加密,所以可以正常登陆。
      jarry 为 ROLE_ADMIN 角色,没有权限,所以不能正常登陆。
      jack 为 ROLE_USER 角色,但密码被加密,所以不能正常登陆。

    5、Spring Security 过滤器链

    (1)本质
      Spring Security 基于 Servlet 过滤器实现的。
      默认由 15 个过滤器组成过滤器链(可以通过配置添加、移除过滤器),通过过滤器拦截请求并进行相关操作。

    (2)简单了解几个过滤器

    【org.springframework.security.web.context.SecurityContextPersistenceFilter】
        此过滤器主要是在 SecurityContextRepository 中 保存或者更新 SecurityContext,并交给后续的过滤器操作。
        而 SecurityContext 中保存了当前用户认证、权限等信息。
        
    【org.springframework.security.web.csrf.CsrfFilter】
        此过滤器用于防止 CSRF 攻击。Spring Security 4.0 开始,默认开启 CSRF 防护,针对 PUT、POST、DELETE 等请求进行防护。
    注:
        CSRF 指的是 Cross Site Request Forgery,即 跨站请求伪造。
        简单理解为:攻击者冒用用户身份去执行操作。
       举例:
           用户打开浏览器并成功登陆某个网站 A,
           此时用户 未登出网站 A,且在同一浏览器中新增一个 Tab 页并访问 网站 B,
           而浏览器接收到网站 B 返回的恶意代码后,在用户不知情的情况下携带 cookie 等用户信息向 A 网站发送请求。
           网站 A 处理该请求,从而导致网站 B 的恶意代码被执行。
       简单理解就是:用户登录一个网站 A,并打开了另一个网站 B,B 网站携带恶意代码 且使用用户身份去访问 网站 A。
    
        XSS 指的是 Cross Site Scripting,即 跨站脚本。
        简单理解:攻击者将恶意代码嵌入网站,当用户访问网站时导致 恶意代码被执行。
        
    【org.springframework.security.web.authentication.logout.LogoutFilter】
        匹配 URL(默认为 /logout),用于实现用户退出并清除认证信息。   
        
    【org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter】
        匹配 URL(默认为 /login),用于实现用户登录认证操作(必须为 POST 请求)。
        
    【org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter】
        若没有指定登录认证界面,此过滤器会提供一个默认的界面。
        
    【org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter】
        若没有指定登出界面,此过滤器会提供一个默认的界面。
        
    【org.springframework.security.web.authentication.AnonymousAuthenticationFilter】
        创建一个匿名身份,用于系统的访问。(兼容游客登录模式)
        
    【org.springframework.security.web.access.ExceptionTranslationFilter】
        位于整个 springSecurityFilterChain 过滤链后方,用于处理链路中的异常(跳转到指定页面或者返回错误信息)。
        
    【org.springframework.security.web.access.intercept.FilterSecurityInterceptor】
        获取资源访问的授权信息,根据 SecurityContext 中存储的用户信息来决定操作是否有权限。

    (3)这些过滤器是如何加载进来的?
      通过前面 SSM + Spring Security 可以看到,在 web.xml 中配置了名为 springSecurityFilterChain 的过滤器,可以 Debug 看下 DelegatingFilterProxy 加载的流程。

    【基本流程:】
    Step1:
        通过 DelegatingFilterProxy 过滤器的 doFilter() 获取到 FilterChainProxy 过滤器并执行。
    Step2:
        通过 FilterChainProxy 过滤器的 doFilter() 调用 doFilterInternal() 加载到 过滤器链。
    Step3:
        doFilterInternal() 内部通过 SecurityFilterChain 接口获取到 过滤器链。
    Step4:
        SecurityFilterChain 接口实现类为 DefaultSecurityFilterChain。

    二、SpringBoot + SpringSecurity 相关操作

    1、三种认证方式(设置用户名、密码)

      不进行任何 SpringSecurity 配置时,系统默认提供用户名以及密码,但是这种情况肯定不适用于工作场景。那么如何进行 认证呢?

    (1)方式一:
      通过配置文件 application.properties 或者 application.yml 中直接定义。
      不太适用于实际工作场景。

    【在 application.properties 中直接定义 用户名、密码】
    spring.security.user.name=tom
    spring.security.user.password=123456

    (2)方式二:
      通过配置类的形式。(需要继承 WebSecurityConfigurerAdapter 抽象类)
      不太适用于实际工作场景。

    【通过配置类的形式:】
    package com.lyh.demo.springsecurity.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * 配置 Spring Security
     */
    @Configuration
    public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            auth.inMemoryAuthentication()
                .withUser("jack")
    //            .password("{noop}" + "123456") // 未配置 PasswordEncoder 时,可以在 密码前拼接上 {noop},防止出错
                .password(bCryptPasswordEncoder.encode("123456"))
                .roles("admin");
        }
    
        /**
         * 配置加密类,若不配置,则 bCryptPasswordEncoder.encode() 进行加密时会出错。
         * java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
         *
         * 若不想配置,可以在 设置 password 时,在密码前添加上 {noop}
         * @return 加密类
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

    (3)方式三
      通过配置类 以及 自定义实现类(实现 UserDetailsService 接口)实现。
      适用于工作场景(从数据库中查询出用户信息并认证)。

    【步骤一:在配置类中 指定使用 UserDetailsService 接口,并注入其 实现类】
    package com.lyh.demo.springsecurity.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * 配置 Spring Security
     */
    @Configuration
    public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

    【步骤二:编写自定义实现类(实现 UserDetailsService 接口)】
    package com.lyh.demo.springsecurity.service;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service("userDetailsService")
    public class MyUserDetailsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            // 设置用户权限,若有多个权限可以使用 逗号分隔
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
            return new User("jarry", new BCryptPasswordEncoder().encode("123456"), auths);
        }
    }

    2、SpringSecurity 中登录认证过程中 的密码加密(BCryptPasswordEncoder)

    (1)为什么要了解密码加密?
      Spring Security 5.0 以上版本 对于密码处理需要特别注意一下,前面也介绍了,Spring Security 认证时会对密码进行加密,采用 {encodingId}password 的形式设置加密方式。
      如果不想密码加密,可以在配置密码时在 密码前拼接上 {noop}, 即 ({noop}password)。
      而实际场景中,数据库存储的密码都是非明文存储(即存储的都是加密后的密码),所以有必要了解一下 SpringSecurity 加密相关内容。

    【相关的 encodingId 与其 对应的 实体类 如下:】
    public static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new LdapShaPasswordEncoder());
        encoders.put("MD4", new Md4PasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new StandardPasswordEncoder());
        encoders.put("argon2", new Argon2PasswordEncoder());
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

    (2)PasswordEncoder
      SpringSecurity 默认需要在容器中存在 PasswordEncoder 实例对象,用于进行密码加密。所以配置 SpringSecurity 时,需要在容器中配置一个 PasswordEncoder Bean 对象(一般使用 BCryptPasswordEncoder 实例对象)。

    【在配置类中通过 @Bean 配置一个 PasswordEncoder 的 Bean 对象:】
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    【PasswordEncoder 常用方法:】
    String encode(CharSequence rawPassword);   // 用于密码加密
    boolean matches(CharSequence rawPassword, String encodedPassword); // 用于密码解密,rawPassword 表示待匹配的密码,encodedPassword 表示加密后的密码。

    (3)BCryptPasswordEncoder
      是最常用的一种密码解析器,其通过 哈希算法 并加上 随机盐(salt)的方式进行密码加密。
      密码解密时,根据加密后的数据 A 得到盐值(salt),将待比较数据根据盐值进行一次加密得到 B,如果 B 与 A 是相同的结果,则说明密码是正确的。
      密码加密、解密的关键点在于 盐值的计算。

    密码加密相关代码如下所示:

    【 encode() 加密:】
    加密代码如下所示,首先调用 BCrypt.gensalt() 方法计算出 盐值(salt),
    然后调用 BCrypt.hashpw() 方法,根据 盐值(salt)进行密码(password)加密。
    
    而在 BCrypt 中的 hashpw() 中,会通过 salt.substring() 截取并得到真实的盐值(real_salt),
    通过 B.crypt_raw() 求得一个哈希数组(hashed),通过 encode_base64() 进行加密。
    
    public String encode(CharSequence rawPassword) {
       if (rawPassword == null) {
          throw new IllegalArgumentException("rawPassword cannot be null");
       }
    
       String salt;
       if (random != null) {
          salt = BCrypt.gensalt(version.getVersion(), strength, random);
       } else {
          salt = BCrypt.gensalt(version.getVersion(), strength);
       }
       return BCrypt.hashpw(rawPassword.toString(), salt);
    }
    
    【BCrypt】
    public static String hashpw(String password, String salt) {
       byte passwordb[];
    
       passwordb = password.getBytes(StandardCharsets.UTF_8);
    
       return hashpw(passwordb, salt);
    }
    
    public static String hashpw(byte passwordb[], String salt) {
       BCrypt B;
       String real_salt;
       byte saltb[], hashed[];
       char minor = (char) 0;
       int rounds, off;
       StringBuilder rs = new StringBuilder();
    
       if (salt == null) {
          throw new IllegalArgumentException("salt cannot be null");
       }
    
       int saltLength = salt.length();
    
       if (saltLength < 28) {
          throw new IllegalArgumentException("Invalid salt");
       }
    
       if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
          throw new IllegalArgumentException ("Invalid salt version");
       if (salt.charAt(2) == '$')
          off = 3;
       else {
          minor = salt.charAt(2);
          if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b')
                || salt.charAt(3) != '$')
             throw new IllegalArgumentException ("Invalid salt revision");
          off = 4;
       }
    
       // Extract number of rounds
       if (salt.charAt(off + 2) > '$')
          throw new IllegalArgumentException ("Missing salt rounds");
    
       if (off == 4 && saltLength < 29) {
          throw new IllegalArgumentException("Invalid salt");
       }
       rounds = Integer.parseInt(salt.substring(off, off + 2));
    
       real_salt = salt.substring(off + 3, off + 25);
       saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
    
       if (minor >= 'a') // add null terminator
          passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
    
       B = new BCrypt();
       hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);
    
       rs.append("$2");
       if (minor >= 'a')
          rs.append(minor);
       rs.append("$");
       if (rounds < 10)
          rs.append("0");
       rs.append(rounds);
       rs.append("$");
       encode_base64(saltb, saltb.length, rs);
       encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
       return rs.toString();
    }

    密码解密相关代码如下所示:

    【matches() 解密:】
    解密代码如下所示,首先确保 encodedPassword 是加密后的代码。
    然后调用 BCrypt.checkpw() 进行密码匹配。
    
    而 BCrypt 的 checkpw() 中,可以看到其会将待比较的密码 重新进行一次 hashpw() 密码加密。
    而此时传入的盐值是 加密的代码,在 hashpw() 方法中会截取出相应的 盐值(real_salt)并用于加密。
    加密完成后,再去比较新加密的密码 与 原来加密的密码 是否相同即可。
    
    所以如果待比较的密码 与 加密的密码是相同的,也即相当于 根据相同的 盐值 再加密了一次,加密结果是相同的。
    
    
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
       if (rawPassword == null) {
          throw new IllegalArgumentException("rawPassword cannot be null");
       }
    
       if (encodedPassword == null || encodedPassword.length() == 0) {
          logger.warn("Empty encoded password");
          return false;
       }
    
       if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
          logger.warn("Encoded password does not look like BCrypt");
          return false;
       }
    
       return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
    }
    
    【BCrypt】
    public static boolean checkpw(String plaintext, String hashed) {
       return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
    }
    
    static boolean equalsNoEarlyReturn(String a, String b) {
       return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
    }

    3、从数据库中查询用户信息并认证(MyBatis-Plus + MySQL 8 )

    (1)建表(SQL)
      MyBatis-Plus 使用可以参考:https://www.cnblogs.com/l-y-h/p/12859477.html

      配置 SpringSecurity 时需要配置 使用密码加密,
      若不使用加密,则需在设置密码时在密码前拼接上 {noop}。
      若使用加密,则使用 BCryptPasswordEncoder 的 encode() 方法对其进行加密。
      若数据库存储的已经是 BCryptPasswordEncoder 加密后的数据,不用再次加密。

    此处为了方便理解,存储密码时均使用 明文存储。

    【建表 SQL :】
    DROP DATABASE IF EXISTS testSpringSecurity;
    
    CREATE DATABASE testSpringSecurity;
    
    USE testSpringSecurity;
    
    DROP TABLE IF EXISTS users;
    
    CREATE TABLE users
    (
        id BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
        name VARCHAR(30) NOT NULL COMMENT '姓名',
        password VARCHAR(64) NOT NULL COMMENT '密码',
        role VARCHAR(20) NOT NULL COMMENT '角色'
    );
    
    INSERT INTO users (name, password, role) VALUES
    ('tom', '123456', 'user'),
    ('jarry', '123456', 'admin'),
    ('jack', '123456', 'ROLE_USER');

    (2)引入 MyBatis-Plus 与 MySQL 相关依赖

    【依赖:】
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1.tmp</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.18</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
    </dependency>

    (3)配置 MyBatis-Plus 以及 MySQL 数据源信息

    【数据源信息】
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/testSpringSecurity?useUnicode=true&characterEncoding=utf8

    (4)编写 数据表对应的 实体类,以及相应的 mapper 或者 service(用于操作数据库)

    【实体类:】
    package com.lyh.demo.springsecurity.entity;
    
    import lombok.Data;
    
    @Data
    public class Users {
        private Long id;
        private String name;
        private String password;
        private String role;
    }
    
    【Mapper:】
    package com.lyh.demo.springsecurity.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.lyh.demo.springsecurity.entity.Users;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Service;
    
    @Mapper
    @Service
    public interface UsersMapper extends BaseMapper<Users> {
    }

    (5)结合 SpringSecurity 进行安全验证。
      通过前面分析,添加上 SpirngSecurity 配置类后,会执行 loadUserByUsername() 方法将需要认证的用户信息加载到当前认证系统中,所以在此添加 查询数据库的逻辑即可。
      首先根据用户名 在数据库中 查询出相应的 用户、密码 并封装到 实体类中,并将此时的用户、密码、角色等加入到 当前认证系统中。然后再根据 输入的用户名、密码 进行验证。

    【修改 MyUserDetailsService 中 loadUserByUsername() 代码:(改为从数据库中获取用户)】
    package com.lyh.demo.springsecurity.service;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.lyh.demo.springsecurity.entity.Users;
    import com.lyh.demo.springsecurity.mapper.UsersMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service("userDetailsService")
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        private UsersMapper usersMapper;
    
        @Override
        public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
            // 定义查询条件,根据用户名 从数据库查询 对应的 用户、密码、角色
            QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("name", name);
            Users user = usersMapper.selectOne(queryWrapper);
    
            // 用户不存在时,直接抛异常
            if (user == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
    
            // 用户存在时,把 用户、密码、角色 加入到当前认证系统中
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole());
            // 将数据库中的密码进行 加密
            return new User(user.getName(), new BCryptPasswordEncoder().encode(user.getPassword()), auths);
    //        return new User(user.getName(), user.getPassword(), auths); // 若数据库密码已经加密过,直接使用即可
        }
    }

    4、自定义页面(不使用默认页面)以及 页面跳转、页面访问权限控制

    (1)自定义页面
      在前面与 数据库 交互的基础上,添加如下代码。

    【登录页面:(login.html)】
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
    <body>
    <div>
        <form method="post" action="/login">
            <h2>Please sign in</h2>
            <p>
                <label for="username">Username</label>
                <input type="text" id="username" name="username" placeholder="Username" required="" autofocus="">
            </p>
            <p>
                <label for="password" class="sr-only">Password</label>
                <input type="password" id="password" name="password" placeholder="Password" required="">
            </p>
            <button type="submit">Sign in</button>
        </form>
    </div>
    </body>
    </html>403 页面:】
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>403</title>
    </head>
    <body>
    <h1>403</h1>
    </body>
    </html>
    
    【在 TestController 中添加一个 处理错误的逻辑:】
    package com.lyh.demo.springsecurity.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RequestMapping("test")
    @RestController
    public class TestController {
    
        @GetMapping("/hello")
        public String hello() {
            return "hello spring security";
        }
    
        @PostMapping("/error")
        public String error() {
            return "login error";
        }
    }

    (2)编写配置类,配置页面跳转规则
      在配置类中,重写 configure() 方法,并通过 formLogin() 方法设置相关页面。

    【配置类中重写 configure() 方法:】
    package com.lyh.demo.springsecurity.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * 配置 Spring Security
     */
    @Configuration
    public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            /**
             * csrf() 表示开启 csrf 防护。
             * disable() 表示关闭 csrf 防护。
             */
            http.csrf().disable();
    
            /**
             * formLogin() 用于自定义表单登录。
             * loginPage() 用于自定义登录页面。
             * defaultSuccessUrl() 登录成功后 跳转的路径。
             * loginProcessingUrl() 表单提交的 action 地址(默认为 /login,修改后,对应的表单 action 也要修改),由系统提供 UsernamePasswordAuthenticationFilter 过滤器拦截并处理。
             * usernameParameter() 用于自定义表单提交的用户参数名,默认为 username,修改后,对应的表单参数也要修改。
             * passwordParameter() 用于自定义表单提交的用户密码名,默认为 password,修改后,对应的表单参数也要修改。
             * failureForwardUrl() 用于自定义表单提交失败后 重定向地址,可用于前后端分离中,指向某个 controller,注意使用 POST 处理。
             */
            http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/test/hello")
                //.usernameParameter("name")
                //.passwordParameter("pwd")
                .failureForwardUrl("/test/error")
            ;
    
            /**
             * authorizeRequests()  用于 开启认证,基于 HttpServletRequest 对 url 进行身份控制并授权访问。
             * antMatchers() 用于匹配 url。
             * permitAll() 用于允许任何人访问该 url。
             * hasAuthority() 用于指定 具有某种权限的 人才能访问 url。
             * hasAnyAuthority() 用于指定 多个权限 进行访问,多个权限间使用逗号分隔。
             *
             * hasRole() 写法与 hasAuthority() 类似,但是其会在 角色前 拼接上 ROLE_,使用时需要注意。
             * hasAnyRole() 写法与 hasAnyAuthority() 类似,同样会在 角色前 拼接上 ROLE_。
             *
             * 使用时 hasAuthority()、hasAnyAuthority() 或者 hasAnyRole()、hasAnyAuthority() 任选一对即可,同时使用四种可能会出现问题。
             */
            http.authorizeRequests()
                    .antMatchers("/test/hello").hasAuthority("user")
                    //.antMatchers("/test/hello").hasAnyRole("USER,GOD")
                    //.antMatchers("/test/hello").hasRole("GOD")
                    .antMatchers("/test/hello").hasAnyAuthority("user,admin")
                    .antMatchers("/login", "/test/error").permitAll();
    
            /**
             * 自定义 403 页面
             */
            http.exceptionHandling().accessDeniedPage("/403.html");
        }
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

      如下图所示,tom 的角色为 user、jarry 的角色为 admin,jack 的角色为 ROLE_USER。
      只允许 user、admin 角色能够访问 /test/hello,即 tom、jarry 可以成功访问系统,而 jack 访问时会跳转到 403 页面,若 用户名 或者 密码输入错误时,将会跳转到 /test/error 画面。

    5、了解几个注解

      为了简化开发,可以使用注解进行相关操作(操作不太灵活,慎用)。
    (1)@Secured
      添加在 方法上,并可以指定用户角色,作用是只允许指定的用户角色去访问 该方法。

    【使用步骤一:】
        在配置类上,通过 @EnableGlobalMethodSecurity(securedEnabled = true) 开启注解。
        
    【使用步骤二:】
        在方法上添加注解 @Secured,并指定 角色,角色前缀要为 ROLE_。
        
    @GetMapping("/testSecured")
    @Secured({"ROLE_USER"})
    public String testSecured() {
        return "success";
    }
    
    注:
        由于 角色需要使用 ROLE_ 为前缀,所以数据库存储的 角色需要以 ROLE_ 为前缀 或者 设置权限时手动加上 ROLE_。

    (2)@PreAuthorize
      添加在 方法上,并可以指定用户角色,作用是只允许指定的用户角色去访问 该方法。
      在进入方法之前 会进行 校验,校验通过后才能执行方法。

    【使用步骤一:】
        在配置类上,通过 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启注解。
        
    【使用步骤二:】
        在方法上添加 @PreAuthorize 注解,并指定角色,角色的指定可以使用 Spring 表达式。
        
    @GetMapping("/testSecured")
    @PreAuthorize("hasAnyAuthority('user', 'ROLE_USER')")
    public String testSecured() {
        return "success";
    }

    (3)@PostAuthorize
      添加在 方法上,并可以指定用户角色,作用是只允许指定的用户角色去访问 该方法。
      在进入方法之后 会进行 校验。不管有没有权限,都会执行方法,适合带有返回值的校验。

    【使用步骤一:】
        在配置类上,通过 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启注解。
    
    【使用步骤二:】
        在方法上添加 @PostAuthorize 注解,并指定角色,角色的指定可以使用 Spring 表达式。
    
    @GetMapping("/testSecured")
    @PostAuthorize("hasAuthority('user')")
    public String testSecured() {
        System.out.println("不管有没有权限,我都会执行");
        return "success";
    }

    6、用户注销操作

    (1)自定义一个登录成功页面,并添加一个 退出链接。

    【登录成功页面 success.html】
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>success</title>
    </head>
    <body>
    <h1>Success</h1>
    <a href="/logout">注销</a>
    </body>
    </html>

    (2)编写配置类,修改页面退出规则。
      此处为了跳转到 success.html 页面,还需要 通过 http.formLogin().defaultSuccessUrl() 去指定页面。

    【添加退出规则:】
    http.formLogin()
        .loginPage("/login.html")
        .loginProcessingUrl("/login")
        .defaultSuccessUrl("/success.html")
        //.usernameParameter("name")
        //.passwordParameter("pwd")
        .failureForwardUrl("/test/error")
    ;
    
    /**
     * logout() 用于自定义退出逻辑。
     * logoutUrl() 用于拦截退出请求,默认为 /logout。
     * logoutSuccessUrl() 用于自定义退出成功后,跳转的页面。
     */
    http.logout()
        .logoutUrl("/logout")
        .logoutSuccessUrl("/login.html")
    ;

      如下图所示,tom、jarry 可以访问 /test/hello,jack 不可以访问,所以当使用 tom、jarry 登录时可以成功登陆,jack 会显示 403,一旦点击注销后,需要再次进行登录才能继续访问 /test/hello。

    7、记住我

    (1)工作流程
      记住我 功能上指的是 用户通过浏览器登录一次网站后,关闭浏览器并再次访问网站时,可以不用再次登录而直接进行相关操作。

    【工作流程:】
    第一次通过浏览器登录系统时:
        首先 用户名、密码 会被 UsernamePasswordAuthenticationFilter 过滤器拦截,并进行认证。
        认证通过后,会调用 RememberMeServices 生成 token,并将 token 写入数据库 以及 浏览器 cookie 中。
        
    第二次通过浏览器登录系统时:
        直接携带 cookie 访问,会被 RememberMeAuthenticationFilter 过滤器拦截,根据 cookie 读取出 token 信息。
        从数据库中查找出 对应的 token 并比较,若相同,则可以登录系统,否则跳转到登录页面。  

    工作流程见下图(图片来源于网络):

    (2)基本实现:
      由于 token 需要存储在 数据库中,所以需要配置数据源信息,并操作,而 SpringSecurity 中已经提供了相关操作类,只需在配置类中配置即可。

    【配置如下:(注入 DataSource,并配置 PersistentTokenRepository 交给 Spring 管理)】
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 设置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自动建表
        // jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
    
    
    【完整配置类:(通过  http.rememberMe() 配置)】
    package com.lyh.demo.springsecurity.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    
    import javax.sql.DataSource;
    
    /**
     * 配置 Spring Security
     */
    @Configuration
    @EnableWebSecurity
    public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Autowired
        private DataSource dataSource;
    
        /**
         * 默认使用 PersistentTokenRepository 的子类  InMemoryTokenRepositoryImpl 将 token 放在内存中,
         * 可以使用子类 JdbcTokenRepositoryImpl 将 token 持久化到 数据库中。
         * 注:
         *
         *  jdbcTokenRepository.setCreateTableOnStartup(true); 等同于下面 SQL,
         *  若不手动创建,可以使用代码自动创建,但是执行一次后需要将其注释掉。
         *
         *  create table persistent_logins (
         *         username varchar(64) not null,
         *         series varchar(64) primary key,
         *         token varchar(64) not null,
         *         last_used timestamp not null
         *  )
         */
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            // 设置数据源
            jdbcTokenRepository.setDataSource(dataSource);
            // 自动建表
            // jdbcTokenRepository.setCreateTableOnStartup(true);
            return jdbcTokenRepository;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            /**
             * rememberMe() 用于实现记住我功能。
             * tokenRepository() 设置数据访问层。
             * userDetailsService() 设置 userDetailsService。
             * tokenValiditySeconds() 设置过期时间。
             * rememberMeParameter() 自定义参数名,默认为 remember-me
             */
            http.rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .userDetailsService(userDetailsService)
                    //.rememberMeParameter("remember")
                    .tokenValiditySeconds(24 * 60 * 60);
    
            /**
             * csrf() 表示开启 csrf 防护。
             * disable() 表示关闭 csrf 防护。
             */
            http.csrf().disable();
    
            /**
             * formLogin() 用于自定义表单登录。
             * loginPage() 用于自定义登录页面。
             * defaultSuccessUrl() 登录成功后 跳转的路径。
             * loginProcessingUrl() 表单提交的 action 地址(默认为 /login,修改后,对应的表单 action 也要修改),由系统提供 UsernamePasswordAuthenticationFilter 过滤器拦截并处理。
             * usernameParameter() 用于自定义表单提交的用户参数名,默认为 username,修改后,对应的表单参数也要修改。
             * passwordParameter() 用于自定义表单提交的用户密码名,默认为 password,修改后,对应的表单参数也要修改。
             * failureForwardUrl() 用于自定义表单提交失败后 重定向地址,可用于前后端分离中,指向某个 controller。
             */
            http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/success.html")
                //.usernameParameter("name")
                //.passwordParameter("pwd")
                .failureForwardUrl("/test/error")
            ;
    
            /**
             * logout() 用于自定义退出逻辑。
             * logoutUrl() 用于拦截退出请求,默认为 /logout。
             * logoutSuccessUrl() 用于自定义退出成功后,跳转的页面。
             */
            http.logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login.html")
            ;
    
            /**
             * authorizeRequests()  用于 开启认证,基于 HttpServletRequest 对 url 进行身份控制并授权访问。
             * antMatchers() 用于匹配 url。
             * permitAll() 用于允许任何人访问该 url。
             * hasAuthority() 用于指定 具有某种权限的 人才能访问 url。
             * hasAnyAuthority() 用于指定 多个权限 进行访问,多个权限间使用逗号分隔。
             *
             * hasRole() 写法与 hasAuthority() 类似,但是其会在 角色前 拼接上 ROLE_,使用时需要注意。
             * hasAnyRole() 写法与 hasAnyAuthority() 类似,同样会在 角色前 拼接上 ROLE_。
             *
             * 使用时 hasAuthority()、hasAnyAuthority() 或者 hasAnyRole()、hasAnyAuthority() 任选一对即可,同时使用四种可能会出现问题。
             */
            http.authorizeRequests()
                    .antMatchers("/test/hello").hasAuthority("user")
                    //.antMatchers("/test/hello").hasAnyRole("USER,GOD")
                    //.antMatchers("/test/hello").hasRole("GOD")
                    .antMatchers("/test/hello").hasAnyAuthority("user,admin")
                    .antMatchers("/login", "/test/error").permitAll();
    
            /**
             * 自定义 403 页面
             */
            http.exceptionHandling().accessDeniedPage("/403.html");
        }
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

      如下图所示,点击 记住我,并登陆后,会在浏览器 cookie 以及 数据库中 各存放一份 token,关闭浏览器并再次登录时,无需重新登录,会自动检测 cookie 中的 token 值是否正确,若相同则可以正常登陆。注销时,浏览器 token 以及 数据库的 token 会一起注销。

    (3)源码分析:
    Step1:
      第一次通过浏览器登录系统时,首先会被 UsernamePasswordAuthenticationFilter 过滤器拦截,认证通过后,会在 AbstractAuthenticationProcessingFilter 抽象类的 successfulAuthentication() 方法中 进行 token 的处理。

    【UsernamePasswordAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter:】
        public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {}
    
    【AbstractAuthenticationProcessingFilter 的 doFilter() 中 调用了 successfulAuthentication() 方法:】
    public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
          implements ApplicationEventPublisherAware, MessageSourceAware {
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
          throws IOException, ServletException {
          ...
          successfulAuthentication(request, response, chain, authResult);
      }
    }

    Step2:
      AbstractAuthenticationProcessingFilter 中定义了 RememberMeServices 接口,在 successfulAuthentication() 方法中 会调用 RememberMeServices 接口的 loginSuccess() 方法。

    【调用 RememberMeServices 的 loginSuccess() 方法:】
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    protected void successfulAuthentication(HttpServletRequest request,
          HttpServletResponse response, FilterChain chain, Authentication authResult)
          throws IOException, ServletException {
          ...
          rememberMeServices.loginSuccess(request, response, authResult);
    }

    Step3:
      RememberMeServices 接口的 loginSuccess() 方法 由子类 AbstractRememberMeServices 实现,loginSuccess() 会先检测是否存在 记住我 的功能,默认参数名为 remember-me,若表单中不存在 或者 为 false 时,会直接返回。为 true 时,会执行 onLoginSuccess() 方法。

    【调用 AbstractRememberMeServices 的 loginSuccess() 方法:】
    public final void loginSuccess(HttpServletRequest request,
          HttpServletResponse response, Authentication successfulAuthentication) {
    
       if (!rememberMeRequested(request, parameter)) {
          logger.debug("Remember-me login not requested.");
          return;
       }
    
       onLoginSuccess(request, response, successfulAuthentication);
    }

    Step4:
      onLoginSuccess() 方法由 AbstractRememberMeServices 的子类 PersistentTokenBasedRememberMeServices 去实现。向数据库中 添加 token 以及 向 cookie 中添加 token。

    【调用 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法:】
    protected void onLoginSuccess(HttpServletRequest request,
          HttpServletResponse response, Authentication successfulAuthentication) {
       String username = successfulAuthentication.getName();
    
       logger.debug("Creating new persistent login for user " + username);
    
       PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
             username, generateSeriesData(), generateTokenData(), new Date());
       try {
          tokenRepository.createNewToken(persistentToken);
          addCookie(persistentToken, request, response);
       }
       catch (Exception e) {
          logger.error("Failed to save persistent token ", e);
       }
    }

    Step5:
      关闭浏览器,再次登录时,由 RememberMeAuthenticationFilter 过滤器拦截请求,在其 doFilter() 方法中 调用 RememberMeServices 接口的 autoLogin() 方法进行处理。

    【RememberMeAuthenticationFilter 的 】
    public class RememberMeAuthenticationFilter extends GenericFilterBean implements
          ApplicationEventPublisherAware {
        private RememberMeServices rememberMeServices;
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
          throws IOException, ServletException {
              ...
              Authentication rememberMeAuth = rememberMeServices.autoLogin(request,response);
              ...
          }
    }

    Step6:
      RememberMeServices 接口的 autoLogin() 方法由 AbstractRememberMeServices 子类实现,其根据 cookie 值解析出相应的 token。并根据 token 从数据库中查询用户,并验证用户是否合法。

    public final Authentication autoLogin(HttpServletRequest request,
          HttpServletResponse response) {
        String rememberMeCookie = extractRememberMeCookie(request);
        ...
        String[] cookieTokens = decodeCookie(rememberMeCookie);
        user = processAutoLoginCookie(cookieTokens, request, response);
        userDetailsChecker.check(user);
    }

    Step7:
      调用 processAutoLoginCookie() 方法根据 token 从数据库中查询出用户。

    protected UserDetails processAutoLoginCookie(String[] cookieTokens,
          HttpServletRequest request, HttpServletResponse response) {
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
    
        PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
        if (!presentedToken.equals(token.getTokenValue())) {
        }
        ...
        return getUserDetailsService().loadUserByUsername(token.getUsername());    
    }

  • 相关阅读:
    canvasnode的设计思路和api介绍
    希望新浪网络学院的童鞋们加油
    和新童鞋们吃饭,见到了jeremy
    MongoDB 学习资料
    [转] Scrum框架及其背后的原则
    twisted学习资料
    创建课程表
    进程
    协程
    支付宝支付流程
  • 原文地址:https://www.cnblogs.com/l-y-h/p/14010034.html
Copyright © 2020-2023  润新知