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

简介

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

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

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

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

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

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

1.快速入门

1.1.引入依赖

xml 复制代码
<!-- 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则使用

shell 复制代码
// 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,密码是在项目启动时,输出在控制台

2.SpringBoot整合Redis

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

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

2.1.引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.1.8</version>
</dependency>
shell 复制代码
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.1.8'

2.2.配置Redis

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

yaml 复制代码
# redis相关配置 
spring:
  data:
    redis:
      port: 6379
      host: 127.0.0.1

2.3.使用Redis Template

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

主要是为了 统一管理

java 复制代码
@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工具类

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

java 复制代码
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.测试

测试是否能正常使用

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

3.SpringBoot整合JJWT

3.1.引入依赖

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

3.2.JJW工具类

为了方便使用,我们将其封装成一个工具类

由于使用的版本是新版本的JDK 以及 JJWT所以网 这里的工具类 写法会有些出入

java 复制代码
/**
 * 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.测试

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

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表用于关联角色与权限。
postgresql 复制代码
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.创建权限表

sql 复制代码
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.创建角色表

sql 复制代码
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.创建用户角色表

sql 复制代码
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.创建角色权限表

sql 复制代码
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.创建用户实体类

java 复制代码
@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.创建权限实体类

java 复制代码
@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

java 复制代码
@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

java 复制代码
@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 复制代码
<?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

java 复制代码
@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

java 复制代码
@Repository
public interface SysMyPermissionMapper extends BaseMapper<SysMyPermission> {

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

4.3.6.SysMyPermissionMapper.xml

java 复制代码
<?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 对象,该对象包含了与用户名对应的用户信息。在实际开发中,我们可能需要自定义的用户信息存储方式,或者希望在加载用户信息时进行一些特定的逻辑处理,比如自定义密码加密方式、从数据库或其他数据源加载用户信息等。

java 复制代码
/**
 * 自定义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认证自定义异常

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

4.5.2.用户认证自定义异常

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

4.6.编写自定义处理器

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

4.6.1.匿名用户访问处理器

AuthenticationEntryPoint

  • 作用:AuthenticationEntryPoint 用于处理用户尝试访问受保护资源但未进行身份验证的情况。当用户尝试访问需要身份验证的资源但尚未进行身份验证时,AuthenticationEntryPoint 将被调用来触发身份验证流程。
  • 详细讲解:当用户尝试访问安全受保护的资源但未进行身份验证时,AuthenticationEntryPoint 的 commence() 方法将被调用。在这个方法中,我们可以定制返回响应给用户,例如重定向到登录页面或返回401未授权错误等。
java 复制代码
/**
 * 匿名用户访问资源处理器
 */
@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() 方法在访问被拒绝时被调用。我们可以在这个方法中定义自定义的行为,例如返回自定义的错误页面、向用户发送通知或记录拒绝的访问尝试。
java 复制代码
/**
 * 认证用户访问无权限处理器
 */
@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() 方法在身份验证失败时被调用。我们可以在这个方法中执行自定义的行为,例如记录登录失败次数、向用户发送通知或返回自定义的错误页面。
java 复制代码
@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() 方法在身份验证成功时被调用。我们可以在这个方法中执行自定义的行为,例如记录登录成功的日志、向用户发送欢迎消息或重定向到特定页面。
java 复制代码
/**
 * 自定义认证成功处理器
 */
@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),将请求传递给下一个过滤器处理。
java 复制代码
@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

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

4.9.编写SpringSecurity配置

markdown 复制代码
#### 注意
	因为新版本的SpringSecurity和旧版本的差距较大,所以这里保留了旧版本的写法
	我使用的SpringBoot 和 SpringSecurity 版本都是相对较新的 3.1.8版本 JDK版本是21
java 复制代码
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.配置文件配置

yaml 复制代码
hrfan:
  login:
    url: "/api/v1/user/login"

5.测试

5.1.测试登录密码错误

5.2.测试正确密码

5.3.测试无token访问接口

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

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

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

5.4.测试不登陆访问

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

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

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

相关推荐
用户9623779544815 小时前
VulnHub DC-3 靶机渗透测试笔记
安全
叶落阁主2 天前
Tailscale 完全指南:从入门到私有 DERP 部署
运维·安全·远程工作
用户908324602732 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840823 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解3 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解3 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记3 天前
Spring Boot Web MVC配置详解
spring boot·后端
用户962377954484 天前
DVWA 靶场实验报告 (High Level)
安全
初次攀爬者4 天前
Kafka 基础介绍
spring boot·kafka·消息队列
数据智能老司机4 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent