目录:
- 一、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 针对 “记住我” 功能 提供了 两种实现 : 一种 是 “简单地” 使用加密来保证基于 Cookie 中 Token 的安全 ; 另一种 是通过 数据库 或 其他持久化机制来保存生成的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 页面是 项目首页页面,common和vip文件夹中分别对应的普通用户 和 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 “发送给” 客户端浏览器。
----( 将 记住我功能 相关的信息 在 数据库存储一份,同时 也将 其组合成 一个 Cookie,将 该 Cookie 向客户端浏览器发送一份 )- (2) 当用户 再次访问系统时,首先检查 客户端 携带的 Cookie信息,如果对应 Cookie信息 中 包含 的 username、序列号和 Token 与数据库 中保存的一致,则 “验证通过” 并 “自动登录”,同时 系统 将 重新生成一个新的 Token 替换数据库中旧的 Token,同时也会组合一个 新的Cookie 并将其 发给 “客户端”。
----( 当 用户再次访问系统时,将 客户端 中的 Cookie信息 和 与 数据库 中的Token信息进行 比对,如果一致,则代表 验证通过并“ 自动登录” ,同时系统将生成一个新的Token “替代” 数据库中 旧的Token ,同时还会组合一个 新的Cookie 并 将其发给 “客户端” )- (3) 如果 Cookie 中的 Token 不匹配 ( 即如果 客户端中的 Token信息与 数据库中的 Token信息不一致 ),则很有可能是用户的 Cookie 被盗用了。由于盗用者使用初次生成的 Token 进行登录时会 生成一个新的 Token,所以当 用户在 不知情时 再次登录就会出现 Token 不匹配的情况,这时就需要重新登录,并生成新的Token 和 Cookie。同时 Spring Securiy 就可以发现 Cookie 可能被盗用的情况,它将 删除数据库 中与 当前用户相关 的 所有Token 记录,这样 盗用者 使用 原有的Cookie 将 不能再次登录。
- (4) 如果 用户"访问系统" 时 没有携带Cookie,或者包含的 username 和 序列号 与 数据库中 保存的不一致,那么将 会 引导用户到 登录页面。
从以上实现逻辑可以看出,持久化Token 的方式比 简单加密Token 的方式 相对更加安全。使用 简单加密 Token 的方式,一旦用户的 Cookie 被盗用,在 Token 有效期内,盗用者可以无限制地自动登录进行恶意操作,直到用户本人发现并修改密码才会避免这种问题 ;
而使用 持久化 Token 的方式 相对安全,用户 每登录一次都会生成新的Token和 Cookie,但也给盗用者留下了在用户进行第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 页面是 项目首页页面,common和vip文件夹中分别对应的普通用户 和 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 表 的 持久化用户信息 不会随之删除,当用户 再次进行访问登录 时,则是 在表中新增一条持久化用户信息。