基于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

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

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

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博客

因为密码要加密存储,我这里用的是证书加密。

sql 复制代码
-- 创建数据库证书用于对密码进行加密
--查看数据库中的证书
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

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

bash 复制代码
server:
  port: 8002
  servlet:
    #为了防止一会在服务之间跳转导致Cookie打架(因为所有服务地址都是localhost,都会存JSESSIONID)
    #这里修改一下context-path,这样保存的Cookie会使用指定的路径,就不会和其他服务打架了
    #但是注意之后的请求都得在最前面加上这个路径
    context-path: /sso

spring:
  application:
    name: auth-service-server
  datasource:
    name: MyTestDataBase
    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    url: jdbc:sqlserver://127.0.0.1:1433;databaseName=MyTestDataBase
    username: wlc
    password: 123456
  redis:
    port: 6379
    database: 0
    host: 127.0.0.1
    password:

mybatis:
  mapper-locations: classpath:mapper/*.xml #注意:一定要对应mapper映射xml文件的所在路径

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/  # Eureka注册中心地址
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}

OAuth2Configuration.java

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 {
    @Resource
    private MyUserDetailsService myUserDetailsService;
    @Resource
    private AuthenticationManager manager;
    @Resource
    private TokenStore store;
    @Resource
    private JwtAccessTokenConverter converter;

    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .tokenServices(serverTokenServices())
                .userDetailsService(myUserDetailsService)
                .authenticationManager(manager);
    }

    /**
     * 这个方法是对客户端进行配置,一个验证服务器可以预设很多个客户端,
     * 之后这些指定的客户端就可以按照下面指定的方式进行验证
     * @param clients 客户端配置工具
     */
    @Override
    public 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");
    }

    @Override
    public 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);   // 添加刚刚的TokenStore
        services.setTokenEnhancer(converter);   // 添加Token增强,其实就是JwtAccessTokenConverter,增强是添加一些自定义的数据到JWT中
        services.setAccessTokenValiditySeconds(60);    //访问token有效期20秒
        services.setRefreshTokenValiditySeconds(120);    //刷新token有效期120秒
        services.setSupportRefreshToken(true);
        return services;
    }
}

SecurityConfiguration.java

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 {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //从数据库中获取用户信息
        auth.userDetailsService(myUserDetailsService).passwordEncoder(encoder);
    }

    @Bean   // 这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }

    /***************************** JWT配置  ************************************/
    @Bean("tokenConverter")
    public JwtAccessTokenConverter tokenConverter(){  // Token转换器,将其转换为JWT
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("wlcKey");   // 这个是对称密钥,一会资源服务器那边也要指定为这个
        return converter;
    }

    //token存放在哪里,放在Redis里面
    @Bean
    public TokenStore tokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

UserInfoDTO.java

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

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

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 {
    @Autowired
    private UserMapper userMapper;
    /**
     * loadUserByUsername
     *
     * description 从数据库中根据用户名获取用户信息,并转为Spring Security的User
     * @param username
     * @return
     * @throws
     * @author Wulc
     * @date 2025/5/12 11:01
     */
    @Override
    public 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

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 复制代码
<?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">
        SELECT
            id,
            userName,
            CONVERT(
                varchar(100),
                DecryptByCert
                    (
                        CERT_ID('MyCert'),pwd
                    )
                ) as pwd
        FROM 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

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

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)

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>
    <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

bash 复制代码
server:
  port: 8004

spring:
  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

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")
    @Override
    public 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

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)

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>
    <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 复制代码
<?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>-->
<!--            &lt;!&ndash;            正式版&ndash;&gt;-->
<!--            <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 复制代码
<?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

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;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截所有请求
        registry.addInterceptor(accessActionInterceptor).addPathPatterns("/**");
    }
}

ProviderController.java

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

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 {
    @Autowired
    private AccessActionFeign accessActionFeign;
    //在请求被处理之前,调用action-controller的权限判断接口,如果有权限就放行,没有权限就拦截
    @Override
    public 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;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }
}

ApplicationStarter.java

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

bash 复制代码
server:
  port: 8003

spring:
  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 复制代码
<?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

bash 复制代码
server:
  port: 8000


spring:
  main:
    web-application-type: reactive  # 强制使用WebFlux
  application:
    name: cloud-gateway-service
  profiles:
    include: route  #使用application-route.yml里面的配置

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8001/eureka/  # Eureka注册中心地址
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}

application-route.yml

bash 复制代码
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: '*'  #允许所有的跨域
            allowed-headers: '*'  #允许所有的头
            allowed-methods: '*'  #允许所有的请求方式
      discovery:
        locator:
          enabled: true  # 开启从注册中心动态创建路由
          lower-case-service-id: true  # 服务名小写
      routes:
        - id: route1
          uri: lb://provider-server  # lb表示负载均衡 loadbalance
          predicates: #断定,遵守哪些规则,就把请求转发给wulc-test-consumer-server这个服务
            - Path=/api/provider/**
          filters:
            - StripPrefix=1

pom.xml

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

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 {
    @Bean
    public 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

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 {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public 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

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博客

相关推荐
坐吃山猪4 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫4 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao5 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区6 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT7 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy7 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss8 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续9 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
benben0449 小时前
ReAct模式解读
java·ai