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

news2024/11/25 11:36:31

简介

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: 6379
      host: 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方法移除.使用构造方法传入ObjectMapper
        GenericJackson2JsonRedisSerializer 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 {
    @Autowired
    public 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) {
        // 生成令牌ID
        String 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;



    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getUsername() {
        return this.userNo;
    }
    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.isCredentialsNonExpired;
    }

    @Override
    public 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 {

    @Resource
    private 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">
        select
            p.permission_code
        from t_sys_my_role r
                 left join t_sys_my_user_role ur on ur.role_sid = r.sid
                 left join t_sys_my_role_permission rp on rp.role_sid = r.sid
                 left join t_sys_my_permission p on p.sid = rp.permission_sid
                 left join t_sys_my_user u on u.sid = ur.user_sid
        where 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());

    @Resource
    private 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">
        select
            p.*
        from t_sys_my_role r
                 left join t_sys_my_user_role ur on ur.role_sid = r.sid
                 left join t_sys_my_role_permission rp on rp.role_sid = r.sid
                 left join t_sys_my_permission p on p.sid = rp.permission_sid
                 left join t_sys_my_user u on u.sid = ur.user_sid
        where 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 {
    @Resource
    private SysMyUserService userService;
    @Resource
    private SysMyPermissionService permissionService;

    @Override
    public 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());
        // 取出权限中配置code
        List<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 {
    @Override
    public 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 {


    @Override
    public 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 {

    @Override
    public 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 {

    @Resource
    private RedisCache redisCache;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        SysMyUser user = (SysMyUser)authentication.getPrincipal();
        // 登录成功处理
        //1.生成token
        String 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;

    @Autowired
    private LoginFiledHandler loginFailureHandler;
    @Autowired
    private CustomerUserDetailsService customerUserDetailsService;

    @Resource
    private RedisCache redisCache;
    @Override
    protected 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){
        //从请求的头部获取token
        String token = request.getHeader("token");
        //如果请求头部没有获取到token,则从请求参数中获取token
        if(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不存在!");
        }
        //解析token
        String 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 {


    @Resource
    private CustomerUserDetailsService customerUserDetailsService;



    @Resource
    private LoginSuccessHandler loginSuccessHandler;
    @Resource
    private LoginFiledHandler loginFiledHandler;
    @Resource
    private LoginAuthenticationHandler loginAuthenticationHandler;
    @Resource
    private LoginAccessDefineHandler loginAccessDefineHandler;

    @Resource
    private CheckTokenFilter checkTokenFilter;
    /**
     * 密码处理
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 新版的实现方法不再和旧版一样在配置类里面重写方法,而是构建了一个过滤链对象并通过@Bean注解注入到IOC容器中
     * 新版整体代码 (注意:新版AuthenticationManager认证管理器默认全局)
     * @param http http安全配置
     * @return SecurityFilterChain
     * @throws Exception 异常
     */
    @Bean
    public 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_code
		throw 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.coloradmin.cn/o/1484353.html

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

相关文章

Python中几个必须知道的函数

Python中自带了几个比较有意思的函数&#xff0c;一般在面试或者笔试基础的时候会问到&#xff0c;其中3个就是map、filter、reduce函数。 1.map(function, iterable) 它第一个要传的元素是函数名或lambda匿名函数表达式&#xff0c;第二个元素传入可迭代对象。 array [1,2,…

【饮食】日常零食 保健食品分类(附食品营养成分表与执行标准,Coursera营养学课程笔记)

程序员生活指南之 【饮食】日常零食 & 保健食品分类和推荐&#xff08;附食品营养成分表与执行标准&#xff09; 文章目录 一、保健食品1、什么是保健食品&#xff1f;2、常见保健食品分类3、常见保健食品推荐 二、日常零食&#xff08;食品营养成分表与执行标准&#xff0…

详解JavaScript的函数

详解 JavaScript 的函数 函数的语法格式 创建函数/函数声明/函数定义 function 函数名(形参列表) { 函数体 return 返回值; // return 语句可省略 } 函数调用 函数名(实参列表) // 不考虑返回值 返回值 函数名(实参列表) // 考虑返回值 示例代码 //定义的没有参数列表&am…

【C语言】sizeof和strlen的比较

1. sizeof和strlen的对比 1.1 sizeof 在学习操作符的时候&#xff0c;我们学习了 sizeof &#xff0c; sizeof 是一个单目操作符&#xff0c; 绝对不是函数&#xff01;&#xff01;&#xff01;sizeof 计算变量所占内存内存空间⼤⼩的&#xff0c;单位是字节。 如果操作数…

three.js 点乘判断平行向量方向异同

效果&#xff1a; 代码&#xff1a; <template><div><el-container><el-main><div class"box-card-left"><div id"threejs"></div><div>判断的前提是两个向量平行<el-button click"judge"…

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

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

latex使用Bibtex添加参考文献指南(TeXstudio)

目录 参考链接 Bibtex 使用方法 编译方法 参考链接 https://www.cnblogs.com/whyaza/p/11803493.html &#xff08;Latex&#xff09;Latex TeXstudio Bibtex 使用指南 - 简书 Latex-bibtex使用方法-CSDN博客 Latex插入参考文献的两种方法—自动与手动_latex 参考文献-…

免费下载全网视频系列:一键下载央视视频

之前分享过全网视频下载工具下载视频不求人&#xff0c;免费下载全网视频&#xff0c;今天再分享几个下载央视视频的工具。 第一个是央视频4k下载器&#xff0c;比如下载这个视频https://www.yangshipin.cn/#/video/home?vidv0000313oqb&#xff0c;打开工具在命令行输入 v00…

Ubuntu将c++编译成.so文件并测试

一、准备cpp和h文件 创建test.cpp 在cpp中定义相加的函数funcAdd&#xff0c;给出函数的细节代码 #include <iostream> using namespace std;int funcAdd(int x, int y) {return xy; }创建test.h 在h中声明定义的函数&#xff0c;不需要任何细节 #ifndef __TEST__ #…

实验:依赖注入之构造器注入

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

文章总结(拉钩教育)

方法论 那对于我们专栏来说&#xff0c;在面对复杂的检索知识的时候&#xff0c;我更建议你通过理解记忆的方式进行学习。具体的方式有啥呢&#xff1f;我比较推荐问答的方式。也就是说&#xff0c;在学习每个知识点的时候&#xff0c;你可以一直问自己几个问题&#xff0c;比…

地信专业未来的前景如何,该怎么学习?

广大普通学校GIS专业学生&#xff0c;如果继续在GIS方向发展&#xff0c;而且走开发路线&#xff0c;结合我这几年面试以及带应届毕业生的经验&#xff0c;学习路线我有这几个方面的建议&#xff0c;仅供参考&#xff1a; 1.大一的时候要学好高数、线性代数和概率论&#xff0c…

STM32单片机示例:ETH_DP83848_DHCP_NonOS_Poll_F407

文章目录 目的基础说明主要配置关键代码示例演示示例链接关于中断总结 目的 以太网是比较常用到的功能&#xff0c;这篇文章讲演示在STM32F407上启用以太网功能&#xff0c;使之能够加入网络中&#xff0c;通过DHCP获得IP地址&#xff0c;可以被Ping通。 基础说明 STM32F407…

EI顶刊复现:基于氨储能技术的电转氨耦合风–光–火综合能源系统双层优化调度程序代码!

适用平台&#xff1a;MatlabYalmipCplex 程序首先提出电转氨耦合综合能源系统构型&#xff0c;并为燃煤机组出力、风光消纳和电转氨运行的经济性和稳定性的综合评价定义风–光–火–氨系统协调运行指标&#xff1b;进而构建以协调运行指标最大为上层目标、电转氨耦合风–光–火…

数据结构与算法:堆

朋友们大家好啊&#xff0c;本篇文章来到堆的内容&#xff0c;堆是一种完全二叉树&#xff0c;再介绍堆之前&#xff0c;我们首先对树进行讲解 树与堆 1.树的介绍1.1节点的分类 2.树的存储结构3.二叉树的概念和结构3.1 二叉树的特点3.2 特殊的二叉树3.3二叉树的存储结构 4.堆的…

(科目三)计算机基础及网络的应用

1.计算基础知识 计算机是20世纪人类最伟大的发明之一&#xff0c;它是由电子逻辑部件组成的一种能够存储信息、自动完成各种运算的逻辑设备。 世界上第一台数字电子计算机ENIAC于1946年诞生在美国的宾夕法尼亚大学&#xff0c;由约翰莫克里和普雷斯波艾克领导研制。 当前&am…

【SpringBoot】mybaitsPlus的多数据源配置

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;SpringBoot ⛺️稳重求进&#xff0c;晒太阳 mybatisPlus的多数据源配置 适用于多种场景&#xff1a;纯粹多库、 读写分离、 一主多从、 混合模式等 目前我们就来模拟一个纯粹多…

React-router的创建和第一个组件

需要先学react框架 首先&#xff1a;找到一个文件夹&#xff0c;在文件夹出打开cmd窗口&#xff0c;输入如下图的口令 npx create-react-app demo 然后等待安装 安装完成 接下来进入创建的demo实例 cd demo 然后可以用如下方式打开vscode code . 注意&#xff1a;不要忽略点号与…

监控与日志

一、监控 1、监控类型 从监控类型上划分&#xff0c;在 K8s 中可以分成四个不同的类型&#xff1a; ① 资源监控&#xff1a;这种监控主要关注于基础资源的使用情况&#xff0c;例如 CPU、内存、网络等。通常使用数值或百分比等单位来统计&#xff0c;可以通过 Zabbix、Tele…