0


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

简介

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

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

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

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

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

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

1.快速入门

1.1.引入依赖

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

如果是gradle则使用

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

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

image-20240217123503799

image-20240217123523370

image-20240217123722190

2.SpringBoot整合Redis

我是在Windos环境下安装Redis,这里在Windows下启动Redis 需要

进入到安装目录库

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

image-20240217124753947

2.1.引入依赖

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

2.2.配置Redis

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

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

2.3.使用Redis Template

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

主要是为了 统一管理

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

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

importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.BoundSetOperations;importorg.springframework.data.redis.core.HashOperations;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.stereotype.Component;importjava.util.*;importjava.util.concurrent.TimeUnit;@ComponentpublicclassRedisCache{@AutowiredpublicRedisTemplate redisTemplate;/**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */public<T>voidsetCacheObject(finalString key,finalT value){
        redisTemplate.opsForValue().set(key, value);}/**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */public<T>voidsetCacheObject(finalString key,finalT value,finalInteger timeout,finalTimeUnit timeUnit){
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */publicbooleanexpire(finalString key,finallong timeout){returnexpire(key, timeout,TimeUnit.SECONDS);}/**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */publicbooleanexpire(finalString key,finallong timeout,finalTimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */public<T>TgetCacheObject(finalString key){ValueOperations<String,T> operation = redisTemplate.opsForValue();return operation.get(key);}/**
     * 删除单个对象
     *
     * @param key
     */publicbooleandeleteObject(finalString key){return redisTemplate.delete(key);}/**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */publiclongdeleteObject(finalCollection collection){return redisTemplate.delete(collection);}/**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */public<T>longsetCacheList(finalString key,finalList<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count ==null?0: count;}/**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */public<T>List<T>getCacheList(finalString key){return redisTemplate.opsForList().range(key,0,-1);}/**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */public<T>BoundSetOperations<String,T>setCacheSet(finalString key,finalSet<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(finalString key){return redisTemplate.opsForSet().members(key);}/**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */public<T>voidsetCacheMap(finalString key,finalMap<String,T> dataMap){if(dataMap !=null){
            redisTemplate.opsForHash().putAll(key, dataMap);}}/**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */public<T>Map<String,T>getCacheMap(finalString key){return redisTemplate.opsForHash().entries(key);}/**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */public<T>voidsetCacheMapValue(finalString key,finalString hKey,finalT value){
        redisTemplate.opsForHash().put(key, hKey, value);}/**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */public<T>TgetCacheMapValue(finalString key,finalString hKey){HashOperations<String,String,T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}publicvoidincrementCacheMapValue(String key,String hKey,int v){
        redisTemplate.opsForHash().increment(key, hKey, v);}/**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */publicvoiddelCacheMapValue(finalString key,finalString hkey){HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);}/**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */public<T>List<T>getMultiCacheMapValue(finalString key,finalCollection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */publicCollection<String>keys(finalString pattern){return redisTemplate.keys(pattern);}}
2.3.3.测试

测试是否能正常使用

@RequestMapping("/redis")publicStringredis(){
        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
 */publicclassTokenUtil{/**
     * 过期时间(单位:秒)
     */publicstaticfinalintACCESS_EXPIRE=60*60*60;/**
     * 加密算法
     */privatefinalstaticSecureDigestAlgorithm<SecretKey,SecretKey>ALGORITHM=Jwts.SIG.HS256;/**
     * 私钥 / 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取。
     * 切记:秘钥不能外露,在任何场景都不应该流露出去。
     * 应该大于等于 256位(长度32及以上的字符串),并且是随机的字符串
     */publicfinalstaticStringSECRET="secrasdddddddddddddddddddddddddddddddddwqeqeqwewqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqetKey";/**
     * 秘钥实例
     */publicstaticfinalSecretKeyKEY=Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));/**
     * jwt签发者
     */privatefinalstaticStringJWT_ISS="Tiam";/**
     * jwt主题
     */privatefinalstaticStringSUBJECT="Peripherals";/**
     * 生成访问令牌
     *
     * @param username 用户名
     * @return 访问令牌
     */publicstaticStringgenAccessToken(String username){// 生成令牌IDString uuid =UUID.randomUUID().toString();// 设置过期时间Date expireDate =Date.from(Instant.now().plusSeconds(ACCESS_EXPIRE));returnJwts.builder()// 设置头部信息.header().add("typ","JWT").add("alg","HS256").and()// 设置自定义负载信息.claim("username", username).id(uuid)// 令牌ID.expiration(expireDate)// 过期日期.issuedAt(newDate())// 签发时间.subject(SUBJECT)// 主题.issuer(JWT_ISS)// 签发者.signWith(KEY,ALGORITHM)// 签名.compact();}/**
     * 获取payload中的用户信息
     *
     * @param token JWT Token
     * @return 用户信息
     */publicstaticStringgetUserFromToken(String token){String user ="";Claims claims =parseClaims(token);if(claims !=null){
            user =(String) claims.get("username");}return user;}/**
     * 获取JWT令牌的过期时间
     *
     * @param token JWT令牌
     * @return 过期时间的毫秒级时间戳
     */publicstaticlonggetExpirationTime(String token){Claims claims =parseClaims(token);if(claims !=null){return claims.getExpiration().getTime();}return0L;}/**
     * 解析token
     *
     * @param token token
     * @return Jws<Claims>
     */publicstaticJws<Claims>parseClaim(String token){returnJwts.parser().verifyWith(KEY).build().parseSignedClaims(token);}/**
     * 解析token的头部信息
     *
     * @param token token
     * @return token的头部信息
     */publicstaticJwsHeaderparseHeader(String token){returnparseClaim(token).getHeader();}/**
     * 解析token的载荷信息
     *
     * @param token token
     * @return token的载荷信息
     */publicstaticClaimsparsePayload(String token){returnparseClaim(token).getPayload();}/**
     * 解析JWT Token中的Claims
     *
     * @param token JWT Token
     * @return Claims
     */publicstaticClaimsparseClaims(String token){try{returnJwts.parser().setSigningKey(KEY).build().parseClaimsJws(token).getBody();}catch(Exception e){returnnull;}}}

3.3.测试

@RequestMapping("/jjwt")publicMap<String,String>jjwt(){Map<String,String> map =newHashMap<>();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.创建权限表

CREATETABLE"hr_manager"."t_sys_my_permission"("sid"VARCHAR(50)COLLATE"pg_catalog"."default"NOTNULL,"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"PRIMARYKEY("sid"));ALTERTABLE"hr_manager"."t_sys_my_permission" OWNER TO"postgres";COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."sid"IS'主键SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."parent_id"IS'父节点ID';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."parent_name"IS'父节点名称';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."permission_name"IS'权限名称';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."permission_code"IS'授权标识符';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."router_path"IS'路由地址';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."router_name"IS'路由名称';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."auth_url"IS'授权路径(对应文件在项目的地址)';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."order_no"IS'序号(用于排序)';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."type"IS'类型 0 目录 1 菜单 2 按钮';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."icon"IS'图标';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."remark"IS'备注';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."insert_user"IS'创建人';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."insert_time"IS'创建时间';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."update_user"IS'更新人';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."update_time"IS'更新时间';COMMENTONCOLUMN"hr_manager"."t_sys_my_permission"."license_code"IS'许可标识';

4.1.3.创建角色表

CREATETABLE"hr_manager"."t_sys_my_role"("sid"VARCHAR(50)COLLATE"pg_catalog"."default"NOTNULL,"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"PRIMARYKEY("sid"));ALTERTABLE"hr_manager"."t_sys_my_role" OWNER TO"postgres";COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."sid"IS'主键SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."role_name"IS'角色名称';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."remark"IS'备注';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."insert_user"IS'创建人';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."insert_time"IS'创建时间';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."update_user"IS'更新人';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."update_time"IS'更新时间';COMMENTONCOLUMN"hr_manager"."t_sys_my_role"."status"IS'是否使用 0 禁用 1 使用';

4.1.4.创建用户角色表

CREATETABLE"hr_manager"."t_sys_my_user_role"("sid"VARCHAR(50)COLLATE"pg_catalog"."default"NOTNULL,"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"PRIMARYKEY("sid"));ALTERTABLE"hr_manager"."t_sys_my_user_role" OWNER TO"postgres";COMMENTONCOLUMN"hr_manager"."t_sys_my_user_role"."sid"IS'主键SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_user_role"."role_sid"IS'角色SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_user_role"."user_sid"IS'用户SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_user_role"."insert_user"IS'创建人';COMMENTONCOLUMN"hr_manager"."t_sys_my_user_role"."insert_time"IS'创建时间';

4.1.5.创建角色权限表

CREATETABLE"hr_manager"."t_sys_my_role_permission"("sid"VARCHAR(50)COLLATE"pg_catalog"."default"NOTNULL,"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"PRIMARYKEY("sid"));ALTERTABLE"hr_manager"."t_sys_my_role_permission" OWNER TO"postgres";COMMENTONCOLUMN"hr_manager"."t_sys_my_role_permission"."sid"IS'主键SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_role_permission"."role_sid"IS'角色SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_role_permission"."permission_sid"IS'权限SID';COMMENTONCOLUMN"hr_manager"."t_sys_my_role_permission"."insert_user"IS'创建人';COMMENTONCOLUMN"hr_manager"."t_sys_my_role_permission"."insert_time"IS'创建时间';

4.2.创建实体类

4.2.1.创建用户实体类

@DatapublicclassSysMyUserimplementsSerializable,UserDetails{privatestaticfinallong serialVersionUID =1L;@TableId/**
     * sid
     */privateString sid;/**
     * user_no
     */privateString userNo;/**
     * user_name
     */privateString userName;/**
     * password
     */privateString password;/**
     * nick_name
     */privateString nickName;/**
     * phone_number
     */privateString phoneNumber;/**
     * email
     */privateString email;/**
     * department_id
     */privateString departmentId;/**
     * department_name
     */privateString departmentName;/**
     * is_admin
     */privateString isAdmin;/**
     * sex
     */privateString sex;/**
     * post_id
     */privateString postId;/**
     * post_name
     */privateString postName;/**
     * is_account_non_expired
     */privateBoolean isAccountNonExpired;/**
     * is_account_non_locked
     */privateBoolean isAccountNonLocked;/**
     * is_credentials_non_expired
     */privateBoolean isCredentialsNonExpired;/**
     * is_enabled
     */privateBoolean isEnabled;/**
     * insert_user
     */privateString insertUser;/**
     * insert_time
     */privateString insertTime;/**
     * update_user
     */privateString updateUser;/**
     * update_time
     */privateString updateTime;/**
     * license_code
     */privateString licenseCode;/**
     * 权限列表 就是菜单列表
     */@TableField(exist =false)privateList<SysMyPermission> permissionList;/**
     * 认证信息 就是用户配置code
     */@TableField(exist =false)Collection<?extendsGrantedAuthority> authorities;/**
     * 用户权限信息
     */@TableField(exist =false)privateList<String> roles;@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){return authorities;}@OverridepublicStringgetUsername(){returnthis.userNo;}@OverridepublicStringgetPassword(){returnthis.password;}@OverridepublicbooleanisAccountNonExpired(){returnthis.isAccountNonExpired;}@OverridepublicbooleanisAccountNonLocked(){returnthis.isAccountNonLocked;}@OverridepublicbooleanisCredentialsNonExpired(){returnthis.isCredentialsNonExpired;}@OverridepublicbooleanisEnabled(){returnthis.isEnabled;}}

4.2.2.创建权限实体类

@DatapublicclassSysMyPermissionimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@TableId/**
    * sid
    */privateString sid;/**
    * parent_id
    */privateString parentId;/**
    * parent_name
    */privateString parentName;/**
    * permission_name
    */privateString permissionName;/**
    * permission_code
    */privateString permissionCode;/**
    * router_path
    */privateString routerPath;/**
    * router_name
    */privateString routerName;/**
    * auth_url
    */privateString authUrl;/**
    * order_no
    */privateString orderNo;/**
    * type
    */privateString type;/**
    * icon
    */privateString icon;/**
    * remark
    */privateString remark;/**
    * insert_user
    */privateString insertUser;/**
    * insert_time
    */privateString insertTime;/**
    * update_user
    */privateString updateUser;/**
    * update_time
    */privateString updateTime;/**
    * license_code
    */privateString licenseCode;/**
     * 菜单的子集合
     */@TableField(exist =false)@JsonInclude(JsonInclude.Include.NON_NULL)privateList<SysMyPermission> children =newArrayList<>();}

4.3.创建Service和Dao

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

4.3.1.UserService

@ServicepublicclassSysMyUserService{@ResourceprivateSysMyUserMapper userMapper;/**
     * 根据用户id获取用户信息(包含用户具备的权限信息)
     * @param username 用户信息
     * @return
     */publicSysMyUsergetUserInfoByUserId(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 加密后的密码
     */publicStringgetEncoderPassword(String password){BCryptPasswordEncoder encoder =newBCryptPasswordEncoder(10);String encodePassword = encoder.encode(password);return encodePassword;}}

4.3.2.UserMapper

@RepositorypublicinterfaceSysMyUserMapperextendsBaseMapper<SysMyUser>{/**
     * 根据用户名账号获取用户信息
     * @param username 用户信息
     * @return 用户信息
     */SysMyUsergetUserInfoByUserId(@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"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.sys.my.core.user.dao.SysMyUserMapper"><!-- 根据用户名账号获取用户信息 --><selectid="getUserInfoByUserId"resultType="com.sys.my.core.user.model.SysMyUser">
        select * from t_sys_my_user u where u.user_no = #{username};
    </select><!-- 根据用户id获取用户具备的权限信息 --><selectid="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

@ServicepublicclassSysMyPermissionService{privatestaticfinalLogger logger =LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());@ResourceprivateSysMyPermissionMapper sysMyPermissionMapper;/**
     * 根据用户id查询对应的权限
     * @param userId 用户id
     * @return 权限列表
     */publicList<SysMyPermission>getPermissionListByUserId(String userId){// 根据用户ID获取用户对应的权限return sysMyPermissionMapper.getMenuListByUserId(userId);}}

4.3.5.SysMyPermissionMapper

@RepositorypublicinterfaceSysMyPermissionMapperextendsBaseMapper<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 负责从数据源(通常是数据库)中加载用户信息,包括用户名、密码和权限等,以便进行身份验证。

通常情况下,我们需要重写

UserDetailsService

loadUserByUsername()

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

/**
 * 自定义UserDetailsService 用于认证和授权
 * 此处把用户的信息和权限交给spring security
 * spring security会对用户的信息和权限信息进行管理
 * @author hffan
 * serDetailService接口主要定义了一个方法 l
 * oadUserByUsername(String username)用于完成用户信息的查询,
 * 其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接口,
 * 完成数据库查询,该接口返回UserDetail。
 */@Component("customerUserDetailsService")publicclassCustomerUserDetailsServiceimplementsUserDetailsService{@ResourceprivateSysMyUserService userService;@ResourceprivateSysMyPermissionService permissionService;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{SysMyUser user = userService.getUserInfoByUserId(username);// 如果用户不存在if(user ==null){thrownewUsernameNotFoundException("用户名或者密码错误");}// 根据用户id查询用户权限List<SysMyPermission> permissionList = permissionService.getPermissionListByUserId(user.getSid());// 取出权限中配置codeList<String> collect = permissionList.stream().filter(item -> item !=null).map(item -> item.getPermissionCode()).filter(item -> item !=null).collect(Collectors.toList());// 转为数据String[] strings = collect.toArray(newString[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异常
 */publicclassTokenExceptionextendsAuthenticationException{publicTokenException(String msg){super(msg);}}

4.5.2.用户认证自定义异常

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

4.6.编写自定义处理器

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

4.6.1.匿名用户访问处理器

AuthenticationEntryPoint

  • 作用:AuthenticationEntryPoint 用于处理用户尝试访问受保护资源但未进行身份验证的情况。当用户尝试访问需要身份验证的资源但尚未进行身份验证时,AuthenticationEntryPoint 将被调用来触发身份验证流程。
  • 详细讲解:当用户尝试访问安全受保护的资源但未进行身份验证时,AuthenticationEntryPoint 的 commence() 方法将被调用。在这个方法中,我们可以定制返回响应给用户,例如重定向到登录页面或返回401未授权错误等。
/**
 * 匿名用户访问资源处理器
 */@Component("loginAuthenticationHandler")publicclassLoginAuthenticationHandlerimplementsAuthenticationEntryPoint{@Overridepublicvoidcommence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException)throwsIOException,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")publicclassLoginAccessDefineHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException accessDeniedException)throwsIOException,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")publicclassLoginFiledHandlerimplementsAuthenticationFailureHandler{@OverridepublicvoidonAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,AuthenticationException exception)throwsIOException,ServletException{//1.设置响应编码
        response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String str =null;int code =500;if(exception instanceofAccountExpiredException){
            str ="账户过期,登录失败!";}elseif(exception instanceofBadCredentialsException){
            str ="用户名或密码错误,登录失败!";}elseif(exception instanceofCredentialsExpiredException){
            str ="密码过期,登录失败!";}elseif(exception instanceofDisabledException){
            str ="账户被禁用,登录失败!";}elseif(exception instanceofLockedException){
            str ="账户被锁,登录失败!";}elseif(exception instanceofInternalAuthenticationServiceException){
            str ="账户不存在,登录失败!";}elseif(exception instanceofCustomerAuthenionException){//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")publicclassLoginSuccessHandlerimplementsAuthenticationSuccessHandler{@ResourceprivateRedisCache redisCache;@OverridepublicvoidonAuthenticationSuccess(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Authentication authentication)throwsIOException,ServletException{SysMyUser user =(SysMyUser)authentication.getPrincipal();// 登录成功处理//1.生成tokenString token =TokenUtil.genAccessToken(user.getUsername());long expireTime =TokenUtil.getExpirationTime(token);// 配置一下返回给前端的token信息LoginResultObject vo =newLoginResultObject();// 将实体类信息转为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)publicclassCheckTokenFilterextendsOncePerRequestFilter{@Value("${hrfan.login.url}")privateString loginUrl;@AutowiredprivateLoginFiledHandler loginFailureHandler;@AutowiredprivateCustomerUserDetailsService customerUserDetailsService;@ResourceprivateRedisCache redisCache;@OverrideprotectedvoiddoFilterInternal(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,FilterChain filterChain)throwsServletException,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);}elseif(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验证privatevoidvalidateToken(HttpServletRequest request){//从请求的头部获取tokenString token = request.getHeader("token");//如果请求头部没有获取到token,则从请求参数中获取tokenif(StringUtils.isEmpty(token)){
            token = request.getParameter("token");}if(StringUtils.isEmpty(token)){// 请求参数中也没有 那就从redis中进行获取根据ip地址取
            token = redisCache.getCacheObject(request.getRemoteAddr());}if(StringUtils.isEmpty(token)){thrownewCustomerAuthenionException("token不存在!");}//解析tokenString username =TokenUtil.getUserFromToken(token);if(StringUtils.isEmpty(username)){thrownewCustomerAuthenionException("token解析失败!");}//获取用户信息UserDetails user = customerUserDetailsService.loadUserByUsername(username);if(user ==null){thrownewCustomerAuthenionException("token验证失败!");}UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
        authenticationToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(request));//设置到spring security上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);}}

4.8.设置登录返回信息

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

/**
 * 登录返回信息
 */@DatapublicclassLoginResultObject{privateString token;//token过期时间privateLong expireTime;privateSysMyUser userInfo;privateLong code;}

4.9.编写SpringSecurity配置

#### 注意
    因为新版本的SpringSecurity和旧版本的差距较大,所以这里保留了旧版本的写法
    我使用的SpringBoot 和 SpringSecurity 版本都是相对较新的 3.1.8版本 JDK版本是21
importcom.sys.my.config.security.details_service.CustomerUserDetailsService;importcom.sys.my.config.security.filter.CheckTokenFilter;importcom.sys.my.config.security.handler.LoginAccessDefineHandler;importcom.sys.my.config.security.handler.LoginAuthenticationHandler;importcom.sys.my.config.security.handler.LoginFiledHandler;importcom.sys.my.config.security.handler.LoginSuccessHandler;importjakarta.annotation.Resource;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;importorg.springframework.web.cors.CorsConfiguration;importorg.springframework.web.cors.CorsConfigurationSource;importorg.springframework.web.cors.UrlBasedCorsConfigurationSource;importjava.util.Collections;/**
 * SpringSecurity配置类
 */@Configuration@EnableWebSecurity//启用Spring SecuritypublicclassSpringSecurityConfig{@ResourceprivateCustomerUserDetailsService customerUserDetailsService;@ResourceprivateLoginSuccessHandler loginSuccessHandler;@ResourceprivateLoginFiledHandler loginFiledHandler;@ResourceprivateLoginAuthenticationHandler loginAuthenticationHandler;@ResourceprivateLoginAccessDefineHandler loginAccessDefineHandler;@ResourceprivateCheckTokenFilter checkTokenFilter;/**
     * 密码处理
     * @return
     */@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}/**
     * 新版的实现方法不再和旧版一样在配置类里面重写方法,而是构建了一个过滤链对象并通过@Bean注解注入到IOC容器中
     * 新版整体代码 (注意:新版AuthenticationManager认证管理器默认全局)
     * @param http http安全配置
     * @return SecurityFilterChain
     * @throws Exception 异常
     */@BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurity http)throwsException{
        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();//// }/**
     * 跨域配置
     */CorsConfigurationSourceconfigurationSource(){CorsConfiguration corsConfiguration =newCorsConfiguration();
        corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
        corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
        corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
        corsConfiguration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source =newUrlBasedCorsConfigurationSource();
        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')")publicMap<String,String>jjwt(){// 这里的user_list 就是我们权限中permission_codethrownewRuntimeException("测试无token访问!");}

image-20240218002547214

5.4.测试不登陆访问

image-20240218002712121

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

image-20240218002909117

image-20240218004426367

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

image-20240218004734952

image-20240218004839439

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

image-20240218091500059


本文转载自: https://blog.csdn.net/q1372302825/article/details/136421121
版权归原作者 爱编程的饭团 所有, 如有侵权,请联系我们删除。

“【Spring Boot 3】的安全防线:整合 【Spring Security 6】”的评论:

还没有评论