【Spring Boot 3】的安全防线:整合 【Spring Security 6】

简介

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

一般Web应用的需要进行认证和授权。

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权也是SpringSecurity作为安全框架的核心功能。

1.快速入门

1.1.引入依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>3.1.8</version>
</dependency>

如果是gradle则使用

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.1.8'

引入SpringSecurity依赖后,再次输入地址,都会统一调转到一个登录界面,登录用户名是user,密码是在项目启动时,输出在控制台

image-20240217123503799

image-20240217123523370

image-20240217123722190

2.SpringBoot整合Redis

我是在Windos环境下安装Redis,这里在Windows下启动Redis 需要进入到安装目录库

输入 redis-server.exe redis.windows.conf

image-20240217124753947

2.1.引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>3.1.8</version>
</dependency>
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.1.8'

2.2.配置Redis

在配置文件中对redis进行配置

# redis相关配置 
spring:data:redis:port: 6379host: 127.0.0.1

2.3.使用Redis Template

2.3.1.将Redis Template注入到Spring容器中

主要是为了 统一管理

@Configuration
public class RedisTemplateConfig {@Bean("sysMyRedisTemplate")public <T> RedisTemplate<String, T> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);RedisSerializer<String> redisSerializer = new StringRedisSerializer();ObjectMapper om = new ObjectMapper();// 持久化改动.设置可见性,om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 持久化改动.非final类型的对象,把对象类型也序列化进去,以便反序列化推测正确的类型om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// 持久化改动.null字段不显示om.setSerializationInclusion(JsonInclude.Include.NON_NULL);// 持久化改动.POJO无public属性或方法时不报错om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);// 持久化改动.setObjectMapper方法移除.使用构造方法传入ObjectMapperGenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);redisTemplate.setKeySerializer(redisSerializer);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashKeySerializer(redisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
2.3.2.RedisTemplate工具类

为了方便使用,可以封装一下工具类进行使用

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;@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 多个对象* @return*/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);}public void incrementCacheMapValue(String key, String hKey, int v) {redisTemplate.opsForHash().increment(key, hKey, v);}/*** 删除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);}
}
2.3.3.测试

测试是否能正常使用

	@RequestMapping("/redis")public String redis(){redisCache.setCacheObject("test", "test");return redisCache.getCacheObject("test").toString();}

image-20240217132211204

3.SpringBoot整合JJWT

3.1.引入依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.12.5</version>
</dependency>
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.12.5'

3.2.JJW工具类

为了方便使用,我们将其封装成一个工具类
由于使用的版本是新版本的JDK 以及 JJWT所以网 这里的工具类 写法会有些出入

/*** JWT Token工具类,用于生成和解析JWT Token** @Author: Tiam* @Date: 2023/10/23 16:38*/
public class TokenUtil {/*** 过期时间(单位:秒)*/public static final int ACCESS_EXPIRE = 60 * 60 * 60;/*** 加密算法*/private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;/*** 私钥 / 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取。* 切记:秘钥不能外露,在任何场景都不应该流露出去。* 应该大于等于 256位(长度32及以上的字符串),并且是随机的字符串*/public final static String SECRET = "secrasdddddddddddddddddddddddddddddddddwqeqeqwewqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqetKey";/*** 秘钥实例*/public static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));/*** jwt签发者*/private final static String JWT_ISS = "Tiam";/*** jwt主题*/private final static String SUBJECT = "Peripherals";/*** 生成访问令牌** @param username 用户名* @return 访问令牌*/public static String genAccessToken(String username) {// 生成令牌IDString uuid = UUID.randomUUID().toString();// 设置过期时间Date expireDate = Date.from(Instant.now().plusSeconds(ACCESS_EXPIRE));return Jwts.builder()// 设置头部信息.header().add("typ", "JWT").add("alg", "HS256").and()// 设置自定义负载信息.claim("username", username).id(uuid) // 令牌ID.expiration(expireDate) // 过期日期.issuedAt(new Date()) // 签发时间.subject(SUBJECT) // 主题.issuer(JWT_ISS) // 签发者.signWith(KEY, ALGORITHM) // 签名.compact();}/*** 获取payload中的用户信息** @param token JWT Token* @return 用户信息*/public static String getUserFromToken(String token) {String user = "";Claims claims = parseClaims(token);if (claims != null) {user = (String) claims.get("username");}return user;}/*** 获取JWT令牌的过期时间** @param token JWT令牌* @return 过期时间的毫秒级时间戳*/public static long getExpirationTime(String token) {Claims claims = parseClaims(token);if (claims != null) {return claims.getExpiration().getTime();}return 0L;}/*** 解析token** @param token token* @return Jws<Claims>*/public static Jws<Claims> parseClaim(String token) {return Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token);}/*** 解析token的头部信息** @param token token* @return token的头部信息*/public static JwsHeader parseHeader(String token) {return parseClaim(token).getHeader();}/*** 解析token的载荷信息** @param token token* @return token的载荷信息*/public static Claims parsePayload(String token) {return parseClaim(token).getPayload();}/*** 解析JWT Token中的Claims** @param token JWT Token* @return Claims*/public static Claims parseClaims(String token) {try {return Jwts.parser().setSigningKey(KEY).build().parseClaimsJws(token).getBody();} catch (Exception e) {return null;}}
}

3.3.测试

	@RequestMapping("/jjwt")public Map<String, String> jjwt(){Map<String, String> map = new HashMap<>();String tokenByKey = TokenUtil.genAccessToken("hrfan");map.put("encoding", tokenByKey);return map;}

image-20240217141907501

4.实战

背景

在企业开发中,一个安全的登录授权系统是至关重要的,它不仅可以保护用户的隐私信息,还能够确保只有经过授权的用户才能够访问特定的资源和功能。这样的系统不仅仅是为了满足用户的安全需求,也是为了保护企业的敏感数据和资源免受未经授权的访问和恶意攻击。

首先,一个安全的登录授权系统必须具备可靠的身份验证机制。用户需要能够通过输入凭据(通常是用户名和密码)来验证其身份。这个过程需要保证用户的密码被安全地存储,并且在传输过程中使用加密技术保障用户凭据的安全性。

其次,授权系统需要根据用户的身份和角色来管理用户对资源和功能的访问权限。不同的用户可能具有不同的角色和权限,例如普通用户、管理员、审计员等。系统需要根据用户的角色和权限来限制他们对资源的访问,以确保敏感数据不会被未经授权的用户获取。

下面使用SpringSecurity来实现一个简易的登录认证

用户身份验证

  1. 登录页面: 我们需要一个登录页面,用户可以在该页面输入他们的凭据以进行身份验证。登录页面应该友好且易于理解。
  2. 身份验证: 用户的用户名和密码应该被验证,只有在验证通过后才能进入系统。密码应该以安全的方式存储,例如使用哈希算法加密存储。
  3. 认证失败处理: 如果用户提供的凭据无效,则系统应该向用户提供相应的错误消息,并允许他们再次尝试登录。

访问控制

  1. 受保护资源: 我们的系统将有一些受保护的资源和功能,例如管理课程、学生信息等。只有经过身份验证的用户才能访问这些资源。
  2. 角色和权限: 不同类型的用户应该有不同的角色和权限。例如,管理员可能具有管理课程和学生的权限,而普通用户可能只能访问课程内容。
  3. 未经授权的访问: 如果用户尝试访问他们没有权限的资源,则系统应该拒绝访问,并向用户显示适当的错误消息。

安全性

  1. 防范攻击: 我们的系统应该能够防范常见的安全攻击,如跨站脚本攻击、SQL注入等。
  2. 密码安全: 用户的密码不应以明文形式存储在数据库中,而应该使用安全的加密算法进行存储。

4.1.创建数据库表

4.1.1.创建用户表

Spring Security要求实现UserDetails接口是为了统一表示用户身份和权限信息,以便于在认证和授权过程中使用。UserDetails提供了标准化的用户信息模型,包括用户名、密码、权限等,使得Spring Security能够与不同的用户信息源集成,同时提供灵活性和可定制性。

RBCA模型介绍

RBAC(Role-Based Access Control)模型是一种访问控制模型,它基于角色来管理对资源的访问权限。在RBAC模型中,用户被分配到不同的角色,而每个角色具有特定的权限。这种模型使得权限管理更加灵活和可扩展,同时降低了管理的复杂性。

  • user表代表系统中的用户。
  • role表代表系统中的角色。
  • permission表代表系统中的权限。
  • user_role表用于关联用户与角色。
  • role_permission表用于关联角色与权限。

image-20240217233212640

CREATE TABLE "hr_manager"."t_sys_my_user" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"user_no" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","user_name" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","password" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","nick_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","phone_number" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","email" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","department_id" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","department_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","is_admin" VARCHAR ( 1 ) COLLATE "pg_catalog"."default","sex" VARCHAR ( 1 ) COLLATE "pg_catalog"."default","post_id" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","post_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","is_account_non_expired" bool,"is_account_non_locked" bool,"is_credentials_non_expired" bool,"is_enabled" bool,"insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,"update_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","update_time" DATE,"license_code" VARCHAR ( 20 ) COLLATE "pg_catalog"."default",CONSTRAINT "t_sys_my_user_pkey" PRIMARY KEY ( "sid" ) 
);
ALTER TABLE "hr_manager"."t_sys_my_user" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."sid" IS '主键SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."user_no" IS '用户登录账号';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."user_name" IS '用户名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."password" IS '用户密码';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."nick_name" IS '用户昵称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."phone_number" IS '手机号码';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."email" IS '邮箱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."department_id" IS '部门ID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."department_name" IS '部门名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_admin" IS '是否为管理员 0 否 1 是';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."sex" IS '性别 0 男 1 女';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."post_id" IS '岗位ID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."post_name" IS '岗位名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_account_non_expired" IS '账户是否过期';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_account_non_locked" IS '账户是否被锁定';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_credentials_non_expired" IS '密码是否过期';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_enabled" IS '账户是否可用';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."insert_user" IS '创建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."insert_time" IS '创建时间';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."update_user" IS '更新人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."update_time" IS '更新时间';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."license_code" IS '许可标识';

4.1.2.创建权限表

CREATE TABLE "hr_manager"."t_sys_my_permission" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"parent_id" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","parent_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","permission_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","permission_code" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","router_path" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","router_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","auth_url" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","order_no" int4,"type" VARCHAR ( 1 ) COLLATE "pg_catalog"."default","icon" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","remark" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,"update_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","update_time" DATE,"license_code" VARCHAR ( 20 ) COLLATE "pg_catalog"."default",CONSTRAINT "t_sys_my_permission_pkey" PRIMARY KEY ( "sid" ) 
);
ALTER TABLE "hr_manager"."t_sys_my_permission" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."sid" IS '主键SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."parent_id" IS '父节点ID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."parent_name" IS '父节点名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."permission_name" IS '权限名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."permission_code" IS '授权标识符';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."router_path" IS '路由地址';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."router_name" IS '路由名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."auth_url" IS '授权路径(对应文件在项目的地址)';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."order_no" IS '序号(用于排序)';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."type" IS '类型 0 目录 1 菜单 2 按钮';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."icon" IS '图标';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."remark" IS '备注';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."insert_user" IS '创建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."insert_time" IS '创建时间';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."update_user" IS '更新人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."update_time" IS '更新时间';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."license_code" IS '许可标识';

4.1.3.创建角色表

CREATE TABLE "hr_manager"."t_sys_my_role" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"role_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","remark" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,"update_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","update_time" DATE,"status" VARCHAR ( 255 ) COLLATE "pg_catalog"."default",CONSTRAINT "t_sys_my_role_pkey" PRIMARY KEY ( "sid" ) 
);
ALTER TABLE "hr_manager"."t_sys_my_role" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."sid" IS '主键SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."role_name" IS '角色名称';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."remark" IS '备注';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."insert_user" IS '创建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."insert_time" IS '创建时间';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."update_user" IS '更新人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."update_time" IS '更新时间';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."status" IS '是否使用 0 禁用 1 使用';

4.1.4.创建用户角色表

CREATE TABLE "hr_manager"."t_sys_my_user_role" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"role_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","user_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,CONSTRAINT "t_sys_my_user_role_pkey" PRIMARY KEY ( "sid" ) 
);
ALTER TABLE "hr_manager"."t_sys_my_user_role" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."sid" IS '主键SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."role_sid" IS '角色SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."user_sid" IS '用户SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."insert_user" IS '创建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."insert_time" IS '创建时间';

4.1.5.创建角色权限表

CREATE TABLE "hr_manager"."t_sys_my_role_permission" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"role_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","permission_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,CONSTRAINT "t_sys_my_role_permission_pkey" PRIMARY KEY ( "sid" ) 
);
ALTER TABLE "hr_manager"."t_sys_my_role_permission" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."sid" IS '主键SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."role_sid" IS '角色SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."permission_sid" IS '权限SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."insert_user" IS '创建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."insert_time" IS '创建时间';

4.2.创建实体类

4.2.1.创建用户实体类

@Data
public class SysMyUser implements Serializable, UserDetails {private static final long serialVersionUID = 1L;@TableId/*** sid*/private String sid;/*** user_no*/private String userNo;/*** user_name*/private String userName;/*** password*/private String password;/*** nick_name*/private String nickName;/*** phone_number*/private String phoneNumber;/*** email*/private String email;/*** department_id*/private String departmentId;/*** department_name*/private String departmentName;/*** is_admin*/private String isAdmin;/*** sex*/private String sex;/*** post_id*/private String postId;/*** post_name*/private String postName;/*** is_account_non_expired*/private Boolean isAccountNonExpired;/*** is_account_non_locked*/private Boolean isAccountNonLocked;/*** is_credentials_non_expired*/private Boolean isCredentialsNonExpired;/*** is_enabled*/private Boolean isEnabled;/*** insert_user*/private String insertUser;/*** insert_time*/private String insertTime;/*** update_user*/private String updateUser;/*** update_time*/private String updateTime;/*** license_code*/private String licenseCode;/*** 权限列表 就是菜单列表*/@TableField(exist = false)private List<SysMyPermission> permissionList;/*** 认证信息 就是用户配置code*/@TableField(exist = false)Collection<? extends GrantedAuthority> authorities;/*** 用户权限信息*/@TableField(exist = false)private List<String> roles;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getUsername() {return this.userNo;}@Overridepublic String getPassword() {return this.password;}@Overridepublic boolean isAccountNonExpired() {return this.isAccountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return this.isAccountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return this.isCredentialsNonExpired;}@Overridepublic boolean isEnabled() {return this.isEnabled;}
}

4.2.2.创建权限实体类

@Data
public class SysMyPermission implements Serializable {private static final long serialVersionUID = 1L;@TableId/*** sid*/private String sid;/*** parent_id*/private String parentId;/*** parent_name*/private String parentName;/*** permission_name*/private String permissionName;/*** permission_code*/private String permissionCode;/*** router_path*/private String routerPath;/*** router_name*/private String routerName;/*** auth_url*/private String authUrl;/*** order_no*/private String orderNo;/*** type*/private String type;/*** icon*/private String icon;/*** remark*/private String remark;/*** insert_user*/private String insertUser;/*** insert_time*/private String insertTime;/*** update_user*/private String updateUser;/*** update_time*/private String updateTime;/*** license_code*/private String licenseCode;/*** 菜单的子集合*/@TableField(exist = false)@JsonInclude(JsonInclude.Include.NON_NULL)private List<SysMyPermission> children = new ArrayList<>();
}

4.3.创建Service和Dao

这里就不过多介绍了,直接贴上代码

4.3.1.UserService

@Service
public class SysMyUserService {@Resourceprivate SysMyUserMapper userMapper;/*** 根据用户id获取用户信息(包含用户具备的权限信息)* @param username 用户信息* @return*/public SysMyUser getUserInfoByUserId(String username) {// 获取用户的基础信息SysMyUser userInfo = userMapper.getUserInfoByUserId(username);Assert.notNull(userInfo, "用户不存在");// 根据用户id对应的权限信息List<String> autorizedList = userMapper.getAutorizedListByUserId(userInfo.getSid());;userInfo.setRoles(autorizedList);return userInfo;}/*** 获取加密后的密码 ,使用BCryptPasswordEncoder加密 10次 生成密码* @param password 密码* @return 加密后的密码*/public String getEncoderPassword(String password) {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);String encodePassword = encoder.encode(password);return encodePassword;}
}

4.3.2.UserMapper

@Repository
public interface SysMyUserMapper extends BaseMapper<SysMyUser> {/*** 根据用户名账号获取用户信息* @param username 用户信息* @return 用户信息*/SysMyUser getUserInfoByUserId(@Param("username") String username);/*** 根据用户id获取用户具备的权限信息* @param sid 用户id* @return 用户具备的权限信息*/List<String> getAutorizedListByUserId(@Param("sid") String sid);
}

4.3.3.UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.sys.my.core.user.dao.SysMyUserMapper"><!-- 根据用户名账号获取用户信息 --><select id="getUserInfoByUserId" resultType="com.sys.my.core.user.model.SysMyUser">select * from t_sys_my_user u where u.user_no = #{username};</select><!-- 根据用户id获取用户具备的权限信息 --><select id="getAutorizedListByUserId" resultType="java.lang.String">selectp.permission_codefrom t_sys_my_role rleft join t_sys_my_user_role ur on ur.role_sid = r.sidleft join t_sys_my_role_permission rp on rp.role_sid = r.sidleft join t_sys_my_permission p on p.sid = rp.permission_sidleft join t_sys_my_user u on u.sid = ur.user_sidwhere p.status = '1' and r.status = '1' and u.sid = #{sid};</select>
</mapper>

4.3.4.SysMyPermissionService

@Service
public class SysMyPermissionService {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());@Resourceprivate SysMyPermissionMapper sysMyPermissionMapper;/*** 根据用户id查询对应的权限* @param userId 用户id* @return 权限列表*/public List<SysMyPermission> getPermissionListByUserId(String userId){// 根据用户ID获取用户对应的权限return sysMyPermissionMapper.getMenuListByUserId(userId);}
}

4.3.5.SysMyPermissionMapper

@Repository
public interface SysMyPermissionMapper extends BaseMapper<SysMyPermission> {/*** 根据用户ID获取用户对应的权限* @param userId 用户ID* @return 权限列表*/List<SysMyPermission> getMenuListByUserId(@Param("userId") String userId);
}

4.3.6.SysMyPermissionMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.sys.my.core.permission.dao.SysMyPermissionMapper"><!-- 根据用户id获取用户具备的权限信息 --><select id="getMenuListByUserId" resultType="com.sys.my.core.permission.model.SysMyPermission">selectp.*from t_sys_my_role rleft join t_sys_my_user_role ur on ur.role_sid = r.sidleft join t_sys_my_role_permission rp on rp.role_sid = r.sidleft join t_sys_my_permission p on p.sid = rp.permission_sidleft join t_sys_my_user u on u.sid = ur.user_sidwhere p.status = '1' and r.status = '1' and u.sid = #{userId};</select>
</mapper>

4.4.重写UserDetailsService方法

重写 Spring Security 中的 UserDetailsService 接口的主要目的是提供自定义的用户认证逻辑。Spring Security 的 UserDetailsService 负责从数据源(通常是数据库)中加载用户信息,包括用户名、密码和权限等,以便进行身份验证。

通常情况下,我们需要重写 UserDetailsServiceloadUserByUsername() 方法,该方法接收用户名作为参数,并返回一个 UserDetails 对象,该对象包含了与用户名对应的用户信息。在实际开发中,我们可能需要自定义的用户信息存储方式,或者希望在加载用户信息时进行一些特定的逻辑处理,比如自定义密码加密方式、从数据库或其他数据源加载用户信息等。

/*** 自定义UserDetailsService 用于认证和授权* 此处把用户的信息和权限交给spring security* spring security会对用户的信息和权限信息进行管理* @author hffan* serDetailService接口主要定义了一个方法 l* oadUserByUsername(String username)用于完成用户信息的查询,* 其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接口,* 完成数据库查询,该接口返回UserDetail。*/
@Component("customerUserDetailsService")
public class CustomerUserDetailsService implements UserDetailsService {@Resourceprivate SysMyUserService userService;@Resourceprivate SysMyPermissionService permissionService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysMyUser user = userService.getUserInfoByUserId(username);// 如果用户不存在if (user == null){throw new UsernameNotFoundException("用户名或者密码错误");}// 根据用户id查询用户权限List<SysMyPermission> permissionList = permissionService.getPermissionListByUserId(user.getSid());// 取出权限中配置codeList<String> collect = permissionList.stream().filter(item -> item != null).map(item -> item.getPermissionCode()).filter(item -> item != null).collect(Collectors.toList());// 转为数据String[] strings = collect.toArray(new String[collect.size()]);List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(strings);// 配置权限user.setAuthorities(authorityList);// 配置菜单user.setPermissionList(permissionList);// 授权return user;}
}

4.5.自定义异常

自定义异常,通过传入的异常 可以获取对应的信息返回给前端

4.5.1.Token认证自定义异常

/*** 自定义异常 * AuthenticationException 是spring security提供的异常* 通过传入的异常 可以获取对应的信息返回给前端* token异常*/
public class TokenException extends AuthenticationException {public TokenException(String msg) {super(msg);}
}

4.5.2.用户认证自定义异常

/*** 自定义异常* 通过传入的异常 可以获取对应的信息返回给前端* 用户认证异常*/
public class CustomerAuthenionException extends AuthenticationException {public CustomerAuthenionException(String msg) {super(msg);}
}

4.6.编写自定义处理器

通过实现SpringSecurity提供的一些接口,我们可以更好地管理身份验证和授权流程,提高用户体验和应用程序的安全性。

4.6.1.匿名用户访问处理器

AuthenticationEntryPoint

  • 作用:AuthenticationEntryPoint 用于处理用户尝试访问受保护资源但未进行身份验证的情况。当用户尝试访问需要身份验证的资源但尚未进行身份验证时,AuthenticationEntryPoint 将被调用来触发身份验证流程。
  • 详细讲解:当用户尝试访问安全受保护的资源但未进行身份验证时,AuthenticationEntryPoint 的 commence() 方法将被调用。在这个方法中,我们可以定制返回响应给用户,例如重定向到登录页面或返回401未授权错误等。
/*** 匿名用户访问资源处理器*/
@Component("loginAuthenticationHandler")
public class LoginAuthenticationHandler implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String res = JSONObject.toJSONString(ResultObject.createInstance(false,600,"匿名用户没有权限进行访问!"));out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}

4.6.2.认证用户无权限处理器

AccessDeniedHandler

  • 作用:AccessDeniedHandler 用于处理用户尝试访问受保护资源但权限不足的情况。当用户虽然进行了身份验证,但由于缺乏足够的权限而被拒绝访问资源时,AccessDeniedHandler 将被调用。
  • 详细讲解:AccessDeniedHandler 的 handle() 方法在访问被拒绝时被调用。我们可以在这个方法中定义自定义的行为,例如返回自定义的错误页面、向用户发送通知或记录拒绝的访问尝试。
/*** 认证用户访问无权限处理器*/
@Component("loginAccessDefineHandler")
public class LoginAccessDefineHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String res = JSONObject.toJSONString(ResultObject.createInstance(false,700,"您没有开通对应的权限,请联系管理员!"));out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}

4.6.3.账户信息异常处理器

AuthenticationFailureHandler

  • 作用:AuthenticationFailureHandler 用于处理身份验证失败的情况。当用户提供的凭据无效或身份验证过程出现错误时,AuthenticationFailureHandler 将被调用。
  • 详细讲解:AuthenticationFailureHandler 的 onAuthenticationFailure() 方法在身份验证失败时被调用。我们可以在这个方法中执行自定义的行为,例如记录登录失败次数、向用户发送通知或返回自定义的错误页面。
@Component("loginFiledHandler")
public class LoginFiledHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {//1.设置响应编码response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String str = null;int code = 500;if(exception instanceof AccountExpiredException){str = "账户过期,登录失败!";}else if(exception instanceof BadCredentialsException){str = "用户名或密码错误,登录失败!";}else if(exception instanceof CredentialsExpiredException){str = "密码过期,登录失败!";}else if(exception instanceof DisabledException){str = "账户被禁用,登录失败!";}else if(exception instanceof LockedException){str = "账户被锁,登录失败!";}else if(exception instanceof InternalAuthenticationServiceException){str = "账户不存在,登录失败!";}else if(exception instanceof CustomerAuthenionException){//token验证失败code = 600;str = exception.getMessage();} else{str = "登录失败!";}// 设置返回格式String res = JSONObject.toJSONString(ResultObject.createInstance(false,str));out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}

4.6.4.登录成功处理器

AuthenticationSuccessHandler

  • 作用:AuthenticationSuccessHandler 用于处理身份验证成功的情况。当用户成功进行身份验证并被授权访问资源时,AuthenticationSuccessHandler 将被调用。
  • 详细讲解:AuthenticationSuccessHandler 的 onAuthenticationSuccess() 方法在身份验证成功时被调用。我们可以在这个方法中执行自定义的行为,例如记录登录成功的日志、向用户发送欢迎消息或重定向到特定页面。
/*** 自定义认证成功处理器*/
@Component("loginSuccessHandler")
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Resourceprivate RedisCache redisCache;@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {SysMyUser user = (SysMyUser)authentication.getPrincipal();// 登录成功处理//1.生成tokenString token = TokenUtil.genAccessToken(user.getUsername());long expireTime = TokenUtil.getExpirationTime(token);// 配置一下返回给前端的token信息LoginResultObject vo = new LoginResultObject();// 将实体类信息转为JSON// TODO 将token存入coookie中 后面加载页面 根据用户的id取查询对应的权限vo.setUserInfo(user);vo.setCode(200L);// TODO 将token存放到redis中 退出或者修改密码 清空token 获取的时候 也从redis中进行获取redisCache.setCacheObject(httpServletRequest.getRemoteAddr(),token,TokenUtil.ACCESS_EXPIRE, TimeUnit.MILLISECONDS);vo.setToken(token);vo.setExpireTime(expireTime);String res = JSONObject.toJSONString(vo);httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream out = httpServletResponse.getOutputStream();out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}

4.7.自定义过滤器

实现 Spring Security 中的 OncePerRequestFilter 接口,用于处理用户请求的过滤逻辑。

  • 该过滤器用于对用户的请求进行拦截,验证用户的访问权限和身份信息。
  • 如果请求的 URL 是某些特定的资源或者登录页面,则直接放行。
  • 如果不是登录请求,则对请求中的 token 进行验证,以确保用户的身份信息有效。
  • 如果验证通过,则将用户的身份信息设置到 Spring Security 的上下文中,从而完成用户的身份认证。
  • @Component("checkTokenFilter"):将该类声明为 Spring 组件,并指定其名称为 “checkTokenFilter”。

  • @EqualsAndHashCode(callSuper=false):生成 equals() 和 hashCode() 方法,忽略父类 OncePerRequestFilter。

  • @Data:Lombok 注解,自动生成 getter、setter、equals、hashCode 等方法。

  • @Autowired@Value:用于依赖注入和获取配置信息。

  • doFilterInternal
    

    方法:这是 OncePerRequestFilter 类的抽象方法,用于实现具体的请求过滤逻辑。

    • 首先判断请求的 URL 是否属于特定的资源,如果是则放行。
    • 判断是否是登录请求,如果是,则直接放行。
    • 如果不是登录请求,则验证请求中的 token,确保用户的身份信息有效。
    • 如果 token 验证失败,则调用 AuthenticationFailureHandler 处理身份验证失败的情况。
    • 如果 token 验证通过,则将用户的身份信息设置到 Spring Security 的上下文中。
  • validateToken
    

    方法:用于验证请求中的 token。

    • 首先从请求头部获取 token,如果没有则从请求参数中获取,如果仍然没有则从 Redis 缓存中获取。
    • 解析 token,获取其中的用户名。
    • 根据用户名加载用户信息,使用自定义的 CustomerUserDetailsService。
    • 如果用户信息加载成功,则创建 UsernamePasswordAuthenticationToken,并将用户信息设置到 Spring Security 上下文中。
  • 最后调用 filterChain.doFilter(httpServletRequest, httpServletResponse),将请求传递给下一个过滤器处理。

@Data
@Component("checkTokenFilter")
@EqualsAndHashCode(callSuper=false)
public class CheckTokenFilter extends OncePerRequestFilter {@Value("${hrfan.login.url}")private String loginUrl;@Autowiredprivate LoginFiledHandler loginFailureHandler;@Autowiredprivate CustomerUserDetailsService customerUserDetailsService;@Resourceprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {//获取请求的url(读取配置文件的url)String url = httpServletRequest.getRequestURI();if (StringUtils.contains(httpServletRequest.getServletPath(), "swagger")|| StringUtils.contains(httpServletRequest.getServletPath(), "webjars")|| StringUtils.contains(httpServletRequest.getServletPath(), "v3")|| StringUtils.contains(httpServletRequest.getServletPath(), "profile")|| StringUtils.contains(httpServletRequest.getServletPath(), "swagger-ui")|| StringUtils.contains(httpServletRequest.getServletPath(), "swagger-resources")|| StringUtils.contains(httpServletRequest.getServletPath(), "csrf")|| StringUtils.contains(httpServletRequest.getServletPath(), "favicon")|| StringUtils.contains(httpServletRequest.getServletPath(), "v2")|| StringUtils.contains(httpServletRequest.getServletPath(), "user")|| StringUtils.contains(httpServletRequest.getServletPath(), "getImageCode")) {filterChain.doFilter(httpServletRequest, httpServletResponse);}else if (StringUtils.equals(url,loginUrl)){// 是登录请求放行filterChain.doFilter(httpServletRequest, httpServletResponse);}else {try {//token验证(如果不是登录请求 验证toekn)if(!url.equals(loginUrl)){validateToken(httpServletRequest);}}catch (AuthenticationException e){loginFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);return;}filterChain.doFilter(httpServletRequest,httpServletResponse);}}//token验证private void validateToken(HttpServletRequest request){//从请求的头部获取tokenString token = request.getHeader("token");//如果请求头部没有获取到token,则从请求参数中获取tokenif(StringUtils.isEmpty(token)){token = request.getParameter("token");}if (StringUtils.isEmpty(token)){// 请求参数中也没有 那就从redis中进行获取根据ip地址取token = redisCache.getCacheObject(request.getRemoteAddr());}if(StringUtils.isEmpty(token)){throw new CustomerAuthenionException("token不存在!");}//解析tokenString username = TokenUtil.getUserFromToken(token);if(StringUtils.isEmpty(username)){throw new CustomerAuthenionException("token解析失败!");}//获取用户信息UserDetails user = customerUserDetailsService.loadUserByUsername(username);if(user == null){throw new CustomerAuthenionException("token验证失败!");}UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//设置到spring security上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);}
}

4.8.设置登录返回信息

用户返回用户登录 成功或者失败的信息,成功后需要包含用户的相关信息 和token

/*** 登录返回信息*/
@Data
public class LoginResultObject {private String token;//token过期时间private Long expireTime;private SysMyUser userInfo;private Long code;
}

4.9.编写SpringSecurity配置

#### 注意因为新版本的SpringSecurity和旧版本的差距较大,所以这里保留了旧版本的写法我使用的SpringBoot 和 SpringSecurity 版本都是相对较新的 3.1.8版本 JDK版本是21
import com.sys.my.config.security.details_service.CustomerUserDetailsService;
import com.sys.my.config.security.filter.CheckTokenFilter;
import com.sys.my.config.security.handler.LoginAccessDefineHandler;
import com.sys.my.config.security.handler.LoginAuthenticationHandler;
import com.sys.my.config.security.handler.LoginFiledHandler;
import com.sys.my.config.security.handler.LoginSuccessHandler;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import java.util.Collections;/*** SpringSecurity配置类*/
@Configuration
@EnableWebSecurity  //启用Spring Security
public class SpringSecurityConfig {@Resourceprivate CustomerUserDetailsService customerUserDetailsService;@Resourceprivate LoginSuccessHandler loginSuccessHandler;@Resourceprivate LoginFiledHandler loginFiledHandler;@Resourceprivate LoginAuthenticationHandler loginAuthenticationHandler;@Resourceprivate LoginAccessDefineHandler loginAccessDefineHandler;@Resourceprivate CheckTokenFilter checkTokenFilter;/*** 密码处理* @return*/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}/*** 新版的实现方法不再和旧版一样在配置类里面重写方法,而是构建了一个过滤链对象并通过@Bean注解注入到IOC容器中* 新版整体代码 (注意:新版AuthenticationManager认证管理器默认全局)* @param http http安全配置* @return SecurityFilterChain* @throws Exception 异常*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http    // 使用自己自定义的过滤器 去过滤接口请求.addFilterBefore(checkTokenFilter, UsernamePasswordAuthenticationFilter.class).formLogin((formLogin) ->// 这里更改SpringSecurity的认证接口地址,这样就默认处理这个接口的登录请求了formLogin.loginProcessingUrl("/api/v1/user/login")// 自定义的登录验证成功或失败后的去向.successHandler(loginSuccessHandler).failureHandler(loginFiledHandler))// 禁用了 CSRF 保护。.csrf((csrf) -> csrf.disable())// 配置了会话管理策略为 STATELESS(无状态)。在无状态的会话管理策略下,应用程序不会创建或使用 HTTP 会话,每个请求都是独立的,服务器不会在请求之间保留任何状态信息。.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeRequests((authorizeRequests) ->// 这里过滤一些 不需要token的接口地址authorizeRequests.requestMatchers("/api/v1/test/getTestInfo").permitAll().requestMatchers(  "/v3/**","/profile/**","/swagger-ui.html","/swagger-resources/**","/v2/api-docs","/v3/api-docs","/webjars/**","/swagger-ui/**","/v2/**","/favicon.ico","/webjars/springfox-swagger-ui/**","/static/**", "/webjars/**", "/v2/api-docs", "/v2/feign-docs","/swagger-resources/configuration/ui","/test/user","/swagger-resources", "/swagger-resources/configuration/security","/swagger-ui.html", "/webjars/**").permitAll().requestMatchers("/api/v1/user/login","/api/v1/user/getImageCode").permitAll().anyRequest().authenticated()).exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(loginAuthenticationHandler) // 匿名处理.accessDeniedHandler(loginAccessDefineHandler)  // 无权限处理).cors((cors) -> cors.configurationSource(configurationSource())).headers((headers) -> headers.frameOptions((frameOptionsConfig -> frameOptionsConfig.disable()))).headers((headers) -> headers.frameOptions((frameOptionsConfig -> frameOptionsConfig.sameOrigin())));// 构建过滤链并返回return http.build();}// 旧版本 需要继承  extends WebSecurityConfigurerAdapter// 新版的比较简单,直接定义好数据源,注入就可以了,无需手动到配置类中去将它提交给AuthenticationManager进行管理。// /**//  * 配置认证处理器//  * 自定义的UserDetailsService//  * @param auth//  * @throws Exception//  */// @Override// protected void configure(AuthenticationManagerBuilder auth) throws Exception {//     auth.userDetailsService(customerUserDetailsService);// }// /**//  * 配置权限资源//  * @param http//  * @throws Exception//  */// @Override// protected void configure(HttpSecurity http) throws Exception {//     // 每次请求前检查token//     http.addFilterBefore(checkTokenFilter, UsernamePasswordAuthenticationFilter.class);//     http.formLogin()//             .loginProcessingUrl("/api/v1/user/login")//             // 自定义的登录验证成功或失败后的去向//             .successHandler(loginSuccessHandler).failureHandler(loginFiledHandler)//             // 禁用csrf防御机制(跨域请求伪造),这么做在测试和开发会比较方便。//             .and().csrf().disable()//             .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//             .and()//             .authorizeRequests()//             .antMatchers("/api/v1/test/getTestInfo").permitAll()//             // 放心swagger相关请求//             .antMatchers(  "/v3/**","/profile/**","/swagger-ui.html",//                     "/swagger-resources/**",//                     "/v2/api-docs",//                     "/v3/api-docs",//                     "/webjars/**","/swagger-ui/**","/v2/**","/favicon.ico","/webjars/springfox-swagger-ui/**","/static/**", "/webjars/**", "/v2/api-docs", "/v2/feign-docs",//                     "/swagger-resources/configuration/ui",//                     "/swagger-resources", "/swagger-resources/configuration/security",//                     "/swagger-ui.html", "/webjars/**").permitAll()//             .antMatchers("/api/v1/user/login","/api/v1/user/getImageCode").permitAll()//             .anyRequest().authenticated()//             .and()//             .exceptionHandling()//             // 匿名处理//             .authenticationEntryPoint(loginAuthenticationHandler)//             // 无权限处理//             .accessDeniedHandler(loginAccessDefineHandler)//             // 跨域配置//             .and()//             .cors()//             .configurationSource(configurationSource());//     // 设置iframe//     http.headers().frameOptions().sameOrigin();//     http.headers().frameOptions().disable();//// }/*** 跨域配置*/CorsConfigurationSource configurationSource() {CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));corsConfiguration.setAllowedMethods(Collections.singletonList("*"));corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));corsConfiguration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", corsConfiguration);return source;}
}

4.10.配置文件配置

hrfan:login:url: "/api/v1/user/login"

5.测试

5.1.测试登录密码错误

image-20240218001551442

5.2.测试正确密码

image-20240218001649070

5.3.测试无token访问接口

SpringSecurity为我们提供了基于注解的权限控制方案。

在启动类上加上 @EnableGlobalMethodSecurity(prePostEnabled = true)

	@GetMapping("/jjwt")@PreAuthorize("hasAuthority('user_list')")public Map<String, String> jjwt(){// 这里的user_list 就是我们权限中permission_codethrow new RuntimeException("测试无token访问!");}

image-20240218002547214

5.4.测试不登陆访问

image-20240218002712121

5.5.测试登录访问不受限制接口

image-20240218002909117

image-20240218004426367

5.6.测试放开的通用接口 例如/**

image-20240218004734952

image-20240218004839439

5.7.测试权限标识 和数据库不一致

image-20240218091500059

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux线程【互斥与同步】

目录 1.资源共享问题 1.1多线程并发访问 1.2临界区和临界资源 1.3互斥锁 2.多线程抢票 2.1并发抢票 2.2 引发问题 3.线程互斥 3.1互斥锁相关操作 3.1.1互斥锁创建与销毁 3.1.2、加锁操作 3.1.3 解锁操作 3.2.解决抢票问题 3.2.1互斥锁细节 3.3互斥…

github用法详解

本文是一篇面向全体小白的文章,图文兼备。为了让小白们知道如何使用GitHub,我努力将本文写得通俗易懂,尽量让刚刚上网的小白也能明白。所以各位程序员们都可以滑走了~ 啥是GitHub? 百度百科会告诉你, GitHub是一个面向开源及私有软件项目的托管平台,因为只支持Git作为…

Topaz Gigapixel AI:让每一张照片都焕发新生mac/win版

Topaz Gigapixel AI 是一款革命性的图像增强软件&#xff0c;它利用先进的人工智能技术&#xff0c;能够显著提升图像的分辨率和质量。无论是摄影爱好者还是专业摄影师&#xff0c;这款软件都能帮助他们将模糊的、低分辨率的照片转化为清晰、细腻的高分辨率图像。 Topaz Gigap…

JavaWeb——011 SpringBootWeb综合案例(删除/修改员工、文件上传、配置文件)

SpringBootWeb案例 目录 SpringBootWeb案例1. 新增员工1.1 需求1.2 接口文档1.3 思路分析1.4 功能开发1.5 功能测试1.6 前后端联调 2. 文件上传2.1 简介2.2 本地存储2.3 阿里云OSS2.3.1 准备2.3.2 入门2.3.3 集成 3. 修改员工3.1 查询回显3.1.1 接口文档3.1.2 实现思路3.1.3 代…

07 编译器

目录 编译过程编译器查看详解函数库自动化构建工具进度条程序 1. 编译过程 预处理: a. 去注释 b.宏替换 c.头文件展开 d.条件编译 编译: 汇编 汇编: 可重定向二进制目标文件 链接: 链接多个.o, .obj合并形成一个可执行exe gcc编译c程序, g编译c程序 2. 编译器查看 输入gcc …

mac苹果电脑c盘满了如何清理内存?2024最新操作教程分享

苹果电脑用户经常会遇到麻烦:内置存储器(即C盘)空间不断缩小&#xff0c;电脑运行缓慢。在这种情况下&#xff0c;苹果电脑c盘满了怎么清理&#xff1f;如何有效清理和优化存储空间&#xff0c;提高计算机性能&#xff1f;成了一个重要的问题。今天&#xff0c;我想给大家详细介…

备战蓝桥杯---线段树基础2

今天我们把线段树的另一个模板看一下&#xff1a; 在这里&#xff0c;我们注意到乘的操作&#xff0c;因此我们用两个懒标记来分别表示加和乘&#xff0c;这时我们面临了一个问题&#xff0c;就是当我们把标记往下传时&#xff0c;它的儿子怎么知道是先乘还是先加&#xff1f; …

2025张宇考研数学,百度网盘视频课+36讲PDF讲义+真题

张宇老师的课属于幽默生动&#xff0c;会让一个文科生爱上数学&#xff0c;但是有的同学不知道在哪看&#xff0c;可以看一下&#xff1a;2025张宇考研数学全程网盘 docs.qq.com/doc/DTmtOa0Fzc0V3WElI 可以粘贴在浏览器 张宇30讲作为一本基础讲义&#xff1a;和教材…

java的线程池介绍

什么是线程池&#xff1f; 线程池是一种用于管理和复用线程的机制&#xff0c;旨在减少线程的创建和销毁次数&#xff0c;提高线程的可重用性和执行效率。通过线程池&#xff0c;可以控制线程的数量、数量大小以及线程的执行方式&#xff0c;从而更加有效地处理并发任务。 线…

如何使用公网地址远程访问内网Nacos UI界面查看注册服务

文章目录 1. Docker 运行Nacos2. 本地访问Nacos3. Linux安装Cpolar4. 配置Nacos UI界面公网地址5. 远程访问 Nacos UI界面6. 固定Nacos UI界面公网地址7. 固定地址访问Plik Nacos是阿里开放的一款中间件,也是一款服务注册中心&#xff0c;它主要提供三种功能&#xff1a;持久化…

6、wuzhicms代码审计

wuzhicms代码审计 前言 安装环境配置 服务器要求 Web服务器: apache/nginx/iis PHP环境要求:支持php5.2、php5.3、php5.4、php5.5、php5.6、php7.1 (推荐使用5.4或更高版本!) 数据库要求: Mysql5www/install文件夹即可进入安装页面 审计开始 首页文件index.php&#xff0c…

kubernetes+prometheus+grafana监控+alertmanager实现qq邮箱报警

prometheus基于kubernetes监控 prometheus对kubernetes的监控 对于Kubernetes而言&#xff0c;我们可以把当中所有的资源分为几类&#xff1a; 基础设施层&#xff08;Node&#xff09;&#xff1a;集群节点&#xff0c;为整个集群和应用提供运行时资源容器基础设施&#xff…

C/C++内存管理及内存泄漏详解

目录 C/C内存分布 C语言中动态内存管理方式&#xff1a;malloc/calloc/realloc/free C内存管理方式 new/delete操作内置类型 new和delete操作自定义类型 operator new与operator delete函数 new和delete的实现原理 内置类型 自定义类型 内存泄漏 概念 内存泄漏分类 ⭐…

180基于matlab的频率切片小波变换程序(FTWT)

基于matlab的频率切片小波变换程序&#xff08;FTWT&#xff09;。从一种新的角度出发&#xff0c;通过自由选择频率切片函数、引进新尺度参数&#xff0c;在频率域实现小波变换&#xff0c;该变换能够很好地刻画信号各成分之间的相对能量关系。此外&#xff0c;频率切片小波变…

【InternLM 实战营笔记】OpenCompass大模型评测

随着人工智能技术的快速发展&#xff0c; 大规模预训练自然语言模型成为了研究热点和关注焦点。OpenAI于2018年提出了第一代GPT模型&#xff0c;开辟了自然语言模型生成式预训练的路线。沿着这条路线&#xff0c;随后又陆续发布了GPT-2和GPT-3模型。与此同时&#xff0c;谷歌也…

探讨苹果 Vision Pro 的 AI 数字人形象问题

Personas 的设计模糊性&#xff1a; 部分人认为这种模糊设计可能是出于安全考虑&#x1f6e1;️。安全角度&#xff1a;Personas 代表着你的 AI 数字形象&#xff0c;在创建时&#xff0c;它相当于你的 AVP&#xff08;生物识别扫描器的存在增加了冒充的难度&#xff09;。如果…

40、网络编程/TCP和UDP通信模型练习20240229

一、使用TCP模型创建服务器和客户端完成简单通信。 服务器代码&#xff1a; #include<myhead.h> #define SER_IP "192.168.32.130" #define SER_PORT 8888 int main(int argc, const char *argv[]) {//1.创建监听的套接字int sfd-1;sfdsocket(AF_INET,SOCK_S…

解决 MySQL 未运行但锁文件存在的问题

查看mysql状态时&#xff0c;显示错误信息"ERROR! MySQL is not running, but lock file (/var/lock/subsys/mysql) exists"。 解决步骤 1、检查 MySQL 进程是否正在运行 在继续之前&#xff0c;我们首先需要确定 MySQL 进程是否正在运行。我们可以使用以下命令检查…

港中文联合MIT提出超长上下文LongLoRA大模型微调算法

论文名称&#xff1a; LongLoRA: Efficient Fine-tuning of Long-Context Large Language Models 文章链接&#xff1a;https://arxiv.org/abs/2309.12307 代码仓库&#xff1a; https://github.com/dvlab-research/LongLoRA 现阶段&#xff0c;上下文窗口长度基本上成为了评估…

算法修炼-动态规划之路径问题(1)

62. 不同路径 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;选定一个网格为终点&#xff0c;走到这个网格的所有走法就是这个网格的上面一个网格的所有走法加上这个网格左边一个网格的所有走法&#xff0c;然后做好初始化工作就行。 class Solution { public:int…