当前位置: 首页 > news >正文

Spring Boot | Spring Boot 实现 “记住我“ 功能

目录:

  • 一、SpringBoot 中 自定义 "用户授权管理" ( 总体内容介绍 )
  • 二、实现 "记住我" 功能 ( 通过 "HttpSecurity类" 的 rememberMe( )方法来实现 "记住我" 功能 ) :
      • 2.1 基于 "简单加密 Token" 的方式 ( 实现 "记住我" 功能 ) - 存在 "安全隐患",不建议使用该方式
        • 基础项目文件准备
        • 实现 "自定义身份认证" ( UserDetailsService身份认证 )
          • ① service层中类 获取 "用户基本信息" 和 "用户权限信息"
          • ② "自定义类" 实现 "UserDetailsService接口" , 在该类中 封装 "用户身份认证信息"
          • ③ "SecurityConfig配置类" 中 实现 "自定义 身份认证"
        • 实现 "自定义用户访问"
          • ④ "SecurityConfig配置类" 中 实现 "自定义 用户访问控制"
          • ⑤ controller层中 实现 "路径访问跳转"
        • 实现 "自定义用户登录"
          • ⑥ 自定义 用户登录 "页面"
          • ⑦ 自定义 用户登录 "跳转"
          • ⑧ 自定义 用户登录 "控制"
        • 实现 "自定义用户退出"
          • ⑨ 添加自定义 "用户退出链接"
          • ⑩ 添加自定义 "用户退出控制"
        • 基于 "简单加密Token方式" 实现 "记住我"功能
          • ⑪ 前端页面中添加 “记住我” 框
          • ⑫ 定制 "记住我" 功能
      • 2.2 基于持久化 Token 的方式 ( 这种方式 比 "第一种" 方式更安全 )
        • 基础项目文件准备
        • 实现 "自定义身份认证" ( UserDetailsService身份认证 )
          • ① service层中类 获取 "用户基本信息" 和 "用户权限信息"
          • ② "自定义类" 实现 "UserDetailsService接口" , 在该类中 封装 "用户身份认证信息"
          • ③ "SecurityConfig配置类" 中 实现 "自定义 身份认证"
        • 实现 "自定义用户访问"
          • ④ "SecurityConfig配置类" 中 实现 "自定义 用户访问控制"
          • ⑤ controller层中 实现 "路径访问跳转"
        • 实现 "自定义用户登录"
          • ⑥ 自定义 用户登录 "页面"
          • ⑦ 自定义 用户登录 "跳转"
          • ⑧ 自定义 用户登录 "控制"
        • 实现 "自定义用户退出"
          • ⑨ 添加自定义 "用户退出链接"
          • ⑩ 添加自定义 "用户退出控制"
        • 基于 "持久化 Token方式" 实现 "记住我"功能
          • ⑪ 创建 "存储Token信息" 的 persistent_logins "数据库表"
          • ⑫ 定制 "记住我" 功能 ( ①注入DataSource数据库信息 + ②调用rememberMe( )方法 + 配置JdbcTokenRepositoryImpl对象并将其加入到IOC容器中 )
          • ⑬ 效果测试

在这里插入图片描述

作者简介 :一只大皮卡丘,计算机专业学生,正在努力学习、努力敲代码中! 让我们一起继续努力学习!

该文章参考学习教材为:
《Spring Boot企业级开发教程》 黑马程序员 / 编著
文章以课本知识点 + 代码为主线,结合自己看书学习过程中的理解和感悟 ,最终成就了该文章

文章用于本人学习使用 , 同时希望能帮助大家。
欢迎大家点赞👍 收藏⭐ 关注💖哦!!!

(侵权可联系我,进行删除,如果雷同,纯属巧合)


一、SpringBoot 中 自定义 “用户授权管理” ( 总体内容介绍 )

  • 一个系统建立之后,通常需要适当地做一些权限控制,使得 不同用户具有不同的权限 操作系统
    例如一般的项目中都会做一些简单的登录控制,只有特定用户才能登录访问。接下来将 针对 Web 应用中常见自定义用户授权管理 进行 介绍

  • SpringBoot自定义 “用户授权管理”实现方式 :
    创建类 继承(extens) WebSecurityConfigurerAdapter类

    重写 WebSecurityConfigurerAdapter类 中的 configure( HttpSecurity http )方法

    通过 HttpSecurity类中的 Xxx方法 来 实现自定义 “用户授权管理”

    ( 通过 configure( HttpSecurity http ) 方法 中的 HttpSecurity 类 实现/进行 “用户授权管理” )

  • HttpSecurity类主要方法说明 ( 通过 该类中的方法 来实现 "用户授权管理"):

    方法描述
    authorizeRequests( ) :
    授权请求
    开启基于 “HttpServletRequest” 请求访问限制
    ps :
    用于实现 “自定义用户访问控制
    ( 通过configure( HttpSecurity http)方法 中的 HttpSecurity类authorizeRequests( )方法实现 “自定义用户访问控制”其他方法则是以此类推。)
    formLogin( )开启基于表单用户登录
    ps :
    用于实现 “自定义用户登录页面
    使用该方法就是 使用 security提供的"默认登录"页面进行"登录验证" ( 如果没有指定 "自定义的登录页面" 的话 )
    httpBasic( )开启基于 HTTP 请求Basic 认证登录
    logout( )开启退出登录支持
    sessionManagement( )开启 Session 管理配置
    rememberMe( )开启 记住我 功能
    csrf( )配置 “CSRF” 跨站请求伪造防护功能

    补充 :
    configure ( HttpSecurity http )方法参数类型HttpSecurity 类HttpSecurity 类提供了 Http请求的限制权限Session 管理配置CSRF跨站请求问题等方法

二、实现 “记住我” 功能 ( 通过 “HttpSecurity类” 的 rememberMe( )方法来实现 “记住我” 功能 ) :

  • 实际开发中,有些项目 为了用户登录方便还会提供记住我( Remember-Me )功能。如果用户登录时勾选了 “记住我” 选项,那么在一段有效时间内,会默认自动登录并允许访问相关页面,这就 免去了重复登录操作麻烦

  • 实现 "记住我 功能 " 的 具体实际操作为 : 通过 WebSecurityConfigurerAdapter 类configure( HttpSecurity http )方法 中的 HttpSecurity 类中的 rememberMe( )方法可以实现 "记住我 功能 " 。

  • rememberMe( )方法 中 的 主要方法说明下表所示 :

    方法 ( 实现 “记住我” 功能的 方法 )描述
    rememberMeParameter( String rememberMeParameter )方法指定登录时 “记住” 用户HTTP 参数 ( 即指定 "记住我"功能框 中的 name属性值 )
    默认值为 : remember-me , 此时 前端的 "记住我"这个name属性属性值必须为 : remember-me
    当然 前端可修改对应的属性值,但 后端rememberMeParameter( ) 方法处也要对应的进行修改
    key( String key )方法设置 记住我认证生成Token 令牌标识
    tokenValiditySeconds( int token aliditySeconds )方法设置 记住我 Token 令牌有效期单位s()。
    ps
    设置 记住我功能中的 token有效期
    tokenRepository( PersistentTokenRepository tokenRepository )方法指定使用PesistentTokenRepository,用来 配置持久化 Token令牌
    ps :
    调用该方法来将Cookie信息/Token信息存储” 到 数据库 中。
    alwaysRemember( boolean alwaysRemember )方法是否应该始终创建 “记住我 Cookie”默认false
    clearAuthentication( boolean clearAuthentication )方法是否设置 Cookie安全的,如果设置true,则 必须通过 HTTPS进行连接请求
  • 需要说明的是Spring Security 针对 “记住我” 功能 提供了 两种实现 : 一种“简单地” 使用加密来保证基于 CookieToken 的安全 ; 另一种 是通过 数据库其他持久化机制保存生成的Token

2.1 基于 “简单加密 Token” 的方式 ( 实现 “记住我” 功能 ) - 存在 “安全隐患”,不建议使用该方式

  • 基于 简单加密 Token 的方式实现 “记住我功能 非常简单当用户选择 "记住我"成功登录后Spring Security 将会 生成一个 Cookie发送客户端浏览器。其中,Cookie 值下列方式组合 加密而成

    base64(username + ":" + expirationTime + ":" + md5Hex(username) + ":" + expirationTime + ":" password + ":" + key))
    

    上述 Cookie 值生成方式中,userame 代表 登录的用户名password 代表 登录用户密码expirationTime 表示 记住我中的 Token 的失效目期,以毫秒单位key 表示 防止修改 Token标识


    基于简单加密 Token 的方式中的 Token指定的时间内有效,且 必须保证 Token 中所包含的 username、password 和 key 没有被改变。需要注意的是,这种 加密方式其实是 存在安全隐患的 ,任何人获取到记住我功能Token后,都可以在该 Token 过期之前进行自动登录只有当用户觉察到 Token 被盗用后才会对自己的登录密码进行修改来立即使其原有的记住我 Token 失效

基础项目文件准备
  • 创建项目 :

    在这里插入图片描述

  • 项目结构 :

    在这里插入图片描述

  • pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.itheima</groupId><artifactId>chapter07</artifactId><version>0.0.1-SNAPSHOT</version><name>chapter07</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><!-- Security与Thymeleaf整合实现前端页面安全访问控制 --><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency><!-- JDBC数据库连接启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- MySQL数据连接驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Redis缓存启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Spring Data JPA操作数据库  --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- 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-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><!--    <build>--><!--        <plugins>--><!--            <plugin>--><!--                <groupId>org.springframework.boot</groupId>--><!--                <artifactId>spring-boot-maven-plugin</artifactId>--><!--            </plugin>--><!--        </plugins>--><!--    </build>--></project>
    
    • 导入 Sql文件 ( 创建数据库表 ) :
      security.sql

    • 创建实体类 :

      Customer.java :

      import javax.persistence.*;@Entity(name = "t_customer")public class Customer {@Id@GeneratedValue(strategy = GenerationType.IDENTITY) //主键自增private Integer id;private String username;private String password;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {return "Customer{" +"id=" + id +", username='" + username + '\'' +", password=" + password +'}';}
      }
      

    Authority.java

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;@Entity(name = "t_authority ")
    public class Authority {@Id@GeneratedValue(strategy = GenerationType.IDENTITY) //主键自增private Integer id;private String authority ;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getAuthority() {return authority;}public void setAuthority(String authority) {this.authority = authority;}@Overridepublic String toString() {return "Authority{" +"id=" + id +", authority='" + authority + '\'' +'}';}
    }
    
  • 创建Repository接口文件 : ( 通过该接口的方法操作数据 ) :

    CustomerRepository.java :

    import org.springframework.data.jpa.repository.JpaRepository;public interface CustomerRepository extends JpaRepository<Customer,Integer> {//根据username查询Customer对象信息Customer findByUsername(String username);
    }
    

    AuthorityRepository.java ( 接口文件 ) :

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;import java.util.List;public interface AuthorityRepository extends JpaRepository<Authority,Integer> {//根据 username 来查询"权限信息"@Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)public List<Authority> findAuthoritiesByUsername(String username);}
    
  • 自定义序列化机制 :

    RedisConfig.java :

    package com.itheima.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@Configuration
    public class RedisConfig { //在该配置类中 自定义存储到Redis数据库的数据的 "序列化机制"/***  定制Redis API模板RedisTemplate* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate();template.setConnectionFactory(redisConnectionFactory);// 使用JSON格式序列化对象,对缓存数据key和value进行转换Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);// 解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和publicom.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jacksonSeial.setObjectMapper(om);// 设置RedisTemplate模板API的序列化方式为JSONtemplate.setDefaultSerializer(jacksonSeial);return template;}
    }
    
  • application.properties :

    spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=rootspring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=123456spring.thymeleaf.cache=false
    
  • 创建 html资源文件 :
    index.html 页面是 项目首页页面commonvip文件夹中分别对应普通用户VIP用户可访问的页面

    index.html

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf模板引擎页面配置 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="UTF-8"><title>影视直播厅</title>
    </head>
    <body><!-- index.html页面是项目首页页面,common和vip文件夹中分别对应的普通用户 和 VIP用户可访问的页面 -->
    <h1 align="center">欢迎进入电影网站首页</h1>
    <hr>
    <h3>普通电影</h3>
    <ul><li><a th:href="@{/detail/common/1}">飞驰人生</a></li><li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
    </ul>
    <h3>VIP专享</h3>
    <ul><li><a th:href="@{/detail/vip/1}">速度与激情</a></li><li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
    </ul>
    </body>
    </html>
    

    1.html : ( 其他三个页面 以此类推 )

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="UTF-8"><title>Title</title>
    </head>
    <body>
    <!-- th:href="@{/}" : 返回项目首页-->
    <a th:href="@{/}">返回</a>
    <h1>飞驰人生</h1>
    .....
    </body>
    </html>
    
实现 “自定义身份认证” ( UserDetailsService身份认证 )
① service层中类 获取 “用户基本信息” 和 “用户权限信息”
  • service层 类中来从 Redis获取 “缓存数据”,如没找到缓存数据,则从Mysql数据库查询数据 : ( 获取 ① “用户基本信息”② “用户权限信息” )

    CustomerService.java :

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;import java.util.List;/*** 该Service层类中实现的代码效果为:* 在Reids数据库中查找是否有指定"缓存数据",有则从其中获取,没有则在Mysql数据库中查找,同时还将查找到的数据也在Redis中进行"缓存"*/
    @Service //加入到IOC容器中
    public class CustomerService {@Autowiredprivate CustomerRepository customerRepository;@Autowiredprivate AuthorityRepository authorityRepository;@Autowiredprivate RedisTemplate redisTemplate;  //通过 Redis API 的方式来进行 "Redis缓存"/*** 业务控制 : 使用唯一用户名查询用户信息*/public Customer getCustomer(String username){Customer customer=null;//从Redis数据库中获取"缓存数据"Object o = redisTemplate.opsForValue().get("customer_"+username);//判断是否有该缓存数据if(o!=null){customer=(Customer)o;}else { //不存在该缓存数据,则从数据库中查询缓存数据customer = customerRepository.findByUsername(username); //根据username来在数据库中查询数据if(customer!=null){redisTemplate.opsForValue().set("customer_"+username,customer);}}return customer;}/*** 业务控制 : 使用"唯一用户名"查询用户权限*/public List<Authority> getCustomerAuthority(String username){List<Authority> authorities=null;//尝试从Redis数据库中获得缓存数据Object o = redisTemplate.opsForValue().get("authorities_"+username);if(o!=null){authorities=(List<Authority>)o;}else {//没找到缓存数据则从Mysql数据库中查询数据authorities=authorityRepository.findAuthoritiesByUsername(username);if(authorities.size()>0){redisTemplate.opsForValue().set("authorities_"+username,authorities);}}return authorities;}
    }
    
② “自定义类” 实现 “UserDetailsService接口” , 在该类中 封装 “用户身份认证信息”
  • 自定义一个 , 该 实现UserDetailsService接口 , 用该接口loadUserByUsername( )方法 来封装 "用户认证信息" , 该最终被用于 configure ( AuthenticationManagerBuilder auth ) 方法中,被最终用于 "UserDetailsService"身份认证

    UserDetailsServiceImpl.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.*;
    import org.springframework.stereotype.Service;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;/***   自定义一个类, 该类实现了UserDetailsService接口 , 用该接口的 loadUserByUsername()方法来封装"用户认证信息" ,*   该类最终被用于 configure(AuthenticationManagerBuilder auth)方法中,被最终用于 "UserDetailsService"身份认证。*/
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService { //实现 UserDetailsService接口@Autowiredprivate CustomerService customerService;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //辅助进行"用户身份认证"的方法//通过业务方法(业务层类)获取用户以及权限信息//根据username获得Customer对象信息Customer customer = customerService.getCustomer(s);List<Authority> authorities = customerService.getCustomerAuthority(s);/***  .stream() : 将"权限信息"集合 转换为一个流(Stream) , 以便进行后续的流式操作**  .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) :*     使用map操作 / map()函数 来转换流中的每个元素 (将流中的"每一个元素"转换为 "另一种形式")*     具体分析:*     authority -> 获得流中的每一个元素,将其转换为另一种形式*     authority.getAuthority() : 获得 authority 这个元素对象的"权限信息" ( 是一个"权限信息"的字符串 )*     new SimpleGrantedAuthority(authority.getAuthority())) : 使用这个 "权限信息字符串" 创建一个 SimpleGrantedAuthority对象,*     该对象 是 Spring Security框架中用于表示 "授权信息" 的类**   .collect(Collectors.toList()); : 是一个终端操作,它告诉流如何收集其元素以生成一个结果。*     具体分析:*     .collect(Collectors.toList()) : 收集转换后的 SimpleGrantedAuthority对象,并将其放入一个新的列表中*/// 对"用户权限信息"进行封装List<SimpleGrantedAuthority> list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());/*创建 UserDetails (用户详情) 对象,并将该对象进行返回*/if(customer!=null){//用 username 、password 、权限信息集合 作为参数创建 UserDetails对象 ( 用户详情对象 )UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);return userDetails;} else {//如果查询的用户不存在 (用户名不存在 ) , 必须抛出异常throw new UsernameNotFoundException("当前用户不存在!");}}
    }
    
③ “SecurityConfig配置类” 中 实现 “自定义 身份认证”
  • SecurityConfig配置类” 中 实现 “自定义身份认证 :

    SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}
    }    
    
实现 “自定义用户访问”
④ “SecurityConfig配置类” 中 实现 “自定义 用户访问控制”
  • SecurityConfig配置类” 中 实现 自定义 “用户访问控制” :

    SecurityConfig.java

    package com.itheima.config;import com.itheima.service.Impl.UserDetailsServiceImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户访问控制 http.authorizeRequests().antMatchers("/").permitAll()  //对/请求进行放行.antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)}}
    
⑤ controller层中 实现 “路径访问跳转”
  • controller层中 实现 "路径访问跳转" :

    LoginController.java

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.ResponseBody;@Controller //加入IOC容器中
    public class LoginController {  //关于跳转到登录页面有关的controller类/*跳转到Detail文件夹下的"视图页面"*/@GetMapping("/detail/{type}/{path}") //其中的变量为"路径变量"// @PathVariable注解获得"路径变量"的值public String toDetail( @PathVariable("type") String type ,@PathVariable("path") String path ) {//返回值为String,可用于返回一个视图页面return "/detail/"+type+"/"+path;}}
    
实现 “自定义用户登录”
⑥ 自定义 用户登录 “页面”
  • 要实现 自定义用户登录功能,首先必须根据需要自定义一个用户登录页面。在项目的 resources/templates 目录下新创建一个名为 login文件夹( 专门处理用户登录 ),在该文件夹中创建一个 用户登录页面 : login.html

    login.html

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf"模板引擎"页面配置 -->
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>用户登录界面</title><link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet"><link th:href="@{/login/css/signin.css}" rel="stylesheet">
    </head>
    <body class="text-center">
    <!--  form表单请求跳转路径为 : /userLogin  -->
    <form class="form-signin" th:action="@{/userLogin}" th:method="post" ><!--  img标签--><img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px"><h1 class="h3 mb-3 font-weight-normal">请登录</h1><!-- 用户登录错误信息提示框 --><!--  th:if 根据条件的真假决定是否渲染 HTML 元素 ,真则渲染,假则不渲染 (有该参数值则渲染,没有该参数值则不渲染)  --><!--  有该参数值则渲染该div,否则就不渲染该div   --><div th:if="${param.error}"style="color: red;height: 40px;text-align: left;font-size: 1.1em"><!--  img标签  --><img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!</div><!--  用户名参数名为 : name  , 密码的参数名为: pws  --><input type="text" name="name" class="form-control"placeholder="用户名" required="" autofocus=""><input type="password" name="pwd" class="form-control"placeholder="密码" required=""><button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button><p class="mt-5 mb-3 text-muted">Copyright© 2024-2025</p>
    </form>
    </body>
    </html>
    

    上面代码中引入了两个 CSS 样式文件和两个 img图片文件用来渲染用户登录页面,它们都存在于 /ogin/** 目录下,需要提前引入这些静态资源文件
    目录中。引入这些静态资源文件后结构如下图所示 :

    获得相应的css文件 和 图片文件

    在这里插入图片描述

⑦ 自定义 用户登录 “跳转”
  • 自定义 “用户登录跳转” :

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.ResponseBody;@Controller //加入IOC容器中
    public class LoginController {  //关于跳转到登录页面有关的controller类/*** 跳转到自定义的 "登录页面"*/@GetMapping("/userLogin")public String toLoginPage() {return "/login/login";}
    }
    

    Spring Security 默认采用 Get 方式“/ogin”请求 用于向 “登录页面” 跳转 ( 可自定义跳转"登录页面"的路径,来指定”登录页面“ ),使用 Post 方式的 “/ogin”请求用于对登录后的数据处理

⑧ 自定义 用户登录 “控制”
  • 完成上面的准备工作后,打开 SecurityConfig 类重写 configure( HttpSecurity http )方法,实现 用户登录控制
    示例代码如下 :

    SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制 ( 权限管理 )*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户授权管理 (自定义用户访问控制 )http.authorizeRequests() .antMatchers("/").permitAll()  //对 "/"请求 进行放行 (进入到项目首页)//对static文件夹下的静态资源进行统一放行 (如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件 ).antMatchers("/login/**").permitAll().antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)/*** 自定义"用户登录控制" :*   .loginPage() : 自定义登录页面的"跳转路径" , 跳转到自己定制的"登录页面"*   .permitAll() : 无条件对请求进行放行*   .usernameParameter("name").passwordParameter("pwd") : 设置登录用户的"用户名参数" 和 "密码参数" (接受登录时提交的"用户名"和"密码")*   .defaultSuccessUrl() : 指定用户登录成功后的默认跳转地址*   .failureUrl() : 指定用户登录失败后的"跳转地址",默认跳转到 /login?error*     ---error时一个错误标识,作用是在登录失败后在登录页面进行接收判断,例如login.html示例中的${param.error},这两者必须保持一直。*///自定义"用户登录控制"http.formLogin().loginPage("/userLogin").permitAll() //自定义登录页面的"跳转路径".usernameParameter("name").passwordParameter("pwd")  //设置登录用户的"用户名参数" 和 "密码参数".defaultSuccessUrl("/") //用户登录成功后默认跳转到"项目首页".failureUrl("/userLogin?error"); //用户登录失败后默认跳转到"项目首页"}
    }    
    
实现 “自定义用户退出”
⑨ 添加自定义 “用户退出链接”
  • 要实现 自定义用户退出功能,必须某个页面定义用户退出链接或者 按钮。为了简化操作我们在之前创建的项目首页 : index.html 上方新增一个用户退出链接修改后示范文件代码如下所示 :

    <!-- th:action : 用于指定“表单提交”时 “应发送的URL”-->
    <!-- 跳转到 /mylogout路径来进行 "用户退出" , 因为不是使用默认的 /logout路径来请求 "用户退出"  所以 后端的 .logoutUrl()方法中内容也修改为 /mylogout -->
    <!-- 同时 /mylogout 请求的方法为post类型的-->
    <form th:action="/mylogout" method="post"><input th:type="submit" th:value="注销">
    </form>
    

    index.html

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf模板引擎页面配置 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="UTF-8"><title>影视直播厅</title>
    </head>
    <body><!-- index.html页面是项目首页页面,common和vip文件夹中分别对应的普通用户 和 VIP用户可访问的页面 -->
    <h1 align="center">欢迎进入电影网站首页</h1>
    <!-- th:action : 用于指定“表单提交”时 “应发送的URL”-->
    <!-- 跳转到 /mylogout路径来进行 "用户退出" , 因为不是使用默认的 /logout路径来请求 "用户退出"  所以 后端的 .logoutUrl()方法中内容也修改为 /mylogout -->
    <!-- 同时 /mylogout 请求的方法为post类型的-->
    <form th:action="/mylogout" method="post"><input th:type="submit" th:value="注销">
    </form>
    <hr>
    <h3>普通电影</h3>
    <ul><li><a th:href="@{/detail/common/1}">飞驰人生</a></li><li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
    </ul>
    <h3>VIP专享</h3>
    <ul><li><a th:href="@{/detail/vip/1}">速度与激情</a></li><li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
    </ul>
    </body>
    </html>
    

    上面的代码中新增了一个 <form>标签进行注销控制 ( 进行 “用户退出控制”),且定义的退出表单 aciton 为“/mylogout ( 默认为“/logout” ),方法post


    需要说明的是,Spring Boot 项目中引入 Spring Security 框架后会 自动开启 : CSRF 防护功能 ( 跨站请求伪造防护),用户退出必须使用 POST请求 ; 如果关闭CSRF 防护功能,那么 可以使用任意方式HTTP 请求进行用户注销

⑩ 添加自定义 “用户退出控制”
  • SecurityConfig.java

    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制 ( 权限管理 )*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户授权管理 (自定义用户访问控制 )http.authorizeRequests().antMatchers("/").permitAll()  //对 "/"请求 进行放行 (进入到项目首页)//对static文件夹下的静态资源进行统一放行 (如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件 ).antMatchers("/login/**").permitAll().antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)/*** 自定义"用户登录控制" :*   .loginPage() : 自定义登录页面的"跳转路径" , 跳转到自己定制的"登录页面"*   .permitAll() : 无条件对请求进行放行*   .usernameParameter("name").passwordParameter("pwd") : 设置登录用户的"用户名参数" 和 "密码参数" (接受登录时提交的"用户名"和"密码")*   .defaultSuccessUrl() : 指定用户登录成功后的默认跳转地址*   .failureUrl() : 指定用户登录失败后的"跳转地址",默认跳转到 /login?error*     ---error时一个错误标识,作用是在登录失败后在登录页面进行接收判断,例如login.html示例中的${param.error},这两者必须保持一直。*///自定义"用户登录控制"http.formLogin().loginPage("/userLogin").permitAll() //自定义登录页面的"跳转路径".usernameParameter("name").passwordParameter("pwd")  //设置登录用户的"用户名参数" 和 "密码参数".defaultSuccessUrl("/") //用户登录成功后默认跳转到"项目首页".failureUrl("/userLogin?error"); //用户登录失败后默认跳转到"项目首页"/*** 自定义"用户退出控制"*/http.logout().logoutUrl("/mylogout") //指定用户退出的请求路径 (前端页面进行"用户退出时,要请求该路径,且方法为post").logoutSuccessUrl("/"); //退出成功后的重定向地址 (退出成功后跳转的地址)//用户退出后,用户会话信息是默认清除的,次情况下无需手动配置}}
    
基于 “简单加密Token方式” 实现 "记住我"功能
⑪ 前端页面中添加 “记住我” 框
  • 下面将结合前面介绍rememberMe()相关方法实现这种简单记住我功能。在已有的 login.html 文件新增一个 “记住我” 功能勾选框示例代码如下 :

       <!--  实现记住我功能  --><div><label><!--Security提供的记住我功能的name属性默认值为: remember-me , 下面的name属性值为 : rememberme ,因此后端的 		   	  rememberMeParameter()方法中也要进行对应的修改-->         <input th:type="checkbox" name="rememberme">记住我</label></div>
    

    login.html :

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf"模板引擎"页面配置 -->
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>用户登录界面</title><link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet"><link th:href="@{/login/css/signin.css}" rel="stylesheet">
    </head>
    <body class="text-center">
    <!--  form表单请求跳转路径为 : /userLogin  -->
    <form class="form-signin" th:action="@{/userLogin}" th:method="post" ><!--  img标签--><img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px"><h1 class="h3 mb-3 font-weight-normal">请登录</h1><!-- 用户登录错误信息提示框 --><!--  th:if 根据条件的真假决定是否渲染 HTML 元素 ,真则渲染,假则不渲染 (有该参数值则渲染,没有该参数值则不渲染)  --><!--  有该参数值则渲染该div,否则就不渲染该div   --><div th:if="${param.error}"style="color: red;height: 40px;text-align: left;font-size: 1.1em"><!--  img标签  --><img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!</div><!--  用户名参数名为 : name  , 密码的参数名为: pws  --><input type="text" name="name" class="form-control"placeholder="用户名" required="" autofocus=""><input type="password" name="pwd" class="form-control"placeholder="密码" required=""><!--  实现记住我功能  --><div><label><!--Security提供的记住我功能的name属性默认值为: remember-me , 下面的name属性值为 : rememberme ,因此后端的 rememberMeParameter()方法中也要进行对应的修改--><input th:type="checkbox" name="rememberme">记住我</label></div><button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button><p class="mt-5 mb-3 text-muted">Copyright© 2024-2025</p>
    </form>
    </body>
    </html>
    
⑫ 定制 “记住我” 功能
  • 定制 “记住我” 功能 :

        /*** 定制记住我功能*/http.rememberMe()//指定"记住我"功能框中的name属性值,如果该框使用了默认的"remember-me",则该方法可以省略.rememberMeParameter("rememberme")//设置记住我功能中的token的有效期为200s.tokenValiditySeconds(200);
    }
    

    SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制 ( 权限管理 )*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户授权管理 (自定义用户访问控制 )http.authorizeRequests().antMatchers("/").permitAll()  //对 "/"请求 进行放行 (进入到项目首页)//对static文件夹下的静态资源进行统一放行 (如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件 ).antMatchers("/login/**").permitAll().antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)/*** 自定义"用户登录控制" :*   .loginPage() : 自定义登录页面的"跳转路径" , 跳转到自己定制的"登录页面"*   .permitAll() : 无条件对请求进行放行*   .usernameParameter("name").passwordParameter("pwd") : 设置登录用户的"用户名参数" 和 "密码参数" (接受登录时提交的"用户名"和"密码")*   .defaultSuccessUrl() : 指定用户登录成功后的默认跳转地址*   .failureUrl() : 指定用户登录失败后的"跳转地址",默认跳转到 /login?error*     ---error时一个错误标识,作用是在登录失败后在登录页面进行接收判断,例如login.html示例中的${param.error},这两者必须保持一直。*///自定义"用户登录控制"http.formLogin().loginPage("/userLogin").permitAll() //自定义登录页面的"跳转路径".usernameParameter("name").passwordParameter("pwd")  //设置登录用户的"用户名参数" 和 "密码参数".defaultSuccessUrl("/") //用户登录成功后默认跳转到"项目首页".failureUrl("/userLogin?error"); //用户登录失败后默认跳转到"项目首页"/*** 自定义"用户退出控制"*/http.logout().logoutUrl("/mylogout") //指定用户退出的请求路径 (前端页面进行"用户退出时,要请求该路径,且方法为post").logoutSuccessUrl("/"); //退出成功后的重定向地址 (退出成功后跳转的地址)//用户退出后,用户会话信息是默认清除的,次情况下无需手动配置/*** 定制记住我功能*/http.rememberMe()//指定"记住我"功能框中的name属性值,如果该框使用了默认的"remember-me",则该方法可以省略.rememberMeParameter("rememberme")//设置记住我功能中的token的有效期为200s.tokenValiditySeconds(200);}}
    

    初次登录时勾选了【记住我】选项后,在设置的 Token 有效期内 再次进行访问不需要重新登录认证如果 Token 失效后再次访问项目,则需要重新登录认证

2.2 基于持久化 Token 的方式 ( 这种方式 比 “第一种” 方式更安全 )

  • 持久化 Token方式简单加密Token方式在实现 Remember-Me (记住我) 功能大体相同 , 都是在 用户 选择【记住我】成功登录 后,将 生成的Token存入Cookie发送客户端浏览器 ,在下次 用户通过同一客户端访问系统时,系统直接客户端 Cookie读取 Token 进行认证

    ( 用户登录成功将生成的Token存入到 Cookie中,并将该Cookie发送给" 客户端浏览器" , 浏览器读取 Cookie 中的 Token信息 进行 认证,这 就实现了"记住我"功能 )

  • 基于 “简单加密 Token”方式基于持久化 Token方式区别 :

    1. 基于简单加密 Token 的方式,生成的Token 将在 客户端保存一段时间 , 如果 用户不退出登录,或者 不修改密码那么在 Cookie 失效之前任何人都可以无限制地使用该 Token 进行 自动登录 ;

    2. 基于持久化 Token方式采用 如下实现逻辑 :

    • (1) 用户 选择【记住我 功能 成功登录后Security 会把 username 随机产生的序列号 生成的Token 进行 持久化存储 ( 例如存储数据表中 ),同时 它们的组合生成一个 Cookie “发送给” 客户端浏览器
      ----( 将 记住我功能 相关的信息数据库存储一份同时 也将 其组合成 一个 CookieCookie 客户端浏览器发送一份 )
    • (2) 当用户 再次访问系统时,首先检查 客户端 携带的 Cookie信息,如果对应 Cookie信息包含username序列号Token 与数据库保存的一致,则 “验证通过” 并 “自动登录”,同时 系统重新生成一个新Token 替换数据库旧的 Token,同时也会组合一个 新的Cookie 并将其 发给 “客户端”
      ----( 当 用户再次访问系统时,将 客户端 中的 Cookie信息 和 与 数据库 中的Token信息进行 比对如果一致,则代表 验证通过并“ 自动登录” ,同时系统将生成一个新的Token替代数据库旧的Token ,同时还会组合一个 新的Cookie将其发给 “客户端” )
    • (3) 如果 Cookie 中的 Token 不匹配 ( 即如果 客户端中的 Token信息数据库中Token信息不一致 ),则很有可能用户的 Cookie 被盗用。由于盗用者使用初次生成Token 进行登录生成一个新的 Token,所以用户不知情再次登录就会出现 Token 不匹配的情况,这时就需要重新登录,并生成新的TokenCookie。同时 Spring Securiy 就可以发现 Cookie 可能被盗用的情况,它将 删除数据库 中与 当前用户相关所有Token 记录,这样 盗用者 使用 原有的Cookie不能再次登录
    • (4) 如果 用户"访问系统" 没有携带Cookie,或者包含的 username序列号数据库保存的不一致,那么将 引导用户登录页面

    以上实现逻辑可以看出持久化Token 的方式比 简单加密Token 的方式 相对更加安全。使用 简单加密 Token 的方式,一旦用户的 Cookie 被盗用,在 Token 有效期内盗用者可以无限制地自动登录进行恶意操作直到用户本人发现修改密码才会避免这种问题 ;
    而使用 持久化 Token 的方式 相对安全,用户 每登录一次都会生成新的TokenCookie,但也给盗用者留下了在用户进行第2次登录前进行恶意操作的机会,只有在用户进行第2次登录更新Token 和 Cookie 时才会避免这种问题。因此,总体来讲,对于 安全性要求很高应用不推荐使用Bemember-Me 功能

基础项目文件准备
  • 创建项目 :

    在这里插入图片描述

  • 项目结构 :

    在这里插入图片描述

  • pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.itheima</groupId><artifactId>chapter07</artifactId><version>0.0.1-SNAPSHOT</version><name>chapter07</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><!-- Security与Thymeleaf整合实现前端页面安全访问控制 --><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency><!-- JDBC数据库连接启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- MySQL数据连接驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Redis缓存启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Spring Data JPA操作数据库  --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- 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-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><!--    <build>--><!--        <plugins>--><!--            <plugin>--><!--                <groupId>org.springframework.boot</groupId>--><!--                <artifactId>spring-boot-maven-plugin</artifactId>--><!--            </plugin>--><!--        </plugins>--><!--    </build>--></project>
    
    • 导入 Sql文件 ( 创建数据库表 ) :
      security.sql

    • 创建实体类 :

      Customer.java :

      import javax.persistence.*;@Entity(name = "t_customer")public class Customer {@Id@GeneratedValue(strategy = GenerationType.IDENTITY) //主键自增private Integer id;private String username;private String password;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {return "Customer{" +"id=" + id +", username='" + username + '\'' +", password=" + password +'}';}
      }
      

    Authority.java

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;@Entity(name = "t_authority ")
    public class Authority {@Id@GeneratedValue(strategy = GenerationType.IDENTITY) //主键自增private Integer id;private String authority ;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getAuthority() {return authority;}public void setAuthority(String authority) {this.authority = authority;}@Overridepublic String toString() {return "Authority{" +"id=" + id +", authority='" + authority + '\'' +'}';}
    }
    
  • 创建Repository接口文件 : ( 通过该接口的方法操作数据 ) :

    CustomerRepository.java :

    import org.springframework.data.jpa.repository.JpaRepository;public interface CustomerRepository extends JpaRepository<Customer,Integer> {//根据username查询Customer对象信息Customer findByUsername(String username);
    }
    

    AuthorityRepository.java ( 接口文件 ) :

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;import java.util.List;public interface AuthorityRepository extends JpaRepository<Authority,Integer> {//根据 username 来查询"权限信息"@Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)public List<Authority> findAuthoritiesByUsername(String username);}
    
  • 自定义序列化机制 :

    RedisConfig.java :

    package com.itheima.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@Configuration
    public class RedisConfig { //在该配置类中 自定义存储到Redis数据库的数据的 "序列化机制"/***  定制Redis API模板RedisTemplate* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate();template.setConnectionFactory(redisConnectionFactory);// 使用JSON格式序列化对象,对缓存数据key和value进行转换Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);// 解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和publicom.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jacksonSeial.setObjectMapper(om);// 设置RedisTemplate模板API的序列化方式为JSONtemplate.setDefaultSerializer(jacksonSeial);return template;}
    }
    
  • application.properties :

    spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=rootspring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=123456spring.thymeleaf.cache=false
    
  • 创建 html资源文件 :
    index.html 页面是 项目首页页面commonvip文件夹中分别对应普通用户VIP用户可访问的页面

    index.html

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf模板引擎页面配置 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="UTF-8"><title>影视直播厅</title>
    </head>
    <body><!-- index.html页面是项目首页页面,common和vip文件夹中分别对应的普通用户 和 VIP用户可访问的页面 -->
    <h1 align="center">欢迎进入电影网站首页</h1>
    <hr>
    <h3>普通电影</h3>
    <ul><li><a th:href="@{/detail/common/1}">飞驰人生</a></li><li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
    </ul>
    <h3>VIP专享</h3>
    <ul><li><a th:href="@{/detail/vip/1}">速度与激情</a></li><li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
    </ul>
    </body>
    </html>
    

    1.html : ( 其他三个页面 以此类推 )

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="UTF-8"><title>Title</title>
    </head>
    <body>
    <!-- th:href="@{/}" : 返回项目首页-->
    <a th:href="@{/}">返回</a>
    <h1>飞驰人生</h1>
    .....
    </body>
    </html>
    
实现 “自定义身份认证” ( UserDetailsService身份认证 )
① service层中类 获取 “用户基本信息” 和 “用户权限信息”
  • service层 类中来从 Redis获取 “缓存数据”,如没找到缓存数据,则从Mysql数据库查询数据 : ( 获取 ① “用户基本信息”② “用户权限信息” )

    CustomerService.java :

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;import java.util.List;/*** 该Service层类中实现的代码效果为:* 在Reids数据库中查找是否有指定"缓存数据",有则从其中获取,没有则在Mysql数据库中查找,同时还将查找到的数据也在Redis中进行"缓存"*/
    @Service //加入到IOC容器中
    public class CustomerService {@Autowiredprivate CustomerRepository customerRepository;@Autowiredprivate AuthorityRepository authorityRepository;@Autowiredprivate RedisTemplate redisTemplate;  //通过 Redis API 的方式来进行 "Redis缓存"/*** 业务控制 : 使用唯一用户名查询用户信息*/public Customer getCustomer(String username){Customer customer=null;//从Redis数据库中获取"缓存数据"Object o = redisTemplate.opsForValue().get("customer_"+username);//判断是否有该缓存数据if(o!=null){customer=(Customer)o;}else { //不存在该缓存数据,则从数据库中查询缓存数据customer = customerRepository.findByUsername(username); //根据username来在数据库中查询数据if(customer!=null){redisTemplate.opsForValue().set("customer_"+username,customer);}}return customer;}/*** 业务控制 : 使用"唯一用户名"查询用户权限*/public List<Authority> getCustomerAuthority(String username){List<Authority> authorities=null;//尝试从Redis数据库中获得缓存数据Object o = redisTemplate.opsForValue().get("authorities_"+username);if(o!=null){authorities=(List<Authority>)o;}else {//没找到缓存数据则从Mysql数据库中查询数据authorities=authorityRepository.findAuthoritiesByUsername(username);if(authorities.size()>0){redisTemplate.opsForValue().set("authorities_"+username,authorities);}}return authorities;}
    }
    
② “自定义类” 实现 “UserDetailsService接口” , 在该类中 封装 “用户身份认证信息”
  • 自定义一个 , 该 实现UserDetailsService接口 , 用该接口loadUserByUsername( )方法 来封装 "用户认证信息" , 该最终被用于 configure ( AuthenticationManagerBuilder auth ) 方法中,被最终用于 "UserDetailsService"身份认证

    UserDetailsServiceImpl.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.*;
    import org.springframework.stereotype.Service;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;/***   自定义一个类, 该类实现了UserDetailsService接口 , 用该接口的 loadUserByUsername()方法来封装"用户认证信息" ,*   该类最终被用于 configure(AuthenticationManagerBuilder auth)方法中,被最终用于 "UserDetailsService"身份认证。*/
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService { //实现 UserDetailsService接口@Autowiredprivate CustomerService customerService;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //辅助进行"用户身份认证"的方法//通过业务方法(业务层类)获取用户以及权限信息//根据username获得Customer对象信息Customer customer = customerService.getCustomer(s);List<Authority> authorities = customerService.getCustomerAuthority(s);/***  .stream() : 将"权限信息"集合 转换为一个流(Stream) , 以便进行后续的流式操作**  .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) :*     使用map操作 / map()函数 来转换流中的每个元素 (将流中的"每一个元素"转换为 "另一种形式")*     具体分析:*     authority -> 获得流中的每一个元素,将其转换为另一种形式*     authority.getAuthority() : 获得 authority 这个元素对象的"权限信息" ( 是一个"权限信息"的字符串 )*     new SimpleGrantedAuthority(authority.getAuthority())) : 使用这个 "权限信息字符串" 创建一个 SimpleGrantedAuthority对象,*     该对象 是 Spring Security框架中用于表示 "授权信息" 的类**   .collect(Collectors.toList()); : 是一个终端操作,它告诉流如何收集其元素以生成一个结果。*     具体分析:*     .collect(Collectors.toList()) : 收集转换后的 SimpleGrantedAuthority对象,并将其放入一个新的列表中*/// 对"用户权限信息"进行封装List<SimpleGrantedAuthority> list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());/*创建 UserDetails (用户详情) 对象,并将该对象进行返回*/if(customer!=null){//用 username 、password 、权限信息集合 作为参数创建 UserDetails对象 ( 用户详情对象 )UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);return userDetails;} else {//如果查询的用户不存在 (用户名不存在 ) , 必须抛出异常throw new UsernameNotFoundException("当前用户不存在!");}}
    }
    
③ “SecurityConfig配置类” 中 实现 “自定义 身份认证”
  • SecurityConfig配置类” 中 实现 “自定义身份认证 :

    SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}
    }    
    
实现 “自定义用户访问”
④ “SecurityConfig配置类” 中 实现 “自定义 用户访问控制”
  • SecurityConfig配置类” 中 实现 自定义 “用户访问控制” :

    SecurityConfig.java

    package com.itheima.config;import com.itheima.service.Impl.UserDetailsServiceImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户访问控制 http.authorizeRequests().antMatchers("/").permitAll()  //对/请求进行放行.antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)}}
    
⑤ controller层中 实现 “路径访问跳转”
  • controller层中 实现 "路径访问跳转" :

    LoginController.java

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.ResponseBody;@Controller //加入IOC容器中
    public class LoginController {  //关于跳转到登录页面有关的controller类/*跳转到Detail文件夹下的"视图页面"*/@GetMapping("/detail/{type}/{path}") //其中的变量为"路径变量"// @PathVariable注解获得"路径变量"的值public String toDetail( @PathVariable("type") String type ,@PathVariable("path") String path ) {//返回值为String,可用于返回一个视图页面return "/detail/"+type+"/"+path;}}
    
实现 “自定义用户登录”
⑥ 自定义 用户登录 “页面”
  • 要实现 自定义用户登录功能,首先必须根据需要自定义一个用户登录页面。在项目的 resources/templates 目录下新创建一个名为 login文件夹( 专门处理用户登录 ),在该文件夹中创建一个 用户登录页面 : login.html

    login.html

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf"模板引擎"页面配置 -->
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>用户登录界面</title><link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet"><link th:href="@{/login/css/signin.css}" rel="stylesheet">
    </head>
    <body class="text-center">
    <!--  form表单请求跳转路径为 : /userLogin  -->
    <form class="form-signin" th:action="@{/userLogin}" th:method="post" ><!--  img标签--><img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px"><h1 class="h3 mb-3 font-weight-normal">请登录</h1><!-- 用户登录错误信息提示框 --><!--  th:if 根据条件的真假决定是否渲染 HTML 元素 ,真则渲染,假则不渲染 (有该参数值则渲染,没有该参数值则不渲染)  --><!--  有该参数值则渲染该div,否则就不渲染该div   --><div th:if="${param.error}"style="color: red;height: 40px;text-align: left;font-size: 1.1em"><!--  img标签  --><img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!</div><!--  用户名参数名为 : name  , 密码的参数名为: pws  --><input type="text" name="name" class="form-control"placeholder="用户名" required="" autofocus=""><input type="password" name="pwd" class="form-control"placeholder="密码" required=""><button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button><p class="mt-5 mb-3 text-muted">Copyright© 2024-2025</p>
    </form>
    </body>
    </html>
    

    上面代码中引入了两个 CSS 样式文件和两个 img图片文件用来渲染用户登录页面,它们都存在于 /ogin/** 目录下,需要提前引入这些静态资源文件
    目录中。引入这些静态资源文件后结构如下图所示 :

    获得相应的css文件 和 图片文件

    在这里插入图片描述

⑦ 自定义 用户登录 “跳转”
  • 自定义 “用户登录跳转” :

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.ResponseBody;@Controller //加入IOC容器中
    public class LoginController {  //关于跳转到登录页面有关的controller类/*** 跳转到自定义的 "登录页面"*/@GetMapping("/userLogin")public String toLoginPage() {return "/login/login";}
    }
    

    Spring Security 默认采用 Get 方式“/ogin”请求 用于向 “登录页面” 跳转 ( 可自定义跳转"登录页面"的路径,来指定”登录页面“ ),使用 Post 方式的 “/ogin”请求 用于 对登录后的数据处理

⑧ 自定义 用户登录 “控制”
  • 完成上面的准备工作后,打开 SecurityConfig 类重写 configure( HttpSecurity http )方法,实现 用户登录控制
    示例代码如下 :

    SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制 ( 权限管理 )*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户授权管理 (自定义用户访问控制 )http.authorizeRequests() .antMatchers("/").permitAll()  //对 "/"请求 进行放行 (进入到项目首页)//对static文件夹下的静态资源进行统一放行 (如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件 ).antMatchers("/login/**").permitAll().antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)/*** 自定义"用户登录控制" :*   .loginPage() : 自定义登录页面的"跳转路径" , 跳转到自己定制的"登录页面"*   .permitAll() : 无条件对请求进行放行*   .usernameParameter("name").passwordParameter("pwd") : 设置登录用户的"用户名参数" 和 "密码参数" (接受登录时提交的"用户名"和"密码")*   .defaultSuccessUrl() : 指定用户登录成功后的默认跳转地址*   .failureUrl() : 指定用户登录失败后的"跳转地址",默认跳转到 /login?error*     ---error时一个错误标识,作用是在登录失败后在登录页面进行接收判断,例如login.html示例中的${param.error},这两者必须保持一直。*///自定义"用户登录控制"http.formLogin().loginPage("/userLogin").permitAll() //自定义登录页面的"跳转路径".usernameParameter("name").passwordParameter("pwd")  //设置登录用户的"用户名参数" 和 "密码参数".defaultSuccessUrl("/") //用户登录成功后默认跳转到"项目首页".failureUrl("/userLogin?error"); //用户登录失败后默认跳转到"项目首页"}
    }    
    
实现 “自定义用户退出”
⑨ 添加自定义 “用户退出链接”
  • 要实现 自定义用户退出功能,必须某个页面定义用户退出链接或者 按钮。为了简化操作我们在之前创建的项目首页 : index.html 上方新增一个用户退出链接修改后示范文件代码如下所示 :

    <!-- th:action : 用于指定“表单提交”时 “应发送的URL”-->
    <!-- 跳转到 /mylogout路径来进行 "用户退出" , 因为不是使用默认的 /logout路径来请求 "用户退出"  所以 后端的 .logoutUrl()方法中内容也修改为 /mylogout -->
    <!-- 同时 /mylogout 请求的方法为post类型的-->
    <form th:action="/mylogout" method="post"><input th:type="submit" th:value="注销">
    </form>
    

    index.html

    <!DOCTYPE html>
    <!-- 配置开启thymeleaf模板引擎页面配置 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head><meta charset="UTF-8"><title>影视直播厅</title>
    </head>
    <body><!-- index.html页面是项目首页页面,common和vip文件夹中分别对应的普通用户 和 VIP用户可访问的页面 -->
    <h1 align="center">欢迎进入电影网站首页</h1>
    <!-- th:action : 用于指定“表单提交”时 “应发送的URL”-->
    <!-- 跳转到 /mylogout路径来进行 "用户退出" , 因为不是使用默认的 /logout路径来请求 "用户退出"  所以 后端的 .logoutUrl()方法中内容也修改为 /mylogout -->
    <!-- 同时 /mylogout 请求的方法为post类型的-->
    <form th:action="/mylogout" method="post"><input th:type="submit" th:value="注销">
    </form>
    <hr>
    <h3>普通电影</h3>
    <ul><li><a th:href="@{/detail/common/1}">飞驰人生</a></li><li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
    </ul>
    <h3>VIP专享</h3>
    <ul><li><a th:href="@{/detail/vip/1}">速度与激情</a></li><li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
    </ul>
    </body>
    </html>
    

    上面的代码中新增了一个 <form>标签进行注销控制 ( 进行 “用户退出控制”),且定义的退出表单 aciton 为“/mylogout ( 默认为“/logout” ),方法post


    需要说明的是,Spring Boot 项目中引入 Spring Security 框架后会 自动开启 : CSRF 防护功能 ( 跨站请求伪造防护),用户退出必须使用 POST ; 如果关闭CSRF 防护功能,那么 可以使用任意方式HTTP 请求进行用户注销

⑩ 添加自定义 “用户退出控制”
  • SecurityConfig.java

    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.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制 ( 权限管理 )*/
    @Override
    protected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户授权管理 (自定义用户访问控制 )http.authorizeRequests().antMatchers("/").permitAll()  //对 "/"请求 进行放行 (进入到项目首页)//对static文件夹下的静态资源进行统一放行 (如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件 ).antMatchers("/login/**").permitAll().antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)/*** 自定义"用户登录控制" :*   .loginPage() : 自定义登录页面的"跳转路径" , 跳转到自己定制的"登录页面"*   .permitAll() : 无条件对请求进行放行*   .usernameParameter("name").passwordParameter("pwd") : 设置登录用户的"用户名参数" 和 "密码参数" (接受登录时提交的"用户名"和"密码")*   .defaultSuccessUrl() : 指定用户登录成功后的默认跳转地址*   .failureUrl() : 指定用户登录失败后的"跳转地址",默认跳转到 /login?error*     ---error时一个错误标识,作用是在登录失败后在登录页面进行接收判断,例如login.html示例中的${param.error},这两者必须保持一直。*///自定义"用户登录控制"http.formLogin().loginPage("/userLogin").permitAll() //自定义登录页面的"跳转路径".usernameParameter("name").passwordParameter("pwd")  //设置登录用户的"用户名参数" 和 "密码参数".defaultSuccessUrl("/") //用户登录成功后默认跳转到"项目首页".failureUrl("/userLogin?error"); //用户登录失败后默认跳转到"项目首页"/*** 自定义"用户退出控制"*/http.logout().logoutUrl("/mylogout") //指定用户退出的请求路径 (前端页面进行"用户退出时,要请求该路径,且方法为post").logoutSuccessUrl("/"); //退出成功后的重定向地址 (退出成功后跳转的地址)//用户退出后,用户会话信息是默认清除的,次情况下无需手动配置}}
    
基于 “持久化 Token方式” 实现 "记住我"功能
⑪ 创建 “存储Token信息” 的 persistent_logins “数据库表”
  • 数据库中创建一个存储 Cookie 信息的持续登录用户表 : persistent_logins ( 在之前springbootdata数据库中创建 )

    persistent_logins.sql

    在这里插入图片描述

    上述建表语句中创建了一个名为 persistent_logins数据表,其中 username 存储 用户名 series 存储 随机生成的序列号 token 存信 每次访问更新的 Token last_used 表示 最近登录日期

    需要说明的是,在 默认情况基于持久化 Token 的方式会使用上述 官方提供用户表 : persistent_logins 进行持久化 Token 的管理

⑫ 定制 “记住我” 功能 ( ①注入DataSource数据库信息 + ②调用rememberMe( )方法 + 配置JdbcTokenRepositoryImpl对象并将其加入到IOC容器中 )
  • 定制 “记住我” 功能 :

    SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    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.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;import javax.sql.DataSource;@EnableWebSecurity  // 开启MVC security安全支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter { //在该配置类中 配置①自定义用户认证(UserDetailsService) 和 ②用户授权管理自定义配置(自定义用户访问控制)/*该类为 配置好了 "UserDetailsService"身份认证信息 的类 ,使用该类来在 configure(AuthenticationManagerBuilder auth)方法中进行 "UserDetailsService"身份认证。*/@Autowiredprivate UserDetailsServiceImpl userDetailsService;/*** 用户身份认证自定义配置 ( 通过UserDetailsService的方式实现 )** 重写configure(AuthenticationManagerBuilder auth)方法 : 自定义用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密码需要设置编码器 ( 添加"密码编辑器")BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//使用UserDetailService进行"身份认证"auth.userDetailsService(userDetailsService)//设置"密码编辑器".passwordEncoder(encoder);}//注入数据库信息@Autowiredprivate DataSource dataSource;/*** 用户授权管理自定义配置 ( 自定义用户访问控制 )** 重写configure(HttpSecurity http)方法 : 自定义用户访问控制 ( 权限管理 )*/@Overrideprotected void configure(HttpSecurity http) throws Exception {/***  .antMatchers : 开启Ant风格的路径匹配*  .permitAll() : 无条件对请求进行放行*  .antMatchers("/").permitAll() : //对/请求进行放行**  .hasRole() : 匹配用户是否是"某一个角色"*  .hasAnyRole() : 匹配用户是否是"某一个角色" 或 是 "另一个角色" ( 匹配"用户"是否有参数中的"任意角色" )*  .antMatchers("detail/common/**").hasAnyRole("common","vip") :  表示 detail/common/** 请求只有用户角色common或用户vip才允许访问/才能访问*    ---( 即ROLE_common权限 和 (即ROLE_vip权限才能访问该路径 )*  .antMatchers("/detail/vip/**").hasRole("vip") :  表示 detail/vip/** 请求只有用户vip才允许访问/才能访问**  .anyRequest() :    匹配任何请求*  .authenticated() : 匹配已经登陆认证的用户*  .and() //功能连接符*  .formLogin() : 启用Spring Security的基于"HTML表单"的 用户登录功能/用户登录页面, 同时通过该页面来进行"登录验证"*    ---简而言之 : 使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)**/// 自定义用户授权管理 (自定义用户访问控制 )http.authorizeRequests().antMatchers("/").permitAll()  //对 "/"请求 进行放行 (进入到项目首页)//对static文件夹下的静态资源进行统一放行 (如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件 ).antMatchers("/login/**").permitAll().antMatchers("/detail/common/**").hasAnyRole("common","vip") //普通电影,则是common和vip用户都能看.antMatchers("/detail/vip/**").hasRole("vip")  //vip电影则是vip用户才能看.anyRequest()  //匹配任何请求.authenticated()  //匹配已经登陆认证的用户.and() //功能连接符.formLogin(); //使用security提供的"默认登录"页面进行"登录验证" (如果没有指定"自定义的登录页面"的话)/*** 自定义"用户登录控制" :*   .loginPage() : 自定义登录页面的"跳转路径" , 跳转到自己定制的"登录页面"*   .permitAll() : 无条件对请求进行放行*   .usernameParameter("name").passwordParameter("pwd") : 设置登录用户的"用户名参数" 和 "密码参数" (接受登录时提交的"用户名"和"密码")*   .defaultSuccessUrl() : 指定用户登录成功后的默认跳转地址*   .failureUrl() : 指定用户登录失败后的"跳转地址",默认跳转到 /login?error*     ---error时一个错误标识,作用是在登录失败后在登录页面进行接收判断,例如login.html示例中的${param.error},这两者必须保持一直。*///自定义"用户登录控制"http.formLogin().loginPage("/userLogin").permitAll() //自定义登录页面的"跳转路径".usernameParameter("name").passwordParameter("pwd")  //设置登录用户的"用户名参数" 和 "密码参数".defaultSuccessUrl("/") //用户登录成功后默认跳转到"项目首页".failureUrl("/userLogin?error"); //用户登录失败后默认跳转到"项目首页"/*** 自定义"用户退出控制"*/http.logout().logoutUrl("/mylogout") //指定用户退出的请求路径 (前端页面进行"用户退出时,要请求该路径,且方法为post").logoutSuccessUrl("/userLogin"); //退出成功后的重定向地址 (退出成功后跳转的地址)//用户退出后,用户会话信息是默认清除的,次情况下无需手动配置/***  定制记住我功能 (基于持久化Token的方式)*/http.rememberMe()//指定"记住我"功能框中的name属性值,如果该框使用了默认的"remember-me",则该方法可以省略.rememberMeParameter("rememberme")//设置记住我功能中的token的有效期为200s.tokenValiditySeconds(200)//对Cookie信息进行"持久化管理".tokenRepository(tokenRepository());}/*** 对Cookie信息进行"持久化管理" ( 通过该方法来将Cookie信息存储到数据库中 )*/
    @Bean
    public JdbcTokenRepositoryImpl tokenRepository() {JdbcTokenRepositoryImpl jr = new JdbcTokenRepositoryImpl();jr.setDataSource(dataSource);return jr;}} 
    

    ​ 上述代码中,与 基于简单加密Token 方式 相比,在持久化 Token 方式rememberMe( )示例中加入了 tokenRepository( tokenRepository( ) )方法Cookie 信息进行持久化管理。其中的tokenRepository( )参数会返回一个设置 dataSource数据源JdbcTokenRepositorylmpl实现类对象,该对象包含操作 Token各种方法

⑬ 效果测试
  • 重启项目项目启动成功后 ,通过浏览器访问项目登录页,在 登录界面输入正确用户名密码信息,同时勾选记住我功能后跳转到项目首页 : index.html。此时查看数据库persistent logins 表数据信息效果如下图所示 :

    在这里插入图片描述


    在这里插入图片描述

    从上图可以看出项目启动后用户使用记住我功能登录时,会在 持久化数据表 : persistent_logins生成对应用户 Cookie 信息,包括 用户名 序列号 Token 最近登录时间

  • 使用浏览器 重新访问项目首页直接查看影片详情 (打开与之前登录用户权限对应的影片) 会发现 无须重新登录可以直接访问。此时,再次查看数据库persistent_ogins 表数据信息效果如下图所示

    在这里插入图片描述

    将上面 两个有Token信息 进行对比可以看出 ,在 Token 有效期内 再次自动登录时,数据库表中的 token会更新其他数据不变如果 启用浏览器 Debug 模式还会发现,第 2次登录返回Cookie 值也会随之更新,这与之前分析持久化Token 方法实现逻辑一致 的。

  • 返回 浏览器首页 首页上方的 用户“注销”链接,在 Token 有效期内进行用户手动注销。此时,再次查看数据库persistent_logins 表数据信息,效果如下图所示 :

    在这里插入图片描述

    登录用户手动实现用户退出后,数据库中 persistent_logins 表持久化用户信息也会随之删除
    如果用户是在
    Token 有效期后自动退出的,那么数据库
    persistent_logins 表持久化用户信息 不会随之删除,当用户 再次进行访问登录 时,则是 在表中新增一条持久化用户信息

相关文章:

Spring Boot | Spring Boot 实现 “记住我“ 功能

目录: 一、SpringBoot 中 自定义 "用户授权管理" ( 总体内容介绍 )二、实现 "记住我" 功能 ( 通过 "HttpSecurity类" 的 rememberMe( )方法来实现 "记住我" 功能 ) :2.1 基于 "简单加密 Token" 的方式 ( 实现 "记住我&…...

AGM AG32 MCU替代GD32F407/STM32F407的手持示波器方案

一、AG32 MCU替代GD32F407/STM32F407的数字示波器方案 市场上有多款数字迷你示波器&#xff0c;特别小巧&#xff0c;携带非常方便。单通道&#xff0c;标注100Mhz带宽&#xff0c;500MS采样率。 这样高的采样率需要使用高速AD及很高性能的处理器。原方案使用AD9288双通道8bit …...

MongoDB基础入门到深入(八)MongoDB整合SpringBoot、Chang Streams

文章目录 系列文章索引十五、MongoDB整合SpringBoot1、环境准备2、集合操作3、文档操作&#xff08;1&#xff09;相关注解&#xff08;2&#xff09;创建实体&#xff08;3&#xff09;添加文档&#xff08;4&#xff09;查询文档&#xff08;5&#xff09;更新文档&#xff0…...

linux下的nfs概述与实验(openEuler22.03)

目录 什么是NFSNFS工作原理NFS常用文件/etc/exports文件/etc/exports格式 /var/lib/nfs/etab文件 NFS常用选项NFS配置方式1. 安装NFS和RPC2. 启动rpcbind和nfs3. 准备目录4. 配置/etc/exports并重启服务5. 连接测试服务端自我测试客户端测试 补充常用命令永久挂载注意 什么是NF…...

影响Oracle数据库打开速度的因素

当Oracle发起一个事务需要更改数据时&#xff0c;如果所涉及的数据块不在BUFFER CACHE中&#xff0c;那么Oracle服务进程首先会将相关数据块从数据文件中读进BUFFER CACHE进行更改&#xff08;直接路径读除外&#xff09;&#xff0c;更改后的数据块称之为脏块&#xff08;DIRT…...

dify:开源 LLMOps平台。

单纯笔记&#xff1a; 一、关于 Dify dify/README_CN.md at main langgenius/dify GitHub Dify 是一款开源的大语言模型&#xff08;LLM&#xff09;应用开发平台。它融合了后端即服务&#xff08;Backend as Service&#xff09;和 LLMOps 的理念&#xff0c;使开发者可以…...

CCF20220601——归一化处理

CCF20220601——归一化处理 代码如下&#xff1a; #include<bits/stdc.h> using namespace std; int main() {int n,a[1000],sum0;scanf("%d",&n);for(int i1;i<n;i){scanf("%d",&a[i]);suma[i];}double aver1.0,b0.0,d1.0;aversum/(n*1…...

用Ai编写一个电机驱动程序

问&#xff1a;帮我写一个步进电机的控制程序&#xff0c;要有包括加减速&#xff0c;以及电机步中断处理函数。 答&#xff1a;编写一个步进电机的控制程序涉及到硬件层面的操作&#xff0c;通常需要依赖特定的硬件平台和相应的驱动程序。以下是一个简化的示例&#xff0c;它展…...

【C++入门】—— C++入门 (下)_内联函数

前言&#xff1a;在了解完前面的C基础内容后&#xff0c;马上我们就要真正不如C的学习了&#xff0c;但在之前让我们最后了解最后一点点C入门知识&#xff01;来迟的520特别篇&#xff01; 本篇主要内容&#xff1a; 内联函数 auto关键字 范围for 指针空值nullptr C入门 1. 内联…...

Java数据结构与算法(最小栈)

前言 设计一个支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获取堆栈顶部的元素。i…...

7 Series FPGAs Integrated Block for PCI Express IP核 Advanced模式配置详解(三)

1 TL Settings Transaction Layer (TL)设置只在Advanced模式下有效。 Endpoint: Unlock and PME_Turn_Off Messages: 与端点的电源管理相关&#xff0c;允许发送解锁和电源管理事件关闭消息。 Root Port: Error Messages: Error Correctable&#xff08;错误可纠正&#xff09…...

k8s 部署mqtt简介

在Kubernetes&#xff08;K8s&#xff09;中部署MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;服务通常涉及以下几个步骤&#xff1a; 选择MQTT Broker MQTT Broker是MQTT消息传递的中间件。流行的MQTT Broker包括Mosquitto, HiveMQ, EMQ X等。你需要选择一…...

汇凯金业:量化交易中常用的数学模型有哪些

量化交易中运用了多种数学模型来识别市场的潜在机会和建立交易策略。以下是一些在量化交易中常用的数学模型&#xff1a; 1. 时间序列分析模型 时间序列分析是研究和预测数据点随时间顺序变化趋势的方法。在量化交易中&#xff0c;常用的时间序列模型包括&#xff1a; 自回归&a…...

局部直方图均衡化去雾算法

目录 1. 引言 2. 算法流程 3. 代码 4. 去雾效果 1. 引言 局部直方图算法是一种基于块的图像去雾方法&#xff0c;它将图像分割为若干个块&#xff0c;并在每个块内计算块的局部直方图。通过对各个块的直方图进行分析和处理&#xff0c;该算法能够更好地适应图像中不同区域的…...

selenium环境安装和web自动化基础

webUI自动化背景 因为web页面经常会变化&#xff0c;所以UI自动化测试的维护成本很高。不如接口的适用面广&#xff0c;所以大部分公司会做接口自动化测试&#xff0c;但是未必会做UI自动化测试&#xff1b; UI自动化测试要做也是覆盖冒烟测试&#xff0c;不会到很高的覆盖率&a…...

【UE Websocket】“WebSocket Server”插件使用记录

1. 在商城中下载“WebSocket Server”插件 该插件具有如下节点&#xff0c;基本可以满足WebSocket服务端的所有需求 2. 如果想创建一个基本的服务端&#xff0c;我们可以新建一个actor蓝图&#xff0c;添加如下节点 3. UE运行后&#xff0c;我们可以使用在线的websocket测试助手…...

spring中依赖注入(DI)是什么?

好的&#xff0c;让我以尽可能通俗易懂的方式来解释什么是依赖注入&#xff08;DI&#xff0c;Dependency Injection&#xff09;。 假设你正在制作一款游戏&#xff0c;游戏中有个角色需要使用武器。在没有依赖注入的情况下&#xff0c;这个角色可能需要自己创建一个武器。这…...

paligemma、Grounding-DINO-1.5简单无需标注无需训练直接可以使用的VLM图像到文本模型

1、paligemma 参考:https://github.com/google-research/big_vision/blob/main/big_vision/configs/proj/paligemma/README.md 模型架构: 文本与图像特征一起送入大模型 在线体验网址: https://huggingface.co/spaces/big-vision/paligemma 通过文字prompt既可与图片对话…...

FreeRTOS学习——FreeRTOS队列(下)之队列创建

本篇文章记录我学习FreeRTOS队列创建的知识。主要分享队列创建需要使用的初始化函数、队列复位函数。 需要进一步了解FreeRTOS队列的相关知识&#xff0c;读者可以参考以下文章&#xff1a; FreeRTOS学习——FreeRTOS队列&#xff08;上&#xff09;_freertos 单元素队列-CSDN博…...

js实现鼠标拖拽多选功能

实现功能 在PC端的H5页面中&#xff0c;客户拖动鼠标可以连选多个选项 效果展示 具体代码如下 <!DOCTYPE html> <html><head><title>鼠标拖拽多选功能</title><script src"https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js&quo…...

浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)

✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义&#xff08;Task Definition&…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘

美国西海岸的夏天&#xff0c;再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至&#xff0c;这不仅是开发者的盛宴&#xff0c;更是全球数亿苹果用户翘首以盼的科技春晚。今年&#xff0c;苹果依旧为我们带来了全家桶式的系统更新&#xff0c;包括 iOS 26、iPadOS 26…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

Java线上CPU飙高问题排查全指南

一、引言 在Java应用的线上运行环境中&#xff0c;CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时&#xff0c;通常会导致应用响应缓慢&#xff0c;甚至服务不可用&#xff0c;严重影响用户体验和业务运行。因此&#xff0c;掌握一套科学有效的CPU飙高问题排查方法&…...

云原生玩法三问:构建自定义开发环境

云原生玩法三问&#xff1a;构建自定义开发环境 引言 临时运维一个古董项目&#xff0c;无文档&#xff0c;无环境&#xff0c;无交接人&#xff0c;俗称三无。 运行设备的环境老&#xff0c;本地环境版本高&#xff0c;ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...

在Ubuntu24上采用Wine打开SourceInsight

1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...

JVM 内存结构 详解

内存结构 运行时数据区&#xff1a; Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器&#xff1a; ​ 线程私有&#xff0c;程序控制流的指示器&#xff0c;分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 ​ 每个线程都有一个程序计数…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...