簡介
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,密碼是在項目啟動時,輸出在控制臺
2.SpringBoot整合Redis
我是在Windos環境下安裝Redis,這里在Windows下啟動Redis 需要
進入到安裝目錄庫
輸入 redis-server.exe redis.windows.conf
2.1.引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>3.1.8</version>
</dependency>
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.1.8'
2.2.配置Redis
在配置文件中對redis進行配置
# redis相關配置
spring:data:redis:port: 6379host: 127.0.0.1
2.3.使用Redis Template
2.3.1.將Redis Template注入到Spring容器中
主要是為了 統一管理
@Configuration
public class RedisTemplateConfig {@Bean("sysMyRedisTemplate")public <T> RedisTemplate<String, T> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);RedisSerializer<String> redisSerializer = new StringRedisSerializer();ObjectMapper om = new ObjectMapper();// 持久化改動.設置可見性,om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// 持久化改動.非final類型的對象,把對象類型也序列化進去,以便反序列化推測正確的類型om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// 持久化改動.null字段不顯示om.setSerializationInclusion(JsonInclude.Include.NON_NULL);// 持久化改動.POJO無public屬性或方法時不報錯om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);// 持久化改動.setObjectMapper方法移除.使用構造方法傳入ObjectMapperGenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);redisTemplate.setKeySerializer(redisSerializer);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashKeySerializer(redisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
2.3.2.RedisTemplate工具類
為了方便使用,可以封裝一下工具類進行使用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;@Component
public class RedisCache {@Autowiredpublic RedisTemplate redisTemplate;/*** 緩存基本的對象,Integer、String、實體類等** @param key 緩存的鍵值* @param value 緩存的值*/public <T> void setCacheObject(final String key, final T value) {redisTemplate.opsForValue().set(key, value);}/*** 緩存基本的對象,Integer、String、實體類等** @param key 緩存的鍵值* @param value 緩存的值* @param timeout 時間* @param timeUnit 時間顆粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 設置有效時間** @param key Redis鍵* @param timeout 超時時間* @return true=設置成功;false=設置失敗*/public boolean expire(final String key, final long timeout) {return expire(key, timeout, TimeUnit.SECONDS);}/*** 設置有效時間** @param key Redis鍵* @param timeout 超時時間* @param unit 時間單位* @return true=設置成功;false=設置失敗*/public boolean expire(final String key, final long timeout, final TimeUnit unit) {return redisTemplate.expire(key, timeout, unit);}/*** 獲得緩存的基本對象。** @param key 緩存鍵值* @return 緩存鍵值對應的數據*/public <T> T getCacheObject(final String key) {ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 刪除單個對象** @param key*/public boolean deleteObject(final String key) {return redisTemplate.delete(key);}/*** 刪除集合對象** @param collection 多個對象* @return*/public long deleteObject(final Collection collection) {return redisTemplate.delete(collection);}/*** 緩存List數據** @param key 緩存的鍵值* @param dataList 待緩存的List數據* @return 緩存的對象*/public <T> long setCacheList(final String key, final List<T> dataList) {Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 獲得緩存的list對象** @param key 緩存的鍵值* @return 緩存鍵值對應的數據*/public <T> List<T> getCacheList(final String key) {return redisTemplate.opsForList().range(key, 0, -1);}/*** 緩存Set** @param key 緩存鍵值* @param dataSet 緩存的數據* @return 緩存數據的對象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()) {setOperation.add(it.next());}return setOperation;}/*** 獲得緩存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key) {return redisTemplate.opsForSet().members(key);}/*** 緩存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 獲得緩存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key) {return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入數據** @param key Redis鍵* @param hKey Hash鍵* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value) {redisTemplate.opsForHash().put(key, hKey, value);}/*** 獲取Hash中的數據** @param key Redis鍵* @param hKey Hash鍵* @return Hash中的對象*/public <T> T getCacheMapValue(final String key, final String hKey) {HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}public void incrementCacheMapValue(String key, String hKey, int v) {redisTemplate.opsForHash().increment(key, hKey, v);}/*** 刪除Hash中的數據** @param key* @param hkey*/public void delCacheMapValue(final String key, final String hkey) {HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 獲取多個Hash中的數據** @param key Redis鍵* @param hKeys Hash鍵集合* @return Hash對象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 獲得緩存的基本對象列表** @param pattern 字符串前綴* @return 對象列表*/public Collection<String> keys(final String pattern) {return redisTemplate.keys(pattern);}
}
2.3.3.測試
測試是否能正常使用
@RequestMapping("/redis")public String redis(){redisCache.setCacheObject("test", "test");return redisCache.getCacheObject("test").toString();}
3.SpringBoot整合JJWT
3.1.引入依賴
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.12.5</version>
</dependency>
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.12.5'
3.2.JJW工具類
為了方便使用,我們將其封裝成一個工具類
由于使用的版本是新版本的JDK 以及 JJWT所以網 這里的工具類 寫法會有些出入
/*** JWT Token工具類,用于生成和解析JWT Token** @Author: Tiam* @Date: 2023/10/23 16:38*/
public class TokenUtil {/*** 過期時間(單位:秒)*/public static final int ACCESS_EXPIRE = 60 * 60 * 60;/*** 加密算法*/private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;/*** 私鑰 / 生成簽名的時候使用的秘鑰secret,一般可以從本地配置文件中讀取。* 切記:秘鑰不能外露,在任何場景都不應該流露出去。* 應該大于等于 256位(長度32及以上的字符串),并且是隨機的字符串*/public final static String SECRET = "secrasdddddddddddddddddddddddddddddddddwqeqeqwewqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqetKey";/*** 秘鑰實例*/public static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));/*** jwt簽發者*/private final static String JWT_ISS = "Tiam";/*** jwt主題*/private final static String SUBJECT = "Peripherals";/*** 生成訪問令牌** @param username 用戶名* @return 訪問令牌*/public static String genAccessToken(String username) {// 生成令牌IDString uuid = UUID.randomUUID().toString();// 設置過期時間Date expireDate = Date.from(Instant.now().plusSeconds(ACCESS_EXPIRE));return Jwts.builder()// 設置頭部信息.header().add("typ", "JWT").add("alg", "HS256").and()// 設置自定義負載信息.claim("username", username).id(uuid) // 令牌ID.expiration(expireDate) // 過期日期.issuedAt(new Date()) // 簽發時間.subject(SUBJECT) // 主題.issuer(JWT_ISS) // 簽發者.signWith(KEY, ALGORITHM) // 簽名.compact();}/*** 獲取payload中的用戶信息** @param token JWT Token* @return 用戶信息*/public static String getUserFromToken(String token) {String user = "";Claims claims = parseClaims(token);if (claims != null) {user = (String) claims.get("username");}return user;}/*** 獲取JWT令牌的過期時間** @param token JWT令牌* @return 過期時間的毫秒級時間戳*/public static long getExpirationTime(String token) {Claims claims = parseClaims(token);if (claims != null) {return claims.getExpiration().getTime();}return 0L;}/*** 解析token** @param token token* @return Jws<Claims>*/public static Jws<Claims> parseClaim(String token) {return Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token);}/*** 解析token的頭部信息** @param token token* @return token的頭部信息*/public static JwsHeader parseHeader(String token) {return parseClaim(token).getHeader();}/*** 解析token的載荷信息** @param token token* @return token的載荷信息*/public static Claims parsePayload(String token) {return parseClaim(token).getPayload();}/*** 解析JWT Token中的Claims** @param token JWT Token* @return Claims*/public static Claims parseClaims(String token) {try {return Jwts.parser().setSigningKey(KEY).build().parseClaimsJws(token).getBody();} catch (Exception e) {return null;}}
}
3.3.測試
@RequestMapping("/jjwt")public Map<String, String> jjwt(){Map<String, String> map = new HashMap<>();String tokenByKey = TokenUtil.genAccessToken("hrfan");map.put("encoding", tokenByKey);return map;}
4.實戰
背景
在企業開發中,一個安全的登錄授權系統是至關重要的,它不僅可以保護用戶的隱私信息,還能夠確保只有經過授權的用戶才能夠訪問特定的資源和功能。這樣的系統不僅僅是為了滿足用戶的安全需求,也是為了保護企業的敏感數據和資源免受未經授權的訪問和惡意攻擊。
首先,一個安全的登錄授權系統必須具備可靠的身份驗證機制。用戶需要能夠通過輸入憑據(通常是用戶名和密碼)來驗證其身份。這個過程需要保證用戶的密碼被安全地存儲,并且在傳輸過程中使用加密技術保障用戶憑據的安全性。
其次,授權系統需要根據用戶的身份和角色來管理用戶對資源和功能的訪問權限。不同的用戶可能具有不同的角色和權限,例如普通用戶、管理員、審計員等。系統需要根據用戶的角色和權限來限制他們對資源的訪問,以確保敏感數據不會被未經授權的用戶獲取。
下面使用SpringSecurity來實現一個簡易的登錄認證
用戶身份驗證
- 登錄頁面: 我們需要一個登錄頁面,用戶可以在該頁面輸入他們的憑據以進行身份驗證。登錄頁面應該友好且易于理解。
- 身份驗證: 用戶的用戶名和密碼應該被驗證,只有在驗證通過后才能進入系統。密碼應該以安全的方式存儲,例如使用哈希算法加密存儲。
- 認證失敗處理: 如果用戶提供的憑據無效,則系統應該向用戶提供相應的錯誤消息,并允許他們再次嘗試登錄。
訪問控制
- 受保護資源: 我們的系統將有一些受保護的資源和功能,例如管理課程、學生信息等。只有經過身份驗證的用戶才能訪問這些資源。
- 角色和權限: 不同類型的用戶應該有不同的角色和權限。例如,管理員可能具有管理課程和學生的權限,而普通用戶可能只能訪問課程內容。
- 未經授權的訪問: 如果用戶嘗試訪問他們沒有權限的資源,則系統應該拒絕訪問,并向用戶顯示適當的錯誤消息。
安全性
- 防范攻擊: 我們的系統應該能夠防范常見的安全攻擊,如跨站腳本攻擊、SQL注入等。
- 密碼安全: 用戶的密碼不應以明文形式存儲在數據庫中,而應該使用安全的加密算法進行存儲。
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表用于關聯角色與權限。
CREATE TABLE "hr_manager"."t_sys_my_user" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"user_no" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","user_name" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","password" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","nick_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","phone_number" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","email" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","department_id" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","department_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","is_admin" VARCHAR ( 1 ) COLLATE "pg_catalog"."default","sex" VARCHAR ( 1 ) COLLATE "pg_catalog"."default","post_id" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","post_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","is_account_non_expired" bool,"is_account_non_locked" bool,"is_credentials_non_expired" bool,"is_enabled" bool,"insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,"update_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","update_time" DATE,"license_code" VARCHAR ( 20 ) COLLATE "pg_catalog"."default",CONSTRAINT "t_sys_my_user_pkey" PRIMARY KEY ( "sid" )
);
ALTER TABLE "hr_manager"."t_sys_my_user" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."sid" IS '主鍵SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."user_no" IS '用戶登錄賬號';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."user_name" IS '用戶名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."password" IS '用戶密碼';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."nick_name" IS '用戶昵稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."phone_number" IS '手機號碼';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."email" IS '郵箱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."department_id" IS '部門ID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."department_name" IS '部門名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_admin" IS '是否為管理員 0 否 1 是';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."sex" IS '性別 0 男 1 女';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."post_id" IS '崗位ID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."post_name" IS '崗位名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_account_non_expired" IS '賬戶是否過期';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_account_non_locked" IS '賬戶是否被鎖定';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_credentials_non_expired" IS '密碼是否過期';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."is_enabled" IS '賬戶是否可用';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."insert_user" IS '創建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."insert_time" IS '創建時間';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."update_user" IS '更新人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."update_time" IS '更新時間';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user"."license_code" IS '許可標識';
4.1.2.創建權限表
CREATE TABLE "hr_manager"."t_sys_my_permission" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"parent_id" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","parent_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","permission_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","permission_code" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","router_path" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","router_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","auth_url" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","order_no" int4,"type" VARCHAR ( 1 ) COLLATE "pg_catalog"."default","icon" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","remark" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,"update_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","update_time" DATE,"license_code" VARCHAR ( 20 ) COLLATE "pg_catalog"."default",CONSTRAINT "t_sys_my_permission_pkey" PRIMARY KEY ( "sid" )
);
ALTER TABLE "hr_manager"."t_sys_my_permission" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."sid" IS '主鍵SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."parent_id" IS '父節點ID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."parent_name" IS '父節點名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."permission_name" IS '權限名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."permission_code" IS '授權標識符';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."router_path" IS '路由地址';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."router_name" IS '路由名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."auth_url" IS '授權路徑(對應文件在項目的地址)';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."order_no" IS '序號(用于排序)';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."type" IS '類型 0 目錄 1 菜單 2 按鈕';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."icon" IS '圖標';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."remark" IS '備注';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."insert_user" IS '創建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."insert_time" IS '創建時間';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."update_user" IS '更新人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."update_time" IS '更新時間';
COMMENT ON COLUMN "hr_manager"."t_sys_my_permission"."license_code" IS '許可標識';
4.1.3.創建角色表
CREATE TABLE "hr_manager"."t_sys_my_role" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"role_name" VARCHAR ( 100 ) COLLATE "pg_catalog"."default","remark" VARCHAR ( 255 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,"update_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","update_time" DATE,"status" VARCHAR ( 255 ) COLLATE "pg_catalog"."default",CONSTRAINT "t_sys_my_role_pkey" PRIMARY KEY ( "sid" )
);
ALTER TABLE "hr_manager"."t_sys_my_role" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."sid" IS '主鍵SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."role_name" IS '角色名稱';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."remark" IS '備注';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."insert_user" IS '創建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."insert_time" IS '創建時間';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."update_user" IS '更新人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."update_time" IS '更新時間';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role"."status" IS '是否使用 0 禁用 1 使用';
4.1.4.創建用戶角色表
CREATE TABLE "hr_manager"."t_sys_my_user_role" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"role_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","user_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,CONSTRAINT "t_sys_my_user_role_pkey" PRIMARY KEY ( "sid" )
);
ALTER TABLE "hr_manager"."t_sys_my_user_role" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."sid" IS '主鍵SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."role_sid" IS '角色SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."user_sid" IS '用戶SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."insert_user" IS '創建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_user_role"."insert_time" IS '創建時間';
4.1.5.創建角色權限表
CREATE TABLE "hr_manager"."t_sys_my_role_permission" ("sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default" NOT NULL,"role_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","permission_sid" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_user" VARCHAR ( 50 ) COLLATE "pg_catalog"."default","insert_time" DATE,CONSTRAINT "t_sys_my_role_permission_pkey" PRIMARY KEY ( "sid" )
);
ALTER TABLE "hr_manager"."t_sys_my_role_permission" OWNER TO "postgres";
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."sid" IS '主鍵SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."role_sid" IS '角色SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."permission_sid" IS '權限SID';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."insert_user" IS '創建人';
COMMENT ON COLUMN "hr_manager"."t_sys_my_role_permission"."insert_time" IS '創建時間';
4.2.創建實體類
4.2.1.創建用戶實體類
@Data
public class SysMyUser implements Serializable, UserDetails {private static final long serialVersionUID = 1L;@TableId/*** sid*/private String sid;/*** user_no*/private String userNo;/*** user_name*/private String userName;/*** password*/private String password;/*** nick_name*/private String nickName;/*** phone_number*/private String phoneNumber;/*** email*/private String email;/*** department_id*/private String departmentId;/*** department_name*/private String departmentName;/*** is_admin*/private String isAdmin;/*** sex*/private String sex;/*** post_id*/private String postId;/*** post_name*/private String postName;/*** is_account_non_expired*/private Boolean isAccountNonExpired;/*** is_account_non_locked*/private Boolean isAccountNonLocked;/*** is_credentials_non_expired*/private Boolean isCredentialsNonExpired;/*** is_enabled*/private Boolean isEnabled;/*** insert_user*/private String insertUser;/*** insert_time*/private String insertTime;/*** update_user*/private String updateUser;/*** update_time*/private String updateTime;/*** license_code*/private String licenseCode;/*** 權限列表 就是菜單列表*/@TableField(exist = false)private List<SysMyPermission> permissionList;/*** 認證信息 就是用戶配置code*/@TableField(exist = false)Collection<? extends GrantedAuthority> authorities;/*** 用戶權限信息*/@TableField(exist = false)private List<String> roles;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getUsername() {return this.userNo;}@Overridepublic String getPassword() {return this.password;}@Overridepublic boolean isAccountNonExpired() {return this.isAccountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return this.isAccountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return this.isCredentialsNonExpired;}@Overridepublic boolean isEnabled() {return this.isEnabled;}
}
4.2.2.創建權限實體類
@Data
public class SysMyPermission implements Serializable {private static final long serialVersionUID = 1L;@TableId/*** sid*/private String sid;/*** parent_id*/private String parentId;/*** parent_name*/private String parentName;/*** permission_name*/private String permissionName;/*** permission_code*/private String permissionCode;/*** router_path*/private String routerPath;/*** router_name*/private String routerName;/*** auth_url*/private String authUrl;/*** order_no*/private String orderNo;/*** type*/private String type;/*** icon*/private String icon;/*** remark*/private String remark;/*** insert_user*/private String insertUser;/*** insert_time*/private String insertTime;/*** update_user*/private String updateUser;/*** update_time*/private String updateTime;/*** license_code*/private String licenseCode;/*** 菜單的子集合*/@TableField(exist = false)@JsonInclude(JsonInclude.Include.NON_NULL)private List<SysMyPermission> children = new ArrayList<>();
}
4.3.創建Service和Dao
這里就不過多介紹了,直接貼上代碼
4.3.1.UserService
@Service
public class SysMyUserService {@Resourceprivate SysMyUserMapper userMapper;/*** 根據用戶id獲取用戶信息(包含用戶具備的權限信息)* @param username 用戶信息* @return*/public SysMyUser getUserInfoByUserId(String username) {// 獲取用戶的基礎信息SysMyUser userInfo = userMapper.getUserInfoByUserId(username);Assert.notNull(userInfo, "用戶不存在");// 根據用戶id對應的權限信息List<String> autorizedList = userMapper.getAutorizedListByUserId(userInfo.getSid());;userInfo.setRoles(autorizedList);return userInfo;}/*** 獲取加密后的密碼 ,使用BCryptPasswordEncoder加密 10次 生成密碼* @param password 密碼* @return 加密后的密碼*/public String getEncoderPassword(String password) {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);String encodePassword = encoder.encode(password);return encodePassword;}
}
4.3.2.UserMapper
@Repository
public interface SysMyUserMapper extends BaseMapper<SysMyUser> {/*** 根據用戶名賬號獲取用戶信息* @param username 用戶信息* @return 用戶信息*/SysMyUser getUserInfoByUserId(@Param("username") String username);/*** 根據用戶id獲取用戶具備的權限信息* @param sid 用戶id* @return 用戶具備的權限信息*/List<String> getAutorizedListByUserId(@Param("sid") String sid);
}
4.3.3.UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.sys.my.core.user.dao.SysMyUserMapper"><!-- 根據用戶名賬號獲取用戶信息 --><select id="getUserInfoByUserId" resultType="com.sys.my.core.user.model.SysMyUser">select * from t_sys_my_user u where u.user_no = #{username};</select><!-- 根據用戶id獲取用戶具備的權限信息 --><select id="getAutorizedListByUserId" resultType="java.lang.String">selectp.permission_codefrom t_sys_my_role rleft join t_sys_my_user_role ur on ur.role_sid = r.sidleft join t_sys_my_role_permission rp on rp.role_sid = r.sidleft join t_sys_my_permission p on p.sid = rp.permission_sidleft join t_sys_my_user u on u.sid = ur.user_sidwhere p.status = '1' and r.status = '1' and u.sid = #{sid};</select>
</mapper>
4.3.4.SysMyPermissionService
@Service
public class SysMyPermissionService {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());@Resourceprivate SysMyPermissionMapper sysMyPermissionMapper;/*** 根據用戶id查詢對應的權限* @param userId 用戶id* @return 權限列表*/public List<SysMyPermission> getPermissionListByUserId(String userId){// 根據用戶ID獲取用戶對應的權限return sysMyPermissionMapper.getMenuListByUserId(userId);}
}
4.3.5.SysMyPermissionMapper
@Repository
public interface SysMyPermissionMapper extends BaseMapper<SysMyPermission> {/*** 根據用戶ID獲取用戶對應的權限* @param userId 用戶ID* @return 權限列表*/List<SysMyPermission> getMenuListByUserId(@Param("userId") String userId);
}
4.3.6.SysMyPermissionMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.sys.my.core.permission.dao.SysMyPermissionMapper"><!-- 根據用戶id獲取用戶具備的權限信息 --><select id="getMenuListByUserId" resultType="com.sys.my.core.permission.model.SysMyPermission">selectp.*from t_sys_my_role rleft join t_sys_my_user_role ur on ur.role_sid = r.sidleft join t_sys_my_role_permission rp on rp.role_sid = r.sidleft join t_sys_my_permission p on p.sid = rp.permission_sidleft join t_sys_my_user u on u.sid = ur.user_sidwhere p.status = '1' and r.status = '1' and u.sid = #{userId};</select>
</mapper>
4.4.重寫UserDetailsService方法
重寫 Spring Security 中的 UserDetailsService 接口的主要目的是
提供自定義的用戶認證邏輯
。Spring Security 的 UserDetailsService 負責從數據源(通常是數據庫)中加載用戶信息,包括用戶名、密碼和權限等,以便進行身份驗證。通常情況下,我們需要重寫
UserDetailsService
的loadUserByUsername()
方法,該方法接收用戶名作為參數,并返回一個 UserDetails 對象,該對象包含了與用戶名對應的用戶信息。在實際開發中,我們可能需要自定義的用戶信息存儲方式,或者希望在加載用戶信息時進行一些特定的邏輯處理,比如自定義密碼加密方式、從數據庫或其他數據源加載用戶信息等。
/*** 自定義UserDetailsService 用于認證和授權* 此處把用戶的信息和權限交給spring security* spring security會對用戶的信息和權限信息進行管理* @author hffan* serDetailService接口主要定義了一個方法 l* oadUserByUsername(String username)用于完成用戶信息的查詢,* 其中username就是登錄時的登錄名稱,登錄認證時,需要自定義一個實現類實現UserDetailService接口,* 完成數據庫查詢,該接口返回UserDetail。*/
@Component("customerUserDetailsService")
public class CustomerUserDetailsService implements UserDetailsService {@Resourceprivate SysMyUserService userService;@Resourceprivate SysMyPermissionService permissionService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysMyUser user = userService.getUserInfoByUserId(username);// 如果用戶不存在if (user == null){throw new UsernameNotFoundException("用戶名或者密碼錯誤");}// 根據用戶id查詢用戶權限List<SysMyPermission> permissionList = permissionService.getPermissionListByUserId(user.getSid());// 取出權限中配置codeList<String> collect = permissionList.stream().filter(item -> item != null).map(item -> item.getPermissionCode()).filter(item -> item != null).collect(Collectors.toList());// 轉為數據String[] strings = collect.toArray(new String[collect.size()]);List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(strings);// 配置權限user.setAuthorities(authorityList);// 配置菜單user.setPermissionList(permissionList);// 授權return user;}
}
4.5.自定義異常
自定義異常,通過傳入的異常 可以獲取對應的信息返回給前端
4.5.1.Token認證自定義異常
/*** 自定義異常 * AuthenticationException 是spring security提供的異常* 通過傳入的異常 可以獲取對應的信息返回給前端* token異常*/
public class TokenException extends AuthenticationException {public TokenException(String msg) {super(msg);}
}
4.5.2.用戶認證自定義異常
/*** 自定義異常* 通過傳入的異常 可以獲取對應的信息返回給前端* 用戶認證異常*/
public class CustomerAuthenionException extends AuthenticationException {public CustomerAuthenionException(String msg) {super(msg);}
}
4.6.編寫自定義處理器
通過實現SpringSecurity提供的一些接口,我們可以更好地管理身份驗證和授權流程,提高用戶體驗和應用程序的安全性。
4.6.1.匿名用戶訪問處理器
AuthenticationEntryPoint:
- 作用:AuthenticationEntryPoint 用于處理用戶嘗試訪問受保護資源但未進行身份驗證的情況。當用戶嘗試訪問需要身份驗證的資源但尚未進行身份驗證時,AuthenticationEntryPoint 將被調用來觸發身份驗證流程。
- 詳細講解:當用戶嘗試訪問安全受保護的資源但未進行身份驗證時,AuthenticationEntryPoint 的 commence() 方法將被調用。在這個方法中,我們可以定制返回響應給用戶,例如重定向到登錄頁面或返回401未授權錯誤等。
/*** 匿名用戶訪問資源處理器*/
@Component("loginAuthenticationHandler")
public class LoginAuthenticationHandler implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String res = JSONObject.toJSONString(ResultObject.createInstance(false,600,"匿名用戶沒有權限進行訪問!"));out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}
4.6.2.認證用戶無權限處理器
AccessDeniedHandler:
- 作用:AccessDeniedHandler 用于處理用戶嘗試訪問受保護資源但權限不足的情況。當用戶雖然進行了身份驗證,但由于缺乏足夠的權限而被拒絕訪問資源時,AccessDeniedHandler 將被調用。
- 詳細講解:AccessDeniedHandler 的 handle() 方法在訪問被拒絕時被調用。我們可以在這個方法中定義自定義的行為,例如返回自定義的錯誤頁面、向用戶發送通知或記錄拒絕的訪問嘗試。
/*** 認證用戶訪問無權限處理器*/
@Component("loginAccessDefineHandler")
public class LoginAccessDefineHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String res = JSONObject.toJSONString(ResultObject.createInstance(false,700,"您沒有開通對應的權限,請聯系管理員!"));out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}
4.6.3.賬戶信息異常處理器
AuthenticationFailureHandler:
- 作用:AuthenticationFailureHandler 用于處理身份驗證失敗的情況。當用戶提供的憑據無效或身份驗證過程出現錯誤時,AuthenticationFailureHandler 將被調用。
- 詳細講解:AuthenticationFailureHandler 的 onAuthenticationFailure() 方法在身份驗證失敗時被調用。我們可以在這個方法中執行自定義的行為,例如記錄登錄失敗次數、向用戶發送通知或返回自定義的錯誤頁面。
@Component("loginFiledHandler")
public class LoginFiledHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {//1.設置響應編碼response.setContentType("application/json;charset=UTF-8");ServletOutputStream out = response.getOutputStream();String str = null;int code = 500;if(exception instanceof AccountExpiredException){str = "賬戶過期,登錄失敗!";}else if(exception instanceof BadCredentialsException){str = "用戶名或密碼錯誤,登錄失敗!";}else if(exception instanceof CredentialsExpiredException){str = "密碼過期,登錄失敗!";}else if(exception instanceof DisabledException){str = "賬戶被禁用,登錄失敗!";}else if(exception instanceof LockedException){str = "賬戶被鎖,登錄失敗!";}else if(exception instanceof InternalAuthenticationServiceException){str = "賬戶不存在,登錄失敗!";}else if(exception instanceof CustomerAuthenionException){//token驗證失敗code = 600;str = exception.getMessage();} else{str = "登錄失敗!";}// 設置返回格式String res = JSONObject.toJSONString(ResultObject.createInstance(false,str));out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}
4.6.4.登錄成功處理器
AuthenticationSuccessHandler:
- 作用:AuthenticationSuccessHandler 用于處理身份驗證成功的情況。當用戶成功進行身份驗證并被授權訪問資源時,AuthenticationSuccessHandler 將被調用。
- 詳細講解:AuthenticationSuccessHandler 的 onAuthenticationSuccess() 方法在身份驗證成功時被調用。我們可以在這個方法中執行自定義的行為,例如記錄登錄成功的日志、向用戶發送歡迎消息或重定向到特定頁面。
/*** 自定義認證成功處理器*/
@Component("loginSuccessHandler")
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Resourceprivate RedisCache redisCache;@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {SysMyUser user = (SysMyUser)authentication.getPrincipal();// 登錄成功處理//1.生成tokenString token = TokenUtil.genAccessToken(user.getUsername());long expireTime = TokenUtil.getExpirationTime(token);// 配置一下返回給前端的token信息LoginResultObject vo = new LoginResultObject();// 將實體類信息轉為JSON// TODO 將token存入coookie中 后面加載頁面 根據用戶的id取查詢對應的權限vo.setUserInfo(user);vo.setCode(200L);// TODO 將token存放到redis中 退出或者修改密碼 清空token 獲取的時候 也從redis中進行獲取redisCache.setCacheObject(httpServletRequest.getRemoteAddr(),token,TokenUtil.ACCESS_EXPIRE, TimeUnit.MILLISECONDS);vo.setToken(token);vo.setExpireTime(expireTime);String res = JSONObject.toJSONString(vo);httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream out = httpServletResponse.getOutputStream();out.write(res.getBytes("UTF-8"));out.flush();out.close();}
}
4.7.自定義過濾器
實現 Spring Security 中的 OncePerRequestFilter 接口,用于處理用戶請求的過濾邏輯。
- 該過濾器用于對用戶的請求進行攔截,驗證用戶的訪問權限和身份信息。
- 如果請求的 URL 是某些特定的資源或者登錄頁面,則直接放行。
- 如果不是登錄請求,則對請求中的 token 進行驗證,以確保用戶的身份信息有效。
- 如果驗證通過,則將用戶的身份信息設置到 Spring Security 的上下文中,從而完成用戶的身份認證。
-
@Component("checkTokenFilter")
:將該類聲明為 Spring 組件,并指定其名稱為 “checkTokenFilter”。 -
@EqualsAndHashCode(callSuper=false)
:生成 equals() 和 hashCode() 方法,忽略父類 OncePerRequestFilter。 -
@Data
:Lombok 注解,自動生成 getter、setter、equals、hashCode 等方法。 -
@Autowired
和@Value
:用于依賴注入和獲取配置信息。 -
doFilterInternal
方法:這是 OncePerRequestFilter 類的抽象方法,用于實現具體的請求過濾邏輯。
- 首先判斷請求的 URL 是否屬于特定的資源,如果是則放行。
- 判斷是否是登錄請求,如果是,則直接放行。
- 如果不是登錄請求,則驗證請求中的 token,確保用戶的身份信息有效。
- 如果 token 驗證失敗,則調用 AuthenticationFailureHandler 處理身份驗證失敗的情況。
- 如果 token 驗證通過,則將用戶的身份信息設置到 Spring Security 的上下文中。
-
validateToken
方法:用于驗證請求中的 token。
- 首先從請求頭部獲取 token,如果沒有則從請求參數中獲取,如果仍然沒有則從 Redis 緩存中獲取。
- 解析 token,獲取其中的用戶名。
- 根據用戶名加載用戶信息,使用自定義的 CustomerUserDetailsService。
- 如果用戶信息加載成功,則創建 UsernamePasswordAuthenticationToken,并將用戶信息設置到 Spring Security 上下文中。
-
最后調用
filterChain.doFilter(httpServletRequest, httpServletResponse)
,將請求傳遞給下一個過濾器處理。
@Data
@Component("checkTokenFilter")
@EqualsAndHashCode(callSuper=false)
public class CheckTokenFilter extends OncePerRequestFilter {@Value("${hrfan.login.url}")private String loginUrl;@Autowiredprivate LoginFiledHandler loginFailureHandler;@Autowiredprivate CustomerUserDetailsService customerUserDetailsService;@Resourceprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {//獲取請求的url(讀取配置文件的url)String url = httpServletRequest.getRequestURI();if (StringUtils.contains(httpServletRequest.getServletPath(), "swagger")|| StringUtils.contains(httpServletRequest.getServletPath(), "webjars")|| StringUtils.contains(httpServletRequest.getServletPath(), "v3")|| StringUtils.contains(httpServletRequest.getServletPath(), "profile")|| StringUtils.contains(httpServletRequest.getServletPath(), "swagger-ui")|| StringUtils.contains(httpServletRequest.getServletPath(), "swagger-resources")|| StringUtils.contains(httpServletRequest.getServletPath(), "csrf")|| StringUtils.contains(httpServletRequest.getServletPath(), "favicon")|| StringUtils.contains(httpServletRequest.getServletPath(), "v2")|| StringUtils.contains(httpServletRequest.getServletPath(), "user")|| StringUtils.contains(httpServletRequest.getServletPath(), "getImageCode")) {filterChain.doFilter(httpServletRequest, httpServletResponse);}else if (StringUtils.equals(url,loginUrl)){// 是登錄請求放行filterChain.doFilter(httpServletRequest, httpServletResponse);}else {try {//token驗證(如果不是登錄請求 驗證toekn)if(!url.equals(loginUrl)){validateToken(httpServletRequest);}}catch (AuthenticationException e){loginFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);return;}filterChain.doFilter(httpServletRequest,httpServletResponse);}}//token驗證private void validateToken(HttpServletRequest request){//從請求的頭部獲取tokenString token = request.getHeader("token");//如果請求頭部沒有獲取到token,則從請求參數中獲取tokenif(StringUtils.isEmpty(token)){token = request.getParameter("token");}if (StringUtils.isEmpty(token)){// 請求參數中也沒有 那就從redis中進行獲取根據ip地址取token = redisCache.getCacheObject(request.getRemoteAddr());}if(StringUtils.isEmpty(token)){throw new CustomerAuthenionException("token不存在!");}//解析tokenString username = TokenUtil.getUserFromToken(token);if(StringUtils.isEmpty(username)){throw new CustomerAuthenionException("token解析失敗!");}//獲取用戶信息UserDetails user = customerUserDetailsService.loadUserByUsername(username);if(user == null){throw new CustomerAuthenionException("token驗證失敗!");}UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//設置到spring security上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);}
}
4.8.設置登錄返回信息
用戶返回用戶登錄 成功或者失敗的信息,成功后需要包含用戶的相關信息 和token
/*** 登錄返回信息*/
@Data
public class LoginResultObject {private String token;//token過期時間private Long expireTime;private SysMyUser userInfo;private Long code;
}
4.9.編寫SpringSecurity配置
#### 注意因為新版本的SpringSecurity和舊版本的差距較大,所以這里保留了舊版本的寫法我使用的SpringBoot 和 SpringSecurity 版本都是相對較新的 3.1.8版本 JDK版本是21
import com.sys.my.config.security.details_service.CustomerUserDetailsService;
import com.sys.my.config.security.filter.CheckTokenFilter;
import com.sys.my.config.security.handler.LoginAccessDefineHandler;
import com.sys.my.config.security.handler.LoginAuthenticationHandler;
import com.sys.my.config.security.handler.LoginFiledHandler;
import com.sys.my.config.security.handler.LoginSuccessHandler;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import java.util.Collections;/*** SpringSecurity配置類*/
@Configuration
@EnableWebSecurity //啟用Spring Security
public class SpringSecurityConfig {@Resourceprivate CustomerUserDetailsService customerUserDetailsService;@Resourceprivate LoginSuccessHandler loginSuccessHandler;@Resourceprivate LoginFiledHandler loginFiledHandler;@Resourceprivate LoginAuthenticationHandler loginAuthenticationHandler;@Resourceprivate LoginAccessDefineHandler loginAccessDefineHandler;@Resourceprivate CheckTokenFilter checkTokenFilter;/*** 密碼處理* @return*/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}/*** 新版的實現方法不再和舊版一樣在配置類里面重寫方法,而是構建了一個過濾鏈對象并通過@Bean注解注入到IOC容器中* 新版整體代碼 (注意:新版AuthenticationManager認證管理器默認全局)* @param http http安全配置* @return SecurityFilterChain* @throws Exception 異常*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http // 使用自己自定義的過濾器 去過濾接口請求.addFilterBefore(checkTokenFilter, UsernamePasswordAuthenticationFilter.class).formLogin((formLogin) ->// 這里更改SpringSecurity的認證接口地址,這樣就默認處理這個接口的登錄請求了formLogin.loginProcessingUrl("/api/v1/user/login")// 自定義的登錄驗證成功或失敗后的去向.successHandler(loginSuccessHandler).failureHandler(loginFiledHandler))// 禁用了 CSRF 保護。.csrf((csrf) -> csrf.disable())// 配置了會話管理策略為 STATELESS(無狀態)。在無狀態的會話管理策略下,應用程序不會創建或使用 HTTP 會話,每個請求都是獨立的,服務器不會在請求之間保留任何狀態信息。.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeRequests((authorizeRequests) ->// 這里過濾一些 不需要token的接口地址authorizeRequests.requestMatchers("/api/v1/test/getTestInfo").permitAll().requestMatchers( "/v3/**","/profile/**","/swagger-ui.html","/swagger-resources/**","/v2/api-docs","/v3/api-docs","/webjars/**","/swagger-ui/**","/v2/**","/favicon.ico","/webjars/springfox-swagger-ui/**","/static/**", "/webjars/**", "/v2/api-docs", "/v2/feign-docs","/swagger-resources/configuration/ui","/test/user","/swagger-resources", "/swagger-resources/configuration/security","/swagger-ui.html", "/webjars/**").permitAll().requestMatchers("/api/v1/user/login","/api/v1/user/getImageCode").permitAll().anyRequest().authenticated()).exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(loginAuthenticationHandler) // 匿名處理.accessDeniedHandler(loginAccessDefineHandler) // 無權限處理).cors((cors) -> cors.configurationSource(configurationSource())).headers((headers) -> headers.frameOptions((frameOptionsConfig -> frameOptionsConfig.disable()))).headers((headers) -> headers.frameOptions((frameOptionsConfig -> frameOptionsConfig.sameOrigin())));// 構建過濾鏈并返回return http.build();}// 舊版本 需要繼承 extends WebSecurityConfigurerAdapter// 新版的比較簡單,直接定義好數據源,注入就可以了,無需手動到配置類中去將它提交給AuthenticationManager進行管理。// /**// * 配置認證處理器// * 自定義的UserDetailsService// * @param auth// * @throws Exception// */// @Override// protected void configure(AuthenticationManagerBuilder auth) throws Exception {// auth.userDetailsService(customerUserDetailsService);// }// /**// * 配置權限資源// * @param http// * @throws Exception// */// @Override// protected void configure(HttpSecurity http) throws Exception {// // 每次請求前檢查token// http.addFilterBefore(checkTokenFilter, UsernamePasswordAuthenticationFilter.class);// http.formLogin()// .loginProcessingUrl("/api/v1/user/login")// // 自定義的登錄驗證成功或失敗后的去向// .successHandler(loginSuccessHandler).failureHandler(loginFiledHandler)// // 禁用csrf防御機制(跨域請求偽造),這么做在測試和開發會比較方便。// .and().csrf().disable()// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// .and()// .authorizeRequests()// .antMatchers("/api/v1/test/getTestInfo").permitAll()// // 放心swagger相關請求// .antMatchers( "/v3/**","/profile/**","/swagger-ui.html",// "/swagger-resources/**",// "/v2/api-docs",// "/v3/api-docs",// "/webjars/**","/swagger-ui/**","/v2/**","/favicon.ico","/webjars/springfox-swagger-ui/**","/static/**", "/webjars/**", "/v2/api-docs", "/v2/feign-docs",// "/swagger-resources/configuration/ui",// "/swagger-resources", "/swagger-resources/configuration/security",// "/swagger-ui.html", "/webjars/**").permitAll()// .antMatchers("/api/v1/user/login","/api/v1/user/getImageCode").permitAll()// .anyRequest().authenticated()// .and()// .exceptionHandling()// // 匿名處理// .authenticationEntryPoint(loginAuthenticationHandler)// // 無權限處理// .accessDeniedHandler(loginAccessDefineHandler)// // 跨域配置// .and()// .cors()// .configurationSource(configurationSource());// // 設置iframe// http.headers().frameOptions().sameOrigin();// http.headers().frameOptions().disable();//// }/*** 跨域配置*/CorsConfigurationSource configurationSource() {CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));corsConfiguration.setAllowedMethods(Collections.singletonList("*"));corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));corsConfiguration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", corsConfiguration);return source;}
}
4.10.配置文件配置
hrfan:login:url: "/api/v1/user/login"
5.測試
5.1.測試登錄密碼錯誤
5.2.測試正確密碼
5.3.測試無token訪問接口
SpringSecurity為我們提供了基于注解的權限控制方案。
在啟動類上加上
@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("/jjwt")@PreAuthorize("hasAuthority('user_list')")public Map<String, String> jjwt(){// 這里的user_list 就是我們權限中permission_codethrow new RuntimeException("測試無token訪問!");}