Spring Security 框架篇-深入了解 Spring Security 的认证功能流程和自定义实现登录接口(实现自定义认证过滤器、登出功能)

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 Spring Security 框架概述

        2.0 Spring Security 核心功能-认证功能

        2.1 过滤器链

        2.2 登录认证流程

        2.3 思路分析

        3.0 登录认证具体操作

        3.1 环境搭建

        3.2 实现 UserDetailService 接口

        3.2.1 密码问题

        3.3 自定义登录接口

        3.3.1 实现登入功能

        3.3.2 注意事项

        3.4 认证过滤器

        3.4.1 思路分析

        3.4.2 具体实现

        3.5 实现登出功能


        1.0 Spring Security 框架概述

        Spring Security 是一个强大且高度可定制的认证和访问控制框架,广泛用于 Java 和 Spring 应用程序中。它为应用程序提供了一层安全保护,帮助开发者管理用户身份验证、授权以及其他安全功能。

其核心功能:

        1)认证(Authentication):
认证是验证用户身份的过程。Spring Security 支持多种认证机制,如使用用户名和密码的表单登录、OAuth2、LDAP、JWT等。开发者可以通过实现 UserDetailsService 接口来自定义用户信息的加载方式。
        2)授权(Authorization):
授权是在用户通过认证后,根据该用户的权限来决定其能否访问特定资源或操作。Spring Security 提供了基于角色的访问控制(RBAC)和基于权限的访问控制(PBAC)两种方式。
        3)过滤器链(Filter Chain):
Spring Security 使用过滤器链来处理请求。每个请求通过一系列过滤器进行处理,从而实现认证、授权、CSRF 保护等功能。开发者可以自定义过滤器,以满足特定的安全需求。
会话管理(Session Management):
Spring Security 提供了会话管理功能,可以控制用户会话的创建、并发控制,以及会话过期等。
        4)密码加密:
框架提供了多种密码编码器,可以用来加密存储用户密码,确保密码的安全性。支持的编码器包括 BCrypt、SCrypt 和 PBKDF2 等。

        

        2.0 Spring Security 核心功能-认证功能

        在 SpringBoot 项目中使用 SpringSecurity 只需引入依赖,在访问项目资源的时候,就需要进行登录认证成功之后才能放行去访问资源。

        1)先引入 Security 框架的依赖:

		<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

        2)测试接口:

        引入依赖后,尝试去访问接口就会自动跳转到一个 Spring Security 的默认登录页面,默认用户名是 user,密码会输出在控制台。

        必须登录之后才能对接口进行访问。

密码:

在页面中登录:

登录之后:

        登录之后,就可以访问到后台数据了。

        因此,知道了 Spring Security 框架提供了认证、权限等核心功能,因此可以借助强大的 Spring Security 功能来实现一个自定义登录校验的功能,简单来说,就是在 Spring Security 框架中进行二次开发。

登录校验的流程图:

         接下来,围绕着登录校验这个功能进行实现。

        2.1 过滤器链

        Spring Security 的原理其实就是一个过滤链,内部包含了提供各种功能的过滤器。

        图中只展示了核心的过滤器,其他非核心的过滤器并没有在图中展示。

核心过滤器的功能介绍:

        1)UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名与密码后的登录请求。

        2)ExceptionTranslationFilter:处理过滤器中抛出的任何 AccessDeniedException 和 AuthenticationException 。简单来说,在认证过程中或者在权限校验过程中,抛出的异常都会被该过滤器进行捕获。

        3)FilterSecurityInterceptor:负责权限校验的过滤器。

同时我们也可以通过Debug查看当前系统中 SpringSecurity 过滤器链中有哪些过滤器及它们的顺序。

        2.2 登录认证流程

        Spring Security 框架登录认证过程中所调用的接口:

        根据需求来重写接口,从而来自定义实现登录认证功能。 

Spring Security 在认证过程中主要是以下接口发挥了重要作用:

        1)Authentication 接口:

        该实现类,表示当前访问系统的用户,封装了用户相关信息。

        2)AuthenticationMapper 接口:定义了认证 Authentication 的方法。

        3)UserDetailsService 接口:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。

        4)UserDetails 接口:提供核心用户信息。通过 UserDetailsService 根据用户名获取处理用户信息封装成 UserDetails 对象返回,然后将这些信息封装到 Authentication 对象中。

        2.3 思路分析

        现在知道了 Spring Security 框架中的相关接口,通过重写或者配置相关的接口,就可以实现自定义登录认证的功能。

大概的实现思路:

        通过实现 UserDetailsService 接口,重写相关的方法,根据用户名 userName 来查询数据库中的信息,然后封装成一个 UserDetails 对象返回。

        通过配置 AuthenticationMapper 来获取到该实现类,通过登录接口来使用该实现类,再通过 AuthenticationMapper 的实现类调用相关的方法进行往后执行,在数据库中拿到数据之后,再返回,判断是否拿到相关的数据,再来创建 jwt 返回给前端,将用户信息存放到 redis 中。

        以上是大体的思路,下面进行详细讲述。

        3.0 登录认证具体操作

        3.1 环境搭建

1)数据库的搭建:

CREATE DATABASE IF NOT EXISTS db_security;-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',`phonenumber` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',`user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',`update_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',`del_flag` int(0) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'xbaozi', '陈宝子', '$2a$10$WCD7xp6lxrS.PvGmL86nhuFHMKJTc58Sh0dG1EQw0zSHjlLFyFvde', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);

2)相关依赖的引入:

<?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 https://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.5.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>SpringSecurity</artifactId><version>0.0.1-SNAPSHOT</version><name>SpringSecurity</name><description>SpringSecurity</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.0.33</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter-test</artifactId><version>3.0.3</version><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>

        使用 springboot  3.XX.XX无法导入 WebSecurityConfigurerAdapter 类( WebSecurityConfigurerAdapter 被当前版本的 spring 弃用了),将版本改查 2.X.X 就可以解决了(无需添加 spring-security-config,会引发依赖冲突,因为 starter-security 的子依赖包含 security-config )

        至于当前最新的 Spring Security 框架配置的使用,在之后的博客会讲解,先用最基础、最普遍的配置来了解 Spring Security 框架的使用。

3)yml 配置文件:

        配置数据库的基本连接信息。

# 指定端口号
server:port: 8080# 配置数据源
spring:application:name: security# 数据库连接池配置datasource:url: jdbc:mysql://127.0.0.1:3306/db04?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver# Redis配置redis:# 这是我的虚拟机IPhost: localhostport: 6379password: 123456# 操作0号数据库,默认有16个数据库database: 0jedis:pool:max-active: 8 # 最大连接数max-wait: 1ms # 连接池最大阻塞等待时间max-idle: 4   # 连接池中的最大空闲连接min-idle: 0   # 连接池中的最小空闲连接cache:redis:time-to-live: 1800000 # 设置数据过期时间为半小时(ms)mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰自动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句

4)实体类:

        因为数据库只有一个表,因此我们只需要与之对应上就可以了。

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_user")
@ToString
public class User implements Serializable {private static final long serialVersionUID = 1L;/** 主键 */@TableIdprivate Long id;/** 用户名 */private String userName;/** 昵称 */private String nickName;/** 密码 */private String password;/** 账号状态(0正常 1停用) */private String status;/** 邮箱 */private String email;/** 手机号 */private String phonenumber;/** 用户性别(0男,1女,2未知) */private String sex;/** 头像 */private String avatar;/** 用户类型(0管理员,1普通用户) */private String userType;/** 创建人的用户id */private Long createBy;/*** 创建时间*/private Date createTime;/** * 更新人 */private Long updateBy;/** * 更新时间 */private Date updateTime;/** 删除标志(0代表未删除,1代表已删除) */private Integer delFlag;
}

5)Redis 配置类:

        主要是对 Redis 默认的序列化器进行一个更换。

@Configuration
public class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}
}

6)Jwt 工具类:

public class JwtUtil {// 设置有效期为60 * 60 *1000  一个小时public static final Long JWT_TTL = 60 * 60 * 1000L;//设置秘钥明文public static final String JWT_KEY = "xbaozi";public static String getUUID() {String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw* @param subject token中要存放的数据(json格式)*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jwt* @param subject   token中要存放的数据(json格式)* @param ttlMillis token超时时间*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if (ttlMillis == null) {ttlMillis = JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid)            //唯一的ID.setSubject(subject)    // 主题  可以是JSON数据.setIssuer("sg")        // 签发者.setIssuedAt(now)       // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}/*** 生成加密后的秘钥 secretKey*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}
}

7)Redis 工具类:

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等* @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等* @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间* @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间* @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。* @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象* @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象* @param collection 多个对象*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 缓存List数据* @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象* @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set* @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set* @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map* @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map* @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据* @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据* @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据* @param key* @param hkey*/public void delCacheMapValue(final String key, final String hkey){HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 获取多个Hash中的数据* @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表* @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}

8)astjson 对 Redis 工具类的配置。

        这里有一个问题就是不要使用高版本的 fastjson 依赖,因为高版本的好像是已经将 ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 去除了,从而后面导致报错。

public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class<?> clazz){return TypeFactory.defaultInstance().constructType(clazz);}
}

        3.2 实现 UserDetailService 接口

        通过实现 UserDetailService 接口,重写 loadUserByUsername 方法,从而实现根据 userName 查询数据库中的用户信息,将结果进行封装进行返回。

具体过程:

        先实现 UserDetailService 接口,再进行重写 loadUserByUsername() 方法。

@Service
public class MyUserDetailService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据username查询数据库User user = userMapper.selectByUserName(username);if (user == null){throw new RuntimeException("用户不存在");}//将查询到的结果user进行封装return new LoginUser(user);}
}

        从数据库中,根据 username 获取用户信息,返回的 user 进行判断,如果为 null,直接抛出异常;如果不为 null,将 user 进行封装成 LoginUser 类。

封装的实体类 LoginUser:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {/** 使用构造方法初始化 */private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}/** 下面的方法暂时全部都让他们返回true */@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

        LoginUser 实现了 UserDetail 接口,重写了七个方法,当前只需要关注 getPassword() 获取用户的密码, getUsername() 获取用户名字的两个方法。

        这就可以通过数据库中存在的用户进行登录认证了。

        但是如果要测试,并且如果想让用户的密码是明文存储,需要在密码前加 {noop},如密码为 1234,那在数据库中的数据就得为 {noop}1234,这与默认使用的 PasswordEncoder 有关。

测试一下:

对以上的流程进行详细解析:

        首先当前用户需要访问的路径为 "/hello",由于存在 Spring Security 框架,会对资源进行一种保护,也就是会进行验证,验证过程中先会来到 login 登录接口,再来到 ProvideManager 的 authentication() 方法进行认证,输入完用户名和密码之后,调用 DaoAuthenticationProvider 接口,再由该接口调用 UserDetailService 接口。

        我们通过实现 UserDetailService 接口,重写 loadUserByUsername() 方法,来自定义的从数据库中查询用户信息,查询到数据之后,判断是否为 null,如果为 null,直接抛出异常,因为在 Spring Security 框架中的过滤链中有相关的异常捕获的过滤链 ExceptionTranslationFilter。如果不为 null,需要进行封装,用一个实现 UserDetails 接口的类进行封装 user 信息。

        将 LoginUser 类进行返回给 DaoAuthenticationProvider 接口,由该接口来进行密码的校验,获取到 UserDetailsService 返回的信息后,通过 PasswordEncoder 进行密码校验。

        3.2.1 密码问题

        在 Spring Security 中默认使用的 PasswordEncoder 要求数据库中的密码格式为 {id}password,框架会根据前面的 id 去判断密码的加密模式,我们上面的 {noop}1234 也是属于这种格式,其中的 noop 就标明着这个密码是以明文的形式进行存储的,就会直接使用后面的 1234 当作密码。

        而一般我们会使用 Spring Security 为我们提供的 BCryptPasswordEncoder,其操作也很简单,我们只需要把 BCryptPasswordEncoder 对象注入 Spring 容器中,Spring Security 就会使用我们自定义的 PasswordEncoder 进行密码校验。

配置 PasswordEncoder:

        首先定义一个 SpringSecurity 的配置类。


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 将BCryptPasswordEncoder加入到容器中**/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}

        这样就可以拿到 passwordEncoder 的 Bea 对象了,该对象中主要有两个方法:

        1)passwordEncoder.encode("明文"):将明文进行加密。

        2)passwordEncoder.matches("明文","使用 encode 加密的密文"):对明文和加密后的密文进行匹配,如果匹配,返回 true;如果不匹配,返回 false。

        因此在 Spring Security 框架中,在登录认证校验密码的过程中,就会调用 passwordEncode.matches() 方法进行校验。

测试:

        现在将数据库的明文 “1234” 使用 encode 方法替换成密文存放到数据库中:

        访问 "/hello" 接口,先会自动跳转登录页面:

        输入用户名、密码之后:

        访问资源成功了。

        3.3 自定义登录接口

        通过自定义登录接口,实现密码验证成功之后,返回前端 JWT 令牌和将用户信息存放到 Redis 中的功能。

        3.3.1 实现登入功能

        首先定义登录接口,用户通过访问 "/login" 接口进行登录。

        LoginController 控制层在 login 方法里面调用 loginService.login() 方法。

        需要暴露 AuthenticationManager 登录认证管理器,由于旧版需要在配置类中,将其放入到 IOC 容器再进行获取,重写 AuthenticationManager 方法:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {private String[]  matchers = new String[]{"/userLogin"};/*** 将BCryptPasswordEncoder加入到容器中**/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

         在配置类中重写 authenticationManagerBean() 方法后,在该方法上加上 @Bean 注解,将其加入到 IOC 容器中,进行全部暴露。

LoginServiceIml 服务层:


@Service
public class LoginServiceIml implements LoginService {@Autowiredprivate RedisCache redisCache;@Autowired//获取到认证管理器private AuthenticationManager authenticationManager;@Overridepublic String login(User user) {//1. 封装用户名和密码为tokenUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());//2. 调用认证管理器进行认证,将token传进去Authentication authenticate = authenticationManager.authenticate(authenticationToken);//3. 判断是否验证成功if (Objects.isNull(authenticate)){throw new RuntimeException("用户名或密码错误");}//4. 认证成功,则获取到用户信息LoginUser principal = (LoginUser) authenticate.getPrincipal();//获取到用户信息User getUser = principal.getUser();//5. 生成JWT,将用户id作为载荷String jwt = JwtUtil.createJWT(getUser.getId().toString());//6. 将用户的信息存放到redis中,且设置时间为30分钟redisCache.setCacheObject("login:"+getUser.getId(),principal,30, TimeUnit.MINUTES);//7. 最后将jwt返回return jwt;}}

        在服务层将 authenticationManager 对象从 IOC 容器进行引入,调用 authenticationManager 认证管理器的 authenticate() 方法且将用户的登录名字与密码进行封装再放入该方法中。

        之后该方法就会进行下一步的调用,直到调用 UserDetailsService接口,从自定义中的 MyUserDetailService 接口从数据库中获取数据进行返回,直到回到 authenticate() 方法。

        判断返回值是否为空,如果不为空,则证明密码校验正确,接着就可以创建 Jwt 令牌和将用户信息放入到 Redis 中。

        在测试之前,还需要进行配置,当用户访问登录接口的时候,应该对自定义的 "/userLogin" 路径进行放行操作,而之前的 "/login" 路径 Spring Security 框架实现的,现在我们不需要用到该功能,而是自定义实现登录接口,可以更好的满足我们的需求。

        因此还需要对 SecurityConfig 进行配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {private String[]  matchers = new String[]{"/userLogin"};/*** 将BCryptPasswordEncoder加入到容器中**/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers(matchers).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}
}

        3.3.2 注意事项

        1)mybatis 的起步依赖版本不能太高,这里用 2.2.0 的版本。

        2)由于当前版本比较低 Spring Boot 的版本为 2.5.5 版本,为什么用那么低的版本呢?这是因为在高版本的 Spring Boot 对 Spring Security 框架中的 WebSecurityConfigurerAdapter 类弃用了,在高版本不能使用了。

        3)如果运行还是有问题,注意是否导入了相关的依赖,比如:

		<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.1</version></dependency>

        4)出现 403 问题:

        - 检查数据库存储的密码是否通过 PasswordEncoder 对象加密存储的,如果不是请调用  passwordEncoder.encode("1234");

        - 检查 SecurityConfig 配置类是否放行了登录链接  http.authorizeRequests().antMatchers("/userLogin").anonymous();

        - 请以 post 的请求方式并且请求头加上 Content-Type=application/json 的方式发送 JSON 格式 {"userName": "xbs", "password": "1234" };

        - 检查控制层接收参数的时候,是否使用 @RequsetBody 注解。

        - 检查发送请求的时候参数是否与 User 对象中的字段保持一致。

测试:

        1)发送请求:

        2)返回 Jwt 结果:

        3)Redis 存放的结果:

        3.4 认证过滤器

        定义一个认证过滤器,用来对 Jwt 令牌进行校验,判断是否通过正确的登录后,才去访问的后台资源。

        3.4.1 思路分析

主要流程:

        1)定义过滤器并将其加入 Spring 容器中,因为后面需要将其插入到过滤器链中。

        2)获取请求头中的 token 数据,判断请求头中是否携带 token 数据,若没有携带存在两种可能:

        - 用户需要登录,正在访问登录接口准备账号密码登录。

        - 用户未登录或登录过期,导致没有 token 或 token 过期。

        3)解析 token 数据,从中拿到 userId;

        4)从 Redis 中获取用户数据,若 Redis 中无数据则证明登录已过期,抛出异常提示;

        5)将用户数据存入 SecurityContextHolder 容器中,因为这里是认证,是假设已经登录之后的状态,所以参数列表分别为用户数据,null,鉴权信息;如果是前面的还未登录状态,参数列表则为账号和密码两个参数。

        6)将过滤器插入至过滤器链中。

        3.4.2 具体实现

代码如下:

@Configuration
public class MySecurityFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//先去尝试获取从请求头中获取token数据String jwt = request.getHeader("token");//判断token是否为nullif (StringUtils.isNullOrEmpty(jwt)){//这里有两种情况//第一种就是正在去登录过程//第二种就是token过期了//因此直接放行即可,在SpringSecurity后续的过滤器中会进行判断filterChain.doFilter(request,response);return;}//如果token不为null,则需要去解析tokenString userId;try {//解析tokenuserId = JwtUtil.parseJWT(jwt).getSubject();} catch (Exception e) {e.printStackTrace();//解析失败,直接返回错误信息throw new RuntimeException("token非法");}//根据用户id去Redis中查找缓存信息LoginUser loginUser = redisCache.getCacheObject("login:" + userId);if (Objects.isNull(loginUser)){throw new RuntimeException("用户登录已过期,请重新登录");}UsernamePasswordAuthenticationToken loginUserToken= new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(loginUserToken);//放行filterChain.doFilter(request,response);}
}

        为什么没有获取到 token 数据时还要放行的原因:

        在过滤器链中,可以看到主要作用的有三个过滤器,其中可以简单的理解为第一个用于登录,第二个过滤器用来捕获异常的,第三个则用来从 SecurityContextHolder 中获取数据来鉴权。

        因此,这里放行是为了可能后面的登录操作,不需要担心绕过认证,因为有第三个过滤器的存在。

        因为如果是已登录的状态会在上面自定义的过滤器中将用户信息(内包含鉴权信息)存放至 SecurityContextHolder 中去,如果在该鉴权过滤器中没能成功从中拿到数据,那就证明该用户这次操作并不是登录操作,而是真正需要拦截的操作,因此就会在 FilterSecurityInterceptor 过滤器中抛出异常。

配置 SecurityConfig 类:

        在完成认证过滤器之后,还需要进行配置让其过滤器操作起来。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {private String[]  matchers = new String[]{"/userLogin"};/*** 将BCryptPasswordEncoder加入到容器中**/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Autowiredprivate MySecurityFilter mySecurityFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers(matchers).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 参数列表分别为需要插入的过滤器和标识过滤器的字节码http.addFilterBefore(mySecurityFilter, UsernamePasswordAuthenticationFilter.class);}
}

        首先引入 mySecurityFilter 认证过滤器,通过 http.addFilterBefore() 的方法,添加到 UsernamePasswordAuthenticationFilter 用户名密码登录器之前。

        3.5 实现登出功能

        从 SecurityContextHolder 中获取认证信息。因为在访问退出接口的时候,肯定是已经登录了且是经过了自定义的过滤器,因此在 SecurityContextHolder 中是已经存放了该登录用户的基本数据信息,这样我们就是可以获取得到的。
        根据获取到的用户数据获取 useId 进行 key 的拼接,并从 Redis 中删除指定 key 的值,即删除该用户已登录的标识

代码如下:

    @Overridepublic String exit() {//首先从SecurityContextHolder中获取到用户信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//其实这里不需要进行判断是否为空,因为如果为空,则直接返回null,不会进入到后面//但是这里为了严谨,防止空指针异常if (Objects.isNull(authentication)){throw new RuntimeException("获取失败");}LoginUser principal = (LoginUser) authentication.getPrincipal();User user = principal.getUser();//拿到用户id,删除redis中的缓存redisCache.deleteObject("login:"+user.getId());return "退出成功";}

        希望我的博客可以帮你解决问题,对于 Spring Security 的授权功能的介绍,会放到下一篇博客。

        加油!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/3177.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript基础语法部分-黑马跟课笔记

一、Javascript介绍 1.JavaScript是什么&#xff1f; 1.是什么&#xff1f; 是一种运行在客户端&#xff08;浏览器&#xff09;的编程语言&#xff0c;实现人机交互效果 2.作用&#xff08;做什么&#xff1f;&#xff09; 网页特效&#xff08;监听用户的一些行为让网页做…

qt QDir详解

1、概述 QDir是Qt框架中的一个核心类&#xff0c;它提供了对文件系统目录的操作接口。Qt是一个跨平台的应用程序开发框架&#xff0c;广泛用于开发桌面、移动和嵌入式设备上的应用程序。QDir类使得开发者能够方便地在不同操作系统上处理目录和文件&#xff0c;如进行目录遍历、…

Jwt加解密

概述 记录jwt加解密的demo。 JSON Web Token (JWT) 是一种开放标准 (RFC 7519)&#xff0c;用于在网络应用环境间安全地传输信息。JWT 通常用于身份验证和信息交换&#xff0c;因为它可以被签名和加密&#xff0c;确保数据的完整性和隐私性。 JWT 的基本结构 JWT 由三部分组…

鸥柏(OBOO)户外触摸广告屏科技创新 高速服务区收费站案例

鸥柏&#xff0c;作为户外液晶显示技术的品牌高端领先者&#xff0c;其新产品鸥柏户外触摸屏在高速服务区收费站入口处得到了真实且广泛的应用。OBOO鸥柏户外广告机能够存储和展示海量信息&#xff0c;包括新闻、政策、天气预报、实时路况等&#xff0c;为过往司乘人员提供丰富…

ASED6015SH-ASEMI中低压MOS管ASED6015SH

ASED6015SH-ASEMI中低压MOS管ASED6015SH 型号&#xff1a;ASED6015SH 品牌&#xff1a;ASEMI 导通内阻&#xff1a;90mΩ 启动电压&#xff1a;2V-4V 最大漏源电流&#xff08;Id&#xff09;&#xff1a;19A 漏源击穿电压&#xff08;VRM&#xff09;&#xff1a;150V …

`掌握Python-PPTX,让PPt制作变得轻而易举!`

文章目录 掌握Python-PPTX&#xff0c;让PPT制作变得轻而易举&#xff01;背景介绍python-pptx 是什么&#xff1f;如何安装 python-pptx&#xff1f;简单库函数使用方法应用场景常见Bug及解决方案总结 掌握Python-PPTX&#xff0c;让PPT制作变得轻而易举&#xff01; 背景介绍…

linux的用户账号与权限管理

一、用户账号 root 和zhang 表示当前的登录用户test1 表示当前的主机名/home&#xff1a; 表示当前所在的目录为/home~&#xff1a; 表示当前所在的目录为~#&#xff1a; 表示当前用户是管理员$&#xff1a; 表示当前用户是普通用户 1.切换用户 su - 用户名 &#xff08;完全切…

Qt项目实战:银行利息(贷款)计算器

目录 一.ui设计 二.初始化表单 三. 存款计算 四.贷款计算 五.效果 六.代码 1.h 2.cpp 一.ui设计 二.初始化表单 获取当前时间&#xff0c;并将开始日期设置为当前日期&#xff0c;将结束日期设置为当前日期加一年 三. 存款计算 1.从文本框获取当前资金、利率、定期期…

无人机高山景区物资吊运技术及前景分析

随着科技的飞速发展&#xff0c;无人机技术已经逐渐渗透到各个领域&#xff0c;并在其中展现出巨大的潜力和应用前景。在高山景区物资运输方面&#xff0c;无人机技术的引入不仅解决了传统运输方式中人力成本高、效率低下的问题&#xff0c;还极大地提升了运输的安全性和灵活性…

就是这个样的粗爆,手搓一个计算器:数线计算器

作为程序员&#xff0c;没有合适的工具&#xff0c;就得手搓一个&#xff0c;PC端&#xff0c;移动端均可适用。废话不多说&#xff0c;直接上代码。 HTML: <div class"calculator"><div class"input-group"><label for"a">…

NSET or MSET算法--原理解析

1.背景 NSET/MSET是一种非线性的多元预测诊断技术&#xff0c;广泛应用于系统状态估计、故障诊断和预测等领域&#xff1b;相比于传统的线性模型和方法&#xff0c;NSET/MSET能够更好地处理非线性系统&#xff0c;并提供更准确的预测和诊断能力。在早期&#xff0c;MSET融合了…

NAS端最强音乐库,多平台服务支持。海康存储部署『Navidrome』

NAS端最强音乐库&#xff0c;多平台服务支持。海康存储部署『Navidrome』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 对于我们NAS用户&#xff0c;我们总是喜欢将自己喜欢的音乐资源通过下载的方式保存在本地&#xff0c;不过海康存储目前对因音乐的支持和管理实在过于薄弱&am…

Vue2+3

Day1 创建Vue实例 准备容器 引包 —— 开发版本 创建Vue实例 —— new Vue() 指定配置项 el 和 data > 渲染数据 el指定挂载点&#xff0c;指定控制的是哪个盒子 data提供数据 <!DOCTYPE html> <html lang"en"> <head><meta charset&qu…

AWTK-HarmonyOS NEXT 发布

AWTK 全称为 Toolkit AnyWhere&#xff0c;是 ZLG 倾心打造的一套基于 C 语言开发的 GUI 框架。旨在为用户提供一个功能强大、高效可靠、简单易用、可轻松做出炫酷效果的 GUI 引擎&#xff0c;支持跨平台同步开发&#xff0c;一次编程&#xff0c;到处编译&#xff0c;跨平台使…

新闻稿件管理:SpringBoot框架实战指南

3系统分析 3.1可行性分析 通过对本新闻稿件管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本新闻稿件管理系统采用SSM框架&#xff0c;JAVA作为开发语…

太炸裂了,Ollama跑本地模型已成为历史,现在都在使用这个工具,而且还能集成本地知识库

AI的发展速度真是超出我们的想象&#xff0c;遥想几个月前&#xff0c;我还在使用Ollama跑本地大模型&#xff0c;最近有另一款可以跑本地大模型的工具迅速崛起&#xff0c;在GitHub上已有70.3K Stars&#xff0c;相信不久就会超越Ollama&#xff0c;除了可以本地运行大模型之外…

在Vue和OpenLayers中使用移动传感器实现飞机航线飞行模拟

项目实现的核心代码 项目概述 该项目的目标是使用Vue.js作为前端框架&#xff0c;结合OpenLayers用于地图显示&#xff0c;实时获取来自手机传感器的数据&#xff08;如经纬度、高度、速度&#xff09;来模拟飞机在地图上的飞行轨迹。整体架构如下&#xff1a; Vue.js 用于构建…

Proteus中单片机IO口外接LED输出低电平时,引脚却一直保持高电平的问题(已解决)

文章目录 前言解决方法后记 前言 一个排阻接八个 LED&#xff0c;方便又省事&#xff0c;但出现了P1端口输出低电平后&#xff0c;仿真引脚却一直显示红色保持高电平不变&#xff0c;用电压表测量显示 2V 左右。 这是仿真的问题&#xff0c;在用开发板时是不会遇到的&#xff…

神经网络进行波士顿房价预测

前言 前一阵学校有五一数模节校赛&#xff0c;和朋友一起参加做B题&#xff0c;波士顿房价预测&#xff0c;算是第一次自己动手实现一个简单的小网络吧&#xff0c;虽然很简单&#xff0c;但还是想记录一下。 题目介绍 波士顿住房数据由哈里森和鲁宾菲尔德于1978年Harrison …

一分钟讲透聚合SDK的工作原理

聚合 SDK 广告是指通过整合多个广告 SDK&#xff08;软件开发工具包&#xff09;&#xff0c;将来自不同广告平台和渠道的广告资源集中管理和调配&#xff0c;并在应用或平台中展示和投放的一种广告模式。 使用聚合 SDK 可以让开发者或广告运营者更方便地接入多种广告源&#…