基于OAuth2+SpringSecurity+Jwt实现身份认证和权限管理后端服务
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博客
相关文章:

基于OAuth2+SpringSecurity+Jwt实现身份认证和权限管理后端服务
1、简介 本文讲述了如何实现简易的后端鉴权服务。所谓“鉴权”,就是“身份鉴定”“权限判断”。涉及的技术有:OAuth2、SpringSecurity、Jwt、过滤器、拦截器。OAuth2用于授权,使用Jwt签发Access Token和Refresh Token,并管理token…...
vue3 el-table实现字段可编辑
在Vue 3中,如果你想让el-table(Element Plus的表格组件)的字段可编辑,你可以通过以下方式来实现: 使用cell-mouse-enter和cell-mouse-leave事件动态显示编辑图标或控件 你可以在鼠标进入单元格时显示一个编辑图标或输…...
基于React的高德地图api教程005:圆形标记的绘制、删除、修改
文章目录 5、圆形绘制和编辑5.1 绘制圆5.1.1 添加绘制圆按钮5.1.2 点击地图绘制圆心5.2 修改圆5.2.1 修改圆的基本信息5.2.2 修改圆的位置和大小5.3 删除圆标记5.4 定位到圆5.5 代码下载5.05、圆形绘制和编辑 5.1 绘制圆 5.1.1 添加绘制圆按钮 实现代码: {!isActiveDrawCi…...

liunx定时任务,centos定时任务
yum install cronie crontabs -y直接运行 crond -n 在前台运行 crond -i 守护进程在没有inotify支持的情况下运行systemctl service crond start # 启动服务 systemctl enable crond.service # 设置开机自启 sudo systemctl restart crond # 重启 cron 服务systemctl serv…...

三种嵌入式开发常用的组网方式
一、三种嵌入式开发常用的组网方式 这里记录了三种嵌入式开发常用的网络环境,最终目标也就是让开发板、虚拟机、物理机在同一个局域网下。一般的网络环境下都非常容易实现,但是对于学生校园网可得想些法子了,因为校园网一般会有设备连接数限…...

ubuntu 20.04 ping baidu.coom可以通,ping www.baidu.com不通 【DNS出现问题】解决方案
ping baidu.coom可以通,ping www.baidu.com不通【DNS出现问题】解决方案 检查IPV6是否有问题 # 1. 检查 IPv6 地址,记住网络接口的名称 ip -6 addr show# 2. 测试本地 IPv6,eth0换成自己的网络接口名称 ping6 ff02::1%eth0# 3. 检查路由 ip…...

城市排水管网流量监测系统解决方案
一、方案背景 随着工业的不断发展和城市人口的急剧增加,工业废水和城市污水的排放量也大量增加。目前,我国已成为世界上污水排放量大、增加速度快的国家之一。然而,总体而言污水处理能力较低,有相当部分未经处理的污水直接或间接排…...

Ubuntu Desktop QEMU/KVM中使用Ubuntu Server 22.04配置k8s集群
Ubuntu Desktop QEMU/KVM中使用Ubuntu Server 22.04配置k8s集群 1.本机部署个代理 这里我是自己用dockersing-box 部署的 http://192.168.2.105:10808 如果 用v2rayN等软件部署的,记得开启局域网共享 2.安装虚拟机 我本机使用的是Ubuntu Desktop ,这里在QEMU/K…...

YOLOv8 在单片机上的几种部署方案
YOLOv8 在单片机上的部署方案 单片机资源(如内存、计算能力)有限,直接部署完整的 YOLOv8 模型并不现实。不过,我们可以通过模型量化、优化和使用轻量级框架来实现简化版的目标检测。下面为你介绍几种可行的方案: 方案…...

后端框架(1):Mybatis
什么是框架? 盖高楼,框架结构。 框架结构就是高楼的主体,基础功能。 把很多基础功能已经实现了(封装了)。 在基础语言之上,对各种基础功能进行封装,方便开发者,提高开发效率。 mybatis:对jd…...

linux下tcp/ip网络通信笔记1,
本文章主要为博主在学习网络通信的笔记一个Udp_echo_server,和client的代码实现 1,网络发展,网络协议,意识到网络通信——不同主机的进程间通信, 2,学习如何在应用层调用系统提供的接口进行通信,echo_Udp…...
SqlHelper 实现类,支持多数据库,提供异步操作、自动重试、事务、存储过程、分页、缓存等功能。
/// <summary> /// SqlHelper 实现类,支持多数据库,提供异步操作、自动重试、事务、存储过程、分页、缓存等功能。 /// </summary> public class SqlHelper : IDbHelper {private readonly IDbConnectionFactory _connectionFactory;private…...

语音识别——声纹识别
通过将说话人的声音与数据库中的记录声音进行比对,判断说话人是否为数据库白名单中的同一人,从而完成语音验证。目前,3D-Speaker 声纹验证的效果较为出色。 3D-Speaker 是一个开源工具包,可用于单模态和多模态的说话人验证、说话…...

window 显示驱动开发-报告图形内存(三)
图形内存报告示例 示例 1:笔记本电脑上的 128 MB 专用板载图形内存 以下屏幕截图显示了使用 Intel Iris 离散图形适配器运行 Windows 11 的 Surface 笔记本电脑的计算图形内存数。 适配器的可用内存总数为 16424 MB,用于图形用途,细分如下&…...
安全运维 -- linux磁盘挂载到windows
0x00 背景 一个需求需要将linux服务器文件同步到windows。在此过程中遇到了三连报错,特此记录一下。 0x00 error 1 一开始死活挂不上,报错 mount: /mnt/Folder: mount(2) system call failed: No route to host. 一开始以为是dns问题,其实…...
使用 Apache POI 生成 Word 文档
创建一个包含标题、段落和表格的简单文档。 步骤 1:添加依赖 确保你的项目中已经添加了 Apache POI 的依赖。如果你使用的是 Maven,可以在 pom.xml 中添加以下内容: <dependency><groupId>org.apache.poi</groupId>...
高防服务器流量“清洗”什么意思
在当今数字化的时代,网络安全成为了备受关注的焦点。其中,高防服务器流量“清洗”这个概念,对于许多朋友来说可能还比较陌生。今天,就让我们一起来揭开它神秘的面纱。 首先,咱们得明白,高防服务器流量“清…...

UE5 GAS框架解析内部数据处理机制——服务器与客户端
当, gas通过点击鼠标光标触发事件时,内部的处理机制。 当通过点击事件,命中中目标时, 可获取到对应的TargetData 目标数据。处理相应的操作。 仅有本地的客户端的情况下。命中并不会有什么异常。 当存在服务器时, 服…...
Unity实用技能-UI定位总结
实用技能系列 Unity实用技能-UI滑动条技能总结Unity实用技能-UI规范总结Unity实用技能-协作规范总结Unity实用技能-UI与粒子效果总结 文章目录 实用技能系列前言什么是定位UI怎么实现定位UI总结 前言 本周接触了UI和定位相关的工作,记录一下 什么是定位UI 一般就是…...

开源GPU架构RISC-V VCIX的深度学习潜力测试:从RTL仿真到MNIST实战
点击 “AladdinEdu,同学们用得起的【H卡】算力平台”,H卡级别算力,按量计费,灵活弹性,顶级配置,学生专属优惠。 一、开篇:AI芯片架构演变的三重挑战 (引述TPUv4采用RISC-V的行业案…...
服务间的“握手”:OpenFeign声明式调用与客户端负载均衡
现在,假设我们有一个新的order-service,它在创建订单时需要获取用户信息。 如果order-service直接硬编码user-service的IP和端口进行调用,会面临以下问题: 缺乏弹性: 如果user-service实例的IP或端口发生变化(在云环境…...
26、DAPO论文笔记(解耦剪辑与动态采样策略优化,GRPO的改进)
DAPO论文笔记 1、项目背景与目标2、DAPO算法与关键技术3、过长响应奖励塑形(Overlong Reward Shaping)**一、问题背景:截断惩罚的缺陷****二、解决方案:分层惩罚与软截断策略**1. **过长过滤:屏蔽无效惩罚**2. **软过长…...
JQuery 禁止页面滚动(防止页面抖动)
// 禁止页面滑动 function unScroll() {const width $(body).width();$(body).css(width, width px);$(body).css(overflow-y, hidden); }// 移除禁止页面滑动 function reUnScroll() {$(body).css(overflow-y, auto);$(body).css(width, ); }使用场景:鼠标局部滑…...
Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(七)
Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(七) 在 Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(六)-CSDN博客 的基础上改进,主要是…...
Halcon与C#:工业级机器视觉开发
Halcon(由MVTec开发)是一款广泛应用于工业机器视觉的高性能软件库,支持C#、C、Python等多种语言。以下是基于C#的Halcon开发详解,涵盖环境配置、核心流程、关键API及最佳实践。 1. 开发环境配置 1.1 安装Halcon …...
Unity序列化字段、单例模式(Singleton Pattern)
一、序列化字段 在Unity中,序列化字段是一个非常重要的概念,主要用于在Unity编辑器中显示和编辑类的成员变量,或者在运行时将对象的状态保存到文件或网络中。 1.Unity序列化字段的作用 在编辑器中显示和编辑字段:默认情况下&…...

【工具】Windows|外接的显示器怎么用软件调亮度(Brightness Slider)
文章目录 工具安装及使用Twinkle Tray:Brightness Slider补充背景知识1. DDC/CI(Display Data Channel Command Interface)2. WMI(Windows Management Instrumentation)3. Twinkle Tray如何结合两者?对比总…...
在 Java MyBatis 中遇到 “操作数类型冲突: varbinary 与 float 不兼容” 的解决方法
在 MyBatis 中遇到 “操作数类型冲突: varbinary 与 float 不兼容” 错误,通常是因为当字段值为 null 时,MyBatis 无法正确推断其 JDBC 类型,导致向数据库传递 null 值时类型不匹配。以下是原因分析和解决方案: 问题原因 未指定 j…...
系统架构设计(十四):解释器风格
概念 解释器风格是一种将程序的每个语句逐条读取并解释执行的体系结构风格。程序在运行时不会先被编译为机器码,而是动态地由解释器分析并执行其语义。 典型应用:Python 解释器、JavaScript 引擎、Bash Shell、SQL 引擎。 组成结构 解释器风格系统的…...

【Nextcloud】使用 LNMP 架构搭建私有云存储:Nextcloud 实战指南
目录 一、环境准备与基础配置 1. 系统环境要求 2. 初始化系统配置 二、搭建 LNMP 基础架构 1. 一键安装 LNMP 组件 2. 启动数据库服务 三、部署 Nextcloud 存储服务 1. 上传并解压安装包 2. 设置目录权限(测试环境配置) 3. 配置 MariaDB 数据库…...