1、簡介
????????本文講述了如何實現簡易的后端鑒權服務。所謂“鑒權”,就是“身份鑒定”+“權限判斷”。涉及的技術有:OAuth2、SpringSecurity、Jwt、過濾器、攔截器。OAuth2用于授權,使用Jwt簽發Access Token和Refresh Token,并管理token的過期時間以及刷新校驗token。SpringSecurity用于認證,會拿著輸入的用戶名和密碼去數據庫中比對,如果比對成功則調用OAuth2取授權簽發token。Jwt則被用于生成token,jwt會根據用戶信息進行base64編碼,并對編碼后的字符串進行加密。過濾器則是用在網關,目的是把那些沒有認證過的請求,即沒有攜帶token或者攜帶的token不合法的請求過濾掉,使那些請求不會打到后端其他服務上去。攔截器的作用是在網關身份認證后,請求會被轉發到具體的各個后端服務上,如果請求的發起者沒有訪問接口的權限,那么請求就會被攔截掉。
2、相關技術介紹
2.1、OAuth2
????????OAuth2是一種授權框架,可以實現第三方授權。OAuth2一共有4種授權模式:
????????(1)客戶端模式:客戶端直接向驗證服務器請求一個token,獲得token后,客戶端攜帶著這個token就能訪問相應的其他服務了。不過這種模式下沒法進行身份驗證。通常適用于服務內部之間調用。類似于feign調用這種。
????????(2)密碼模式:客戶端提供用戶名密碼給驗證服務器,用戶名和密碼驗證通過后,驗證服務器返回給token,客戶端再攜帶著token去訪問其他服務。不過這種模式容易把用戶名密碼泄露給客戶端。比如,你在網站登錄頁面輸入用戶名和密碼,那么你的用戶名和密碼就有可能泄露給登錄頁面。有些釣魚網站就會以欺騙登錄頁面的方式獲取到用戶的用戶名和密碼。因此使用這種模式需確保客戶端是可信的。
????????(3)隱式授權模式:用戶訪問某個頁面時,如果該用戶尚未被身份認證,頁面就會重定向到認證服務器,認證服務器會給用戶一個認證頁面,用戶在上面輸入用戶名和密碼完成身份認證后,認證服務器就會返回token。用戶就可以拿著token去訪問其他服務了。隱式授權模式通常會用于實現sso單點登錄。不過該方式會暴露token給用戶。
????????(4)授權碼模式:這種模式是最安全的一種模式,也是推薦使用的一種,比如我們手機上的很多 App 都是使用的這種模式。相比隱式授權模式,它并不會直接返回 token,而是返回授權碼,真正的 token 是通過應用服務器訪問驗證服務器獲得的。在一開始的時候,應用服務器(客戶端通過訪問自己的應用服務器來進而訪問其他服務)和驗證服務器之間會共享一個 secret,這個東西沒有其他人知道,而驗證服務器在用戶驗證完成之后,會返回一個授權碼,應用服務器最后將授權碼和 secret 一起交給驗證服務器進行驗證,并且 Token 也是在服務端之間傳遞,是存放在應用服務器上的,不會直接給到客戶端。
2.2、SpringSecurity
????????SpringSecurity是一種安全框架,通常是會集成OAuth2一起使用。
????????SpringSecurity+OAuth2協作方式:
????????SpringSecurity可以作為OAuth2授權服務器,驗證用戶身份的合法性,如果身份合法則讓OAuth2簽發token。SpringSecurity框架本身也自帶了一個登錄頁面,并且提供了一個WebSecurityConfigurerAdapter類,可以通過繼承該類并重載configure方法,實現自定的權限攔截。
?????????簡而言之,OAuth2定義了 授權的標準協議,解決“如何安全地允許第三方訪問資源”的問題。Spring Security提供了 實現 OAuth2 和安全控制的工具鏈,包括認證、授權、令牌管理等具體功能。
2.3、JWT
????????JWT(JSON Web Token),是用于生成token的,其原理是將用戶身份信息和聲明,編碼為緊湊的、自包含的字符串,并通過數字簽名保證其完整性和真實性。JWT由Header、Payload、Signature三部分組成:
(1)Header是定義token的元數據,如簽名算法和類型(常用的加密算法有SHA256)。并通過base64對Header數據進行編碼。
(2)Payload是用于存儲用戶身份信息和自定義聲明。會存儲簽發者信息、過期時間、簽發時間等。也是采用base64編碼。
(3)Signature是用于驗證token的完整性和真實性,防止篡改。先對 Header 和 Payload 進行 Base64Url 編碼,然后再使用密鑰(Secret Key)和指定算法(如 HS256、RS256)對編碼后的字符串簽名。
????????而最后生成的token就是將三部分用"."拼接起來。即:token=Header.Payload.Signature
2.4、過濾器
????????過濾器(filter)是java web的核心組件,是用于攔截請求并執行預處理或者后處理邏輯。比較常用的過濾器有Filter和GlobalFilter,Filter是局部過濾器,是java servlet下的組件,僅對特定的路由生效,通常可以在yml里面通過filters關鍵字進行配置。而GlobalFilter是Spring Cloud Gateway下的組件,是全局過濾器。過濾攔截所有的請求。通常是加在網關服務中,可以對發向網關的請求進行全局身份認證、全局限流、日志記錄(記錄所有的請求信息)、統一修改請求的Header等。
2.5、攔截器
????????攔截器(Interceptor)是Spring MVC提供的組件,和過濾器一樣,也是用于攔截請求并執行預處理或者后處理邏輯。繼承HandlerInterceptorAdapter類,preHandle是預處理方法(在請求前執行),postHandle是后處理方法(在請求后執行)。攔截器通常會用在對接口的權限控制。使用preHandle進行請求預處理,沒有權限則攔截。也可以記錄請求情況日志,使用postHandle在請求后記錄日志。
3、代碼實現
【免費】基于OAuth2+SpringSecurity+Jwt實現身份認證和權限管理后端服務代碼合集資源-CSDN文庫
3.1、Eureka注冊中心
????????所有的服務都要向注冊中心注冊,以便于服務發現和服務之間的調用。
????????pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>eureka-center</artifactId><version>1.0-SNAPSHOT</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><springframework.version>1.5.4.RELEASE</springframework.version><springframework.version1>1.3.5.RELEASE</springframework.version1></properties><dependencies><!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka-server --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-eureka-server</artifactId><version>${springframework.version1}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>${springframework.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>${springframework.version}</version><scope>test</scope></dependency></dependencies>
</project>
?????????application.yml
server:port: 8001#Eureka配置
eureka:instance:hostname: localhost #Eureka服務端的實例名稱client:register-with-eureka: false #是否向eureka注冊中心注冊自己,因為這里本身就是eureka服務端,所以無需向eureka注冊自己fetch-registry: false #fetch-registry為false,則表示自己為注冊中心service-url: #監控頁面defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
????????SpringcloudEurekaApplication.java
package eureka;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/*** @author: Wulc* @createTime: 2025-05-02* @description:* @version: 1.0*/
@SpringBootApplication
@EnableEurekaServer //使eureka服務端可以工作
public class SpringcloudEurekaApplication {public static void main(String[] args) {SpringApplication.run(SpringcloudEurekaApplication.class, args);}
}
3.2、auth-service認證授權中心
????????認證授權中心是用于對用戶進行身份認證,授權可以訪問的范圍,生成token,管理token。
????????auth-service這部分的代碼我是直接用這篇文章里的:OAuth2.0 實現單點登錄_oauth2.0單點登錄-CSDN博客
????????因為密碼要加密存儲,我這里用的是證書加密。
-- 創建數據庫證書用于對密碼進行加密
--查看數據庫中的證書
select * from sys.certificates;
--創建數據庫主密鑰
CREATE MASTER KEY ENCRYPTION BY PASSWORD ='123@#456';--創建證書
CREATE CERTIFICATE MyCert
with SUBJECT = 'Certificate To Password'
GO-- 用戶表
CREATE TABLE UserInfo
(id int primary key identity(1,1),userName varchar(50),pwd varbinary(2000)
);--使用MyCert證書加密pwd字段
insert into UserInfo(userName,pwd) values('zhangsan',ENCRYPTBYCERT(CERT_ID('MyCert'),'123456')
);
insert into UserInfo(userName,pwd) values('lisi',ENCRYPTBYCERT(CERT_ID('MyCert'),'qwerty')
);
insert into UserInfo(userName,pwd) values('wangwu',ENCRYPTBYCERT(CERT_ID('MyCert'),'112233')
);--使用MyCert證書解密pwd字段
select id,userName,CONVERT(
varchar(100),
DecryptByCert(CERT_ID('MyCert'),pwd)
) as pwd from UserInfo;
????????pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>auth-service</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.6</version><relativePath/> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId><version>3.1.1</version></dependency><dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>mssql-jdbc</artifactId><version>9.4.0.jre8</version></dependency><!--Mybatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version></dependency><!-- Junit4--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.7.11</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.1</version><scope>test</scope></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- spring2.X集成redis所需common-pool2--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.2</version> <!-- 對應 Spring Boot 2.7.x --><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
</project>
????????application.yml
server:port: 8002servlet:#為了防止一會在服務之間跳轉導致Cookie打架(因為所有服務地址都是localhost,都會存JSESSIONID)#這里修改一下context-path,這樣保存的Cookie會使用指定的路徑,就不會和其他服務打架了#但是注意之后的請求都得在最前面加上這個路徑context-path: /ssospring:application:name: auth-service-serverdatasource:name: MyTestDataBasedriverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriverurl: jdbc:sqlserver://127.0.0.1:1433;databaseName=MyTestDataBaseusername: wlcpassword: 123456redis:port: 6379database: 0host: 127.0.0.1password:mybatis:mapper-locations: classpath:mapper/*.xml #注意:一定要對應mapper映射xml文件的所在路徑eureka:client:service-url:defaultZone: http://localhost:8001/eureka/ # Eureka注冊中心地址register-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.application.name}:${server.port}
????????OAuth2Configuration.java
package com.auth.config;import com.auth.service.impl.MyUserDetailsService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import javax.annotation.Resource;/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/@EnableAuthorizationServer //開啟驗證服務器
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {@Resourceprivate MyUserDetailsService myUserDetailsService;@Resourceprivate AuthenticationManager manager;@Resourceprivate TokenStore store;@Resourceprivate JwtAccessTokenConverter converter;private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.tokenServices(serverTokenServices()).userDetailsService(myUserDetailsService).authenticationManager(manager);}/*** 這個方法是對客戶端進行配置,一個驗證服務器可以預設很多個客戶端,* 之后這些指定的客戶端就可以按照下面指定的方式進行驗證* @param clients 客戶端配置工具*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory() // 這里我們直接硬編碼創建,當然也可以像Security那樣自定義或是使用JDBC從數據庫讀取.withClient("web") // 客戶端ID,隨便起就行.secret(encoder.encode("654321")) // 只與客戶端分享的secret,隨便寫,但是注意要加密.autoApprove(false) // 自動審批,這里關閉,要的就是一會體驗那種感覺.scopes("user").authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) {security.passwordEncoder(encoder) // 編碼器設定為BCryptPasswordEncoder.allowFormAuthenticationForClients() // 允許客戶端使用表單驗證,一會我們POST請求中會攜帶表單信息.checkTokenAccess("permitAll()"); // 允許所有的Token查詢請求}/**************************** JWT 配置 **********************************/private AuthorizationServerTokenServices serverTokenServices(){ // 這里對AuthorizationServerTokenServices進行一下配置DefaultTokenServices services = new DefaultTokenServices();services.setSupportRefreshToken(true); // 允許Token刷新services.setTokenStore(store); // 添加剛剛的TokenStoreservices.setTokenEnhancer(converter); // 添加Token增強,其實就是JwtAccessTokenConverter,增強是添加一些自定義的數據到JWT中services.setAccessTokenValiditySeconds(60); //訪問token有效期20秒services.setRefreshTokenValiditySeconds(120); //刷新token有效期120秒services.setSupportRefreshToken(true);return services;}
}
?????????SecurityConfiguration.java
package com.auth.config;import com.auth.service.impl.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Autowiredprivate MyUserDetailsService myUserDetailsService;@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//從數據庫中獲取用戶信息auth.userDetailsService(myUserDetailsService).passwordEncoder(encoder);}@Bean // 這里需要將AuthenticationManager注冊為Bean,在OAuth配置中使用@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Bean@Overridepublic UserDetailsService userDetailsServiceBean() throws Exception {return super.userDetailsServiceBean();}/***************************** JWT配置 ************************************/@Bean("tokenConverter")public JwtAccessTokenConverter tokenConverter(){ // Token轉換器,將其轉換為JWTJwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("wlcKey"); // 這個是對稱密鑰,一會資源服務器那邊也要指定為這個return converter;}//token存放在哪里,放在Redis里面@Beanpublic TokenStore tokenStore(){return new RedisTokenStore(redisConnectionFactory);}
}
?????????UserInfoDTO.java
package com.auth.dto;import lombok.Data;/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/@Data
public class UserInfoDTO {private Integer id;private String userName;private String pwd;
}
?????????UserMapper.java
package com.auth.mapper;import com.auth.dto.UserInfoDTO;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper {UserInfoDTO getUserInfoByUserName(String userName);
}
?????????MyUserDetailsService.java
package com.auth.service.impl;import com.auth.dto.UserInfoDTO;
import com.auth.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.List;/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/@Service
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;/*** loadUserByUsername** description 從數據庫中根據用戶名獲取用戶信息,并轉為Spring Security的User* @param username* @return* @throws* @author Wulc* @date 2025/5/12 11:01*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {UserInfoDTO userInfoDTO=userMapper.getUserInfoByUserName(username);List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("user");return new User(userInfoDTO.getUserName(), new BCryptPasswordEncoder().encode(userInfoDTO.getPwd()), authorities);}
}
?????????ApplicationStarter.java
package com.auth;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/@SpringBootApplication
@EnableDiscoveryClient
public class ApplicationStarter {public static void main(String[] args) {SpringApplication.run(ApplicationStarter.class, args);}
}
?????????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.auth.mapper.UserMapper"><select id="getUserInfoByUserName" resultType="com.auth.dto.UserInfoDTO">SELECTid,userName,CONVERT(varchar(100),DecryptByCert(CERT_ID('MyCert'),pwd)) as pwdFROM UserInfo WHERE userName = #{userName}</select>
</mapper>
????????啟動該服務后:
????????訪問:http://localhost:8002/sso/oauth/token 獲取到token。
????????因為token是存放在redis里面的,可以在redis里面查看到token。
????????訪問:http://localhost:8002/sso/oauth/check_token 可以檢查token是否有效。
????????訪問:http://localhost:8002/sso/oauth/token 可以在access_token過期時,使用refresh_token重新獲取一遍token。這樣子就避免了用戶再次輸入用戶名密碼了。
3.3、action-controller-service權限控制中心
?action-controller-api
????????AccessActionControl.java
package com.action.annotation;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author Wulc* @date 2025/5/13 8:55* @description 定義注解用于加在接口方法上進行權限控制*/
@Target({ElementType.METHOD})// 可用在方法名上
@Retention(RetentionPolicy.RUNTIME)// 運行時有效
@Documented
public @interface AccessActionControl {String[] resource() default {};String[] action() default {};
}
????????AccessActionFeign.java
package com.action.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value = "action-controller-server")
public interface AccessActionFeign {@PostMapping("/api/checkAccessAction")boolean checkAccessAction(@RequestParam("username") String username,@RequestParam("resource") String[] resource,@RequestParam("action") String[] action);
}
?????????pom.xml(action-controller-api)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.example</groupId><artifactId>action-controller-service</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>action-controller-api</artifactId><packaging>jar</packaging><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency></dependencies><build><plugins><!-- 禁用 Spring Boot 的 Fat JAR 打包 --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><skip>true</skip> <!-- 關鍵!禁止生成 BOOT-INF --></configuration></plugin><!-- 可選:確保生成的 JAR 包含源碼(方便調試) --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-source-plugin</artifactId><executions><execution><id>attach-sources</id><goals><goal>jar-no-fork</goal></goals></execution></executions></plugin></plugins></build>
</project>
action-controller-server
????????application.yml
server:port: 8004spring:application:name: action-controller-server#eureka配置,服務注冊到哪?
eureka:client:service-url:defaultZone: http://localhost:8001/eureka/instance:#修改eureka上默認描述信息instance-id: ${spring.application.name}:${server.port}
?????????AccessActionController.java
package com.action.controller.feign;import com.action.feign.AccessActionFeign;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/*** @author: Wulc* @createTime: 2025-05-13* @description:* @version: 1.0*/@RestController
@RequestMapping("/api")
public class AccessActionController implements AccessActionFeign {@PostMapping("/checkAccessAction")@Overridepublic boolean checkAccessAction(@RequestParam("username") String username,@RequestParam("resource") String[] resource,@RequestParam("action") String[] action) {//這里可以寫你的權限判斷邏輯,通常是根據數據庫中的角色表權限表計算出來的。//我這里作為例子,就直接寫死了if ("zhangsan".equals(username) && "1086".equals(resource[0]) && "read".equals(action[0])) {return true;}return false;}
}
?????????ApplicationStarter.java
package com.action;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** @author: Wulc* @createTime: 2025-05-13* @description:* @version: 1.0*/
@EnableDiscoveryClient
@SpringBootApplication
public class ApplicationStarter {public static void main(String[] args) {SpringApplication.run(ApplicationStarter.class, args);}
}
?????????pom.xml(action-controller-server)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.example</groupId><artifactId>action-controller-service</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>action-controller-server</artifactId><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.example</groupId><artifactId>action-controller-api</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
</project>
?????????pom.xml(action-controller-service)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>action-controller-service</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules><module>action-controller-api</module><module>action-controller-server</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.3</version><relativePath/> <!-- lookup parent from repository --></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency></dependencies><!-- 使用dependencyManagement統一管理SpringCloud組件,集中定義所有SpringCloud相關組件的兼容版本,避免手動指定每個依賴的版本號,--><!-- 解決版本沖突問題。我這里使用了2021.0.3,對應的是Springboot2.6.x--><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifest><!-- 指定主類,格式為:包名.類名 --><mainClass>com.action.ApplicationStarter</mainClass></manifest></archive></configuration></plugin></plugins></build><!-- 上傳需要的配置到nexus倉庫 --><!--我這里是把action-controller-api打包成一個jar包上傳到nexus去了,這樣的話,如果要用到action-controller-api就可以直接在pom.xml添加依賴信息,從nexus中下載就行--><distributionManagement>
<!-- <repository>-->
<!-- <id>wulc-nexus</id>-->
<!-- <!– 正式版–>-->
<!-- <url>http://192.168.10.104:8081/repository/maven-releases/</url>-->
<!-- </repository>--><snapshotRepository><id>wulc-nexus</id><!-- 快照版--><url>http://192.168.10.104:8081/repository/maven-snapshots/</url></snapshotRepository></distributionManagement>
</project>
?
????????關于如果上傳到nexus可以參考我的這篇:使用Nexus搭建遠程maven倉庫_nexus 倉庫教程-CSDN博客
????????當然如果嫌搭建一個Nexus太麻煩的話,可以直接本地對action-controller-api進行maven install,在本地maven倉庫中生成一個jar包。供其他服務需要時直接導入。
?
3.4、provider-server
????????provider-server是被訪問的服務,會引入action-controller-api依賴,在服務的接口上加上@AccessActionControl用于方法級別的權限控制。會寫一個攔截器,用于對所有加了@AccessActionControl注解的接口進行權限判斷預處理。
?????????pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>provider-server</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.3</version><relativePath/> <!-- lookup parent from repository --></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>
<!-- 引入action-controller-api--><dependency><groupId>org.example</groupId><artifactId>action-controller-api</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies><!-- 使用dependencyManagement統一管理SpringCloud組件,集中定義所有SpringCloud相關組件的兼容版本,避免手動指定每個依賴的版本號,--><!-- 解決版本沖突問題。我這里使用了2021.0.3,對應的是Springboot2.6.x--><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><!-- 拉取需要的配置 --><repositories><repository><!-- id和name可以隨便配置,因為在setting文件中配置過了--><id>wulc-nexus</id><name>wulc-nexus</name><url>http://192.168.10.104:8081/repository/maven-public/</url><releases><enabled>true</enabled></releases><snapshots><enabled>true</enabled></snapshots></repository></repositories>
</project>
?????????注:pom.xml中的<repositories><repository>的配置表示直接從192.168.10.104:8081上的nexus倉庫中獲取action-controller-api的jar包。當然你也可以直接引入action-controller-api的jar包。
?????????WebConfiguration.java
package com.provider.config;import com.provider.interceptor.AccessActionInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @author: Wulc* @createTime: 2025-05-13* @description:* @version: 1.0*/@Configuration
public class WebConfiguration implements WebMvcConfigurer {@Autowired@Lazy // 延遲注入,避免循環依賴AccessActionInterceptor accessActionInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//攔截所有請求registry.addInterceptor(accessActionInterceptor).addPathPatterns("/**");}
}
?????????ProviderController.java
package com.provider.controller;import com.action.annotation.AccessActionControl;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/@RestController
@RequestMapping("/provider")
public class ProviderController {@AccessActionControl(resource = {"1086"}, action = "read")@PostMapping("/getMsg")public String getMsg(){return "訪問到了provider";}
}
?????????AccessActionInterceptor.java
package com.provider.interceptor;import com.action.annotation.AccessActionControl;
import com.action.feign.AccessActionFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;/*** @author: Wulc* @createTime: 2025-05-13* @description:* @version: 1.0*/@Component
public class AccessActionInterceptor extends HandlerInterceptorAdapter {@Autowiredprivate AccessActionFeign accessActionFeign;//在請求被處理之前,調用action-controller的權限判斷接口,如果有權限就放行,沒有權限就攔截@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 如果不是映射到方法直接通過if (!(handler instanceof HandlerMethod)) {return true;}// ①:START 方法注解級攔截器HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();AccessActionControl accessActionControl=method.getAnnotation(AccessActionControl.class);if(accessActionControl!=null){String username=request.getHeader("username");String[] resource=accessActionControl.resource();String[] action=accessActionControl.action();// accessActionFeign.checkAccessAction(username,resource,action);boolean flag=accessActionFeign.checkAccessAction(username,resource,action);if(!flag){// 設置響應狀態碼(如403 Forbidden)response.setStatus(HttpServletResponse.SC_FORBIDDEN);// 設置響應內容類型(如JSON)response.setContentType("application/json;charset=UTF-8");// 構建響應內容(示例:返回JSON格式錯誤信息)String errorMessage = "{\"code\":403,\"message\":\"權限不足,禁止訪問\"}";// 寫入響應體response.getWriter().write(errorMessage);// 關閉輸出流(重要!)response.getWriter().close();return flag;}}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {super.postHandle(request, response, handler, modelAndView);}
}
????????ApplicationStarter.java ?
package com.provider;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;/*** @author: Wulc* @createTime: 2025-05-01* @description:* @version: 1.0*/
@EnableDiscoveryClient
@EnableFeignClients({"com.action.feign"})
@SpringBootApplication
public class ApplicationStarter {public static void main(String[] args) {SpringApplication.run(ApplicationStarter.class, args);}
}
?????????application.yml
server:port: 8003spring:application:name: provider-server#eureka配置,服務注冊到哪?
eureka:client:service-url:defaultZone: http://localhost:8001/eureka/instance:#修改eureka上默認描述信息instance-id: ${spring.application.name}:${server.port}
3.5、SpringCloud網關
????????網關的作用是進行反向代理,把客戶端的請求轉發到對應的服務端。這里的網關是集成了身份認證服務。客戶端的請求發送到網關,會先經過網關的全局過濾器,在過濾器中先去判斷客戶端的請求中是否有攜帶token?如果攜帶了token,則去redis中驗證該token是否有效?如果token有效則過濾器放行,如果token失效了則使用refresh_token去調用http://localhost:8002/sso/oauth/token接口重新獲取token,獲取到新token后,過濾器再放行。
????????pom.xml ?
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>cloud-gateway</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.3</version><relativePath/> <!-- lookup parent from repository --></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- spring2.X集成redis所需common-pool2--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId><exclusions><!-- 排除可能引入的Spring MVC依賴 --><!-- Spring Cloud Gateway基于WebFlux響應式框架(非阻塞式),而Spring MVC是傳統的Servlet-based框架(阻塞式)。--><!-- 當兩者同時存在于classpath時,Spring Boot無法決定使用哪種Web服務器(Tomcat vs Netty),導致啟動失敗。--><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></exclusion></exclusions></dependency><!-- loadbalancer是負載均衡,對應yml中的lb--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies><!-- 使用dependencyManagement統一管理SpringCloud組件,集中定義所有SpringCloud相關組件的兼容版本,避免手動指定每個依賴的版本號,--><!-- 解決版本沖突問題。我這里使用了2021.0.3,對應的是Springboot2.6.x--><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
?????????application.yml
server:port: 8000spring:main:web-application-type: reactive # 強制使用WebFluxapplication:name: cloud-gateway-serviceprofiles:include: route #使用application-route.yml里面的配置eureka:client:service-url:defaultZone: http://localhost:8001/eureka/ # Eureka注冊中心地址register-with-eureka: truefetch-registry: trueinstance:prefer-ip-address: trueinstance-id: ${spring.application.name}:${server.port}
????????application-route.yml
spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowed-origin-patterns: '*' #允許所有的跨域allowed-headers: '*' #允許所有的頭allowed-methods: '*' #允許所有的請求方式discovery:locator:enabled: true # 開啟從注冊中心動態創建路由lower-case-service-id: true # 服務名小寫routes:- id: route1uri: lb://provider-server # lb表示負載均衡 loadbalancepredicates: #斷定,遵守哪些規則,就把請求轉發給wulc-test-consumer-server這個服務- Path=/api/provider/**filters:- StripPrefix=1
?????????pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>cloud-gateway</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.3</version><relativePath/> <!-- lookup parent from repository --></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- spring2.X集成redis所需common-pool2--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId><exclusions><!-- 排除可能引入的Spring MVC依賴 --><!-- Spring Cloud Gateway基于WebFlux響應式框架(非阻塞式),而Spring MVC是傳統的Servlet-based框架(阻塞式)。--><!-- 當兩者同時存在于classpath時,Spring Boot無法決定使用哪種Web服務器(Tomcat vs Netty),導致啟動失敗。--><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></exclusion></exclusions></dependency><!-- loadbalancer是負載均衡,對應yml中的lb--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies><!-- 使用dependencyManagement統一管理SpringCloud組件,集中定義所有SpringCloud相關組件的兼容版本,避免手動指定每個依賴的版本號,--><!-- 解決版本沖突問題。我這里使用了2021.0.3,對應的是Springboot2.6.x--><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
?????????RedisConfig.java
package com.gateway.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** @author Wulc* @date 2024/4/8 11:39* @description*/@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate1(RedisTemplate redisTemplate) {RedisSerializer stringSerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(stringSerializer);redisTemplate.setStringSerializer(stringSerializer);redisTemplate.setValueSerializer(stringSerializer);redisTemplate.setHashKeySerializer(stringSerializer);redisTemplate.setHashValueSerializer(stringSerializer);return redisTemplate;}
}
?????????GatewayGlobalFilter.java
package com.gateway.filter;import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Map;/*** @author: Wulc* @createTime: 2025-05-12* @description: 過濾器,當請求發送到網關時,先走過濾器進行身份認證,再路由轉發* @version: 1.0*/@Component
public class GatewayGlobalFilter implements GlobalFilter {@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//獲取請求頭HttpHeaders headers = exchange.getRequest().getHeaders();String authorization = headers.getFirst("Authorization");String accessToken = authorization.substring(7);String refreshAccessToken = headers.get("Refresh").get(0);String accessKey = "access" + ":" + accessToken;String refreshAccessKey = "refresh" + ":" + refreshAccessToken;RestTemplate restTemplate = new RestTemplate();//判斷access_token在redis中是否存在if (redisTemplate.opsForValue().get(accessKey) == null) {//如果access_token在redis中不存在,但refresh_access_token存在,則用refresh_access_token自動重新認證一下if (redisTemplate.opsForValue().get(refreshAccessKey) != null) {//構建表頭數據HttpHeaders requestHeaders = new HttpHeaders();String auth = "web" + ":" + "654321";String encodedAuth = Base64Utils.encodeToString(auth.getBytes());requestHeaders.set("Authorization", "Basic " + encodedAuth);// 構建表單數據MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();formData.add("refresh_token", refreshAccessToken);formData.add("grant_type", "refresh_token");// 構建請求實體HttpEntity<MultiValueMap<String, Object>> requestEntity =new HttpEntity<>(formData, requestHeaders);try {ResponseEntity<Map> responseEntity = restTemplate.exchange("http://localhost:8002/sso/oauth/token", HttpMethod.POST, requestEntity, Map.class);return chain.filter(exchange);} catch (Exception ex) {return sendErrorResponse(exchange, HttpStatus.UNAUTHORIZED, "請登錄");}}return sendErrorResponse(exchange, HttpStatus.UNAUTHORIZED, "請登錄");}//繼續后續處理return chain.filter(exchange);}/*** sendErrorResponse* <p>* description //返回錯誤信息** @param* @return* @throws* @author Wulc* @date 2025/5/12 22:30*/private Mono<Void> sendErrorResponse(ServerWebExchange exchange,HttpStatus status,String message) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(status);response.getHeaders().setContentType(MediaType.TEXT_PLAIN);byte[] bytes = message.getBytes(StandardCharsets.UTF_8);DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);return response.writeWith(Flux.just(buffer));}
}
?????????ApplicationStarter.java
package com.gateway;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** @author: Wulc* @createTime: 2025-05-12* @description:* @version: 1.0*/
//gateway服務一定要等其他服務啟動注冊eureka成功后,再最后啟動
@SpringBootApplication
@EnableDiscoveryClient
public class ApplicationStarter {public static void main(String[] args) {SpringApplication.run(ApplicationStarter.class, args);}
}
?????????啟動網關服務:
?????????先調用接口:http://localhost:8002/sso/oauth/token進行身份認證,并獲取token信息。
?????????使用從/oauth/token接口獲取的token訪問網關:http://localhost:8000/api/provider/getMsg 先經過網關的過濾器,根據token判斷用戶是否認證?如果是認證用戶,網關會根據yml里面配置的路由將/api/provider/getMsg請求轉發到相應的后端服務上。
?
????????如果username="wangwu",因為wangwu沒有在action-controller-server中checkAccessAction方法中配置權限,所以wangwu用戶是沒有訪問權限的。會被provider-server的攔截器給攔截掉。 ?
?????????只有當username="zhangsan"時,才有訪問權限。
????????以上就是基于OAuth2+SpringSecurity+Jwt+過濾器+攔截器+注解+feign+網關+Eureka實現的一個簡易身份認證和權限管理系統。
4、總結
? ? ? ? 實現一個鑒權系統其實只要有token+攔截器就行了,身份認證用token,權限控制用攔截器。使用OAuth2框架是為了應對不同的場景,比如隱式授權模式用來實現單點登錄,授權碼模式用于實現第三方登錄,即通過驗證服務器去代理客戶端進行身份驗證,而不是讓客戶端拿著token去身份認證。
? ? ? ? 而Spring Security本身提供了一個登錄頁面,但實際中不會用到。Spring Security提供了完整的鑒權、授權、會話管理、防護攻擊(如CRSF跨站請求偽造、XSS跨站腳本攻擊)。Spring Security默認開啟CRSF Token驗證防護,對所有的請求(Post、Put、Delete)統統要求攜帶有效token。防護XSS攻擊會設置一些內容安全策略,限制外部訪問,白名單黑名單等。
? ? ? ? 其實對于鑒權系統而言,最難反而是根據業務設計一個權限控制模型。常用的權限模型有RBAC和ABAC兩種。RBAC是基于角色的權限模型,角色是權限的集合,通過定義權限組(角色),把權限組授權給用戶。而ABAC是基于屬性的訪問控制,是在RBAC的基礎上更進一步細粒度的控制權限。比如某個用戶有訪問文檔庫的權限,這個可以用角色去授權文檔資源。每個用戶只能訪問自己所屬團隊的文檔,這個要基于團隊屬性進行授權。
? ? ? ? 在實際的授權中,通常會用到這些表:
- 用戶表:存儲用戶基本信息,用戶名&密碼等,敏感信息要加密處理。
- 角色表:存儲角色的定義,角色Id,角色名,角色說明等字段。
- 權限表:存儲系統中各種可被訪問的資源,權限Id,資源Id,資源名稱,資源操作,說明等字段。
- 角色權限表:角色Id,權限Id。
- 用戶角色表:用戶Id,角色Id。
????????以上這些是基于角色的訪問控制RBAC,是外部權限。
????????基于屬性的訪問控制ABAC,通常會寫在權限判斷的方法里,定制化更強一些,是內部權限。
????????外部權限+內部權限,RBAC+ABAC共同構成了權限控制。
? ? ? ? 至于實際過程中如何使用?這里舉一個簡單的例子:
@AccessActionControl(resource = {"1086"}, action = "read")這個注解會加在接口方法上,用于對接口方法進行權限控制。會傳入“資源resource”和“動作action”。根據“資源”和“動作”去“權限表”中獲取對應的權限Id,然后根據權限Id在“角色權限表”獲取哪些角色有該權限(記為arryRoles1)。根據用戶Id在“用戶角色表”在查詢該用戶有哪些角色(記為arrRoles2)。最后只要判斷arrRoles2和arryRoles1有沒有交集就行了。如果有交集就說明該用戶有權限,如果沒有就說明該用戶沒有權限。
?
5、參考資料
2、用戶認證和授權嗶哩嗶哩bilibili
springsecurity+jwt+oauth2.0入門到精通視頻教程【免費學習】嗶哩嗶哩bilibili
Spring Cloud 微服務安全:OAuth2 + JWT 實現認證與授權_springcloud oauth2 jwt-CSDN博客
OAuth2.0 實現單點登錄_oauth2.0單點登錄-CSDN博客
Sql Server數據庫實現表中字段的列加密研究_sql實現對密碼字段加密-CSDN博客
Spring Security實現從數據庫中訪問用戶名和密碼實現登錄_spring security5 實現數據庫登錄-CSDN博客
OAuth2.0系列之信息Redis存儲實踐(七) - smileNicky - 博客園
IDEA使用系列之導入外部jar包_idea添加外部jar包-CSDN博客