【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: 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

巧用眼精星票证识别系统将车辆合格证快速转为结构化excel数据,简单方便

眼精星票证识别系统是一款高效且精准的OCR软件&#xff0c;它的魔力在于能将纸质文档迅速转化为电子文档&#xff0c;并实现自动化的数据结构化处理。它拥有一双"火眼金睛"&#xff0c;无论是各类发票、护照&#xff0c;还是车辆合格证等&#xff0c;都能一一识别。而…

LLC谐振变换器变频移相混合控制MATLAB仿真

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 基本控制原理 为了实现变换器较小的电压增益&#xff0c;同时又有较 高的效率&#xff0c;文中在变频控制的基础上加入移相控制&#xff0c; 在这种控制策略下&#xff0c;变换器通过调节一次侧开关管 的开关…

数据增加

目录 增加数据 实现数据增加&#xff0c;保存新的内容 注意 Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 增加数据 由于 emp 表中的数据对日后的开发依然有用处&#xff0c;所以在讲解更新之前 建议将emp 表数据做一个复制。将…

【C++入门】C++关键字 | 命名空间 | C++的输入输出

目录 0.C与C 1.C的关键字 2.命名空间 2.1域 2.2C中命名冲突问题 2.3命名空间定义 2.4命名空间使用 2.5命令空间的展开&头文件的展开 3.C的输入&输出 3.1cout&cin 3.1<<流插入运算符 3.2>>流提取运算符 0.C与C C是在C的基础之上&#xff…

latex小技巧

目录 如何输入"I"、“II”、“III”、“IV”等大小写罗马数字。 注释&#xff08;单行/多行&#xff09; 单行注释&#xff1a;直接“%” 多行注释&#xff1a; 在TeXstudio: 如何输入"I"、“II”、“III”、“IV”等大小写罗马数字。 \uppercase\expa…

AI-数学-高中-29-样本的数字特征(标准差、方差)

原作者视频&#xff1a;【统计】【一数辞典】3样本的数字特征_哔哩哔哩_bilibili 标准差(s)、方差(S^2)公式&#xff1a;判断数据的稳定性。

数字化转型导师坚鹏:BLM证券公司数字化转型战略

BLM证券公司数字化转型战略 ——以BLM模型为核心&#xff0c;实现知行果合一 课程背景&#xff1a; 很多证券公司存在以下问题&#xff1a; 不知道如何系统地制定证券公司数字化转型战略&#xff1f; 不清楚其它证券公司数字化转型战略是如何制定的&#xff1f; 不知道…

ArmSoM Rockchip系列产品 通用教程 之 Camera 使用

Camera 使用 1. Camera 简介 ArmSoM系列产品使用的是mipi-csi接口的摄像头 ArmSoM-Sige7支持双摄同显&#xff1a; 2. RK3588硬件通路框图 rk3588支持2个isp硬件&#xff0c;每个isp设备可虚拟出多个虚拟节点&#xff0c;软件上通过回读的方式&#xff0c;依次从ddr读取每…

【C语言】熟悉文件顺序读写函数

前言 本篇详细介绍了 文件顺序读写常用函数&#xff0c;快来看看吧~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 前言 ​编辑 文件顺序读写函数 fgetc函数 示例 fputc函数 逐个字符写入 写入26个字母 文…

element-plus+vue3表单含图片(可预览)(线上图片)

一、要实现的效果&#xff1a; 二、如果期间出现这样的效果&#xff08;表格穿透过来了&#xff09;&#xff0c;加上了这行代码就可以了&#xff1a; preview-teleported“true” 如果仅测试用&#xff0c;建议使用线上图片链接的形式&#xff0c;免得本地地址不生效&#xf…

STM32 DMA入门指导

什么是DMA DMA&#xff0c;全称直接存储器访问&#xff08;Direct Memory Access&#xff09;&#xff0c;是一种允许硬件子系统直接读写系统内存的技术&#xff0c;无需中央处理单元&#xff08;CPU&#xff09;的介入。下面是DMA的工作原理概述&#xff1a; 数据传输触发&am…

《Spring Security 简易速速上手小册》第9章 测试与维护 (2024 最新版)

文章目录 9.1 编写安全测试9.1.1 基础知识9.1.2 重点案例&#xff1a;保护 REST API9.1.3 拓展案例 1&#xff1a;自定义登录逻辑测试9.1.4 拓展案例 2&#xff1a;CSRF 保护测试 9.2 Spring Security 升级和维护9.2.1 基础知识9.2.2 重点案例&#xff1a;适配新的密码存储格式…

贪吃蛇(C语言)步骤讲解

一&#xff1a;文章大概 使用C语言在windows环境的控制台中模拟实现经典小游戏 实现基本功能&#xff1a; 1.贪吃蛇地图绘制 2.蛇吃食物的功能&#xff08;上&#xff0c;下&#xff0c;左&#xff0c;右方向控制蛇的动作&#xff09; 3.蛇撞墙死亡 4.计算得分 5.蛇身加…

【数据结构与算法】整数二分

问题描述 对一个排好序的数组&#xff0c;要求找到大于等于7的最小位置和小于等于7的最大位置 大于等于7的最小位置 易知从某个点开始到最右边的边界都满足条件&#xff0c;我们要找到这个区域的最左边的点。 开始二分&#xff01; left指针指向最左边界&#xff0c;right…

STM32单片机示例:ETH_DP83848_DHCP_NonOS_Poll_F407

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

C++ 补充之常用遍历算法

C遍历算法和原理 C标准库提供了丰富的遍历算法&#xff0c;涵盖了各种不同的功能。以下是一些常见的C遍历算法以及它们的概念和原理的简要讲解&#xff1a; for_each&#xff1a;对容器中的每个元素应用指定的函数。 概念&#xff1a;对于给定的容器和一个可调用对象&#xff…

LeetCode 热题 HOT 100(P1~P10)

&#x1f525; LeetCode 热题 HOT 100 这里记录下刷题过程中的心得&#xff0c;其实算法题基本就是个套路问题&#xff0c;很多时候你不知道套路或者模板&#xff0c;第一次尝试去做的时候就会非常懵逼、沮丧和无助。而且就算你一时理解并掌握了&#xff0c;过一段时间往往会绝…

#WEB前端(CSS基础)

1.实验&#xff1a;HTML是网页骨架&#xff0c;CCS是网页装修 2.IDE&#xff1a;VSCODE 3.记录&#xff1a; style 4.代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"view…

为什么模电这么难学?这是我见过最好的回答

大家好&#xff0c;我是砖一&#xff0c;有很多人抱怨模电难学&#xff0c;被誉为电子信息挂科率最高之一&#xff0c;下面听我分析一下为啥模电这么难学&#xff1f; 01 理科的抽象思维 在高等教育体系中&#xff0c;模电是涉及半导体方向的第一门工程类课程&#xff0c;是一…

Unity中URP下实现水体(C#动态生成渐变图)

文章目录 前言一、Shader部分1、申明水渐变图纹理和采样器2、在片元着色器&#xff0c;进行纹理采样&#xff0c;并且输出 二、C#脚本部分1、我们新建一个C#脚本2、我们定义两个变量3、在Start内&#xff0c;new 一个Texture2D(宽&#xff0c;高)4、定义一个Color[宽*高]的颜色…