前言
通常微服务的认证和授权思路有两种:
- 网关只负责转发请求,认证鉴权交给每个微服务控制
- 统一在网关层面认证鉴权,微服务只负责业务
第二种方案的流程图
![]()
采用技术栈
父工程依赖及统一版本
附:父工程依赖
<packaging>pom</packaging>
<properties>
<fate.project.version>1.0.0</fate.project.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven.plugin.version>3.8.1</maven.plugin.version>
<spring.boot.version>2.6.3</spring.boot.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
<alibaba.nacos.version>1.4.2</alibaba.nacos.version>
<alibaba.sentinel.version>1.8.3</alibaba.sentinel.version>
<alibaba.dubbo.version>2.7.15</alibaba.dubbo.version>
<alibaba.rocketMq.version>4.9.2</alibaba.rocketMq.version>
<alibaba.seata.version>1.4.2</alibaba.seata.version>
<mybatis.plus.version>3.5.1</mybatis.plus.version>
<knife4j.version>3.0.2</knife4j.version>
<swagger.version>3.0.0</swagger.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- springBoot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- springCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
1. 搭建Oauth2-server
1.1 oauth2-server 依赖
引用版本可查看上面的父工程依赖
<dependencies>
<dependency>
<groupId>top.fate</groupId>
<artifactId>fate-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--排除logback-->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--添加log4j2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--SpringBoot2.4.x之后默认不加载bootstrap.yml文件,需要在pom里加上依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>${alibaba.nacos.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<!-- zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.9.3</version>
</dependency>
<!--security -->
<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>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
</dependencies>
1.2 bootstrap.yml
server:
port: 9999
url:
nacos: localhost:8848
spring:
application:
name: oauth2-server #实例名
profiles:
active: dev
cloud:
nacos:
discovery:
#集群环境隔离
cluster-name: shanghai
#命名空间
namespace: ${spring.profiles.active}
#持久化实例 ture为临时实例 false为持久化实例 临时实例发生异常直接剔除, 而持久化实例等待恢复
ephemeral: true
#注册中心地址
server-addr: ${url.nacos}
config:
namespace: ${spring.profiles.active}
file-extension: yaml
#配置中心地址
server-addr: ${url.nacos}
extension-configs[0]:
data-id: mysql-oauth2.yaml
group: DEFAULT_GROUP
refresh: false
extension-configs[1]:
data-id: log.properties
group: DEFAULT_GROUP
refresh: false
extension-configs[2]:
data-id: zipkin.yaml
group: DEFAULT_GROUP
refresh: false
extension-configs[3]:
data-id: mybatis-plus.yaml
group: DEFAULT_GROUP
refresh: false
extension-configs[4]:
data-id: redis.yaml
group: DEFAULT_GROUP
refresh: false
mysql-oauth2.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/oauth?useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
log.properties
logging.level.root=info
zipkin.yaml
spring:
zipkin:
base-url: http://127.0.0.1:9411
sender:
type: web
sleuth:
sampler:
probability: 1.0
mybatis-plus.yaml
mybatis-plus:
mapper-locations: classpath:mapper/*/*.xml,mapper/*.xml
global-config:
db-config:
id-type: auto
field-strategy: NOT_EMPTY
db-type: MYSQL
configuration:
map-underscore-to-camel-case: true
call-setters-on-nulls: true
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
redis.yaml
spring:
redis:
host: localhost
port: 6379
1.3 keytool生成RSA证书
在jdk/bin目录下执行该命令,生成jks文件之后复制到项目中resources中
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks -keypass 123456
1.4 SysUserServiceImpl 用户信息实现类
实现Spring Security的UserDetailsService接口,用于加载用户信息
package top.fate.service.impl;
import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import top.fate.domain.SecurityUser;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* <p>
* 用户信息表 服务实现类
* </p>
*
* @author fate急速出击
* @since 2022-05-13
*/
@Service
public class SysUserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return SecurityUser.builder()
.userId(UUID.randomUUID().toString().replaceAll("-",""))
.username("admin")
.password(new BCryptPasswordEncoder().encode("123456"))
.authorities(AuthorityUtils.createAuthorityList("user", "admin"))
.build();
}
}
SecurityUser 用户封装类
package top.fate.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 存储用户的详细信息,实现UserDetails,后续有定制的字段可以自己拓展
* @auther:Wangxl
* @Emile:18335844494@163.com
* @Time:2022/5/13 11:36
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SecurityUser implements UserDetails {
private String userId;
//用户名
private String username;
//密码
private String password;
//权限+角色集合
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 账户是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否未被锁
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
## 1.5 JWT内容增强器
package top.fate.component;
import org.s
pringframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import top.fate.domain.SecurityUser;
import java.util.HashMap;
import java.util.Map;
/**
- JWT内容增强器
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/13 11:32
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>();
//把用户ID设置到JWT中
info.put("id", securityUser.getUserId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
## 1.6 Oauth2ServerConfig 认证服务器配置
> 加载用户信息的服务UserServiceImpl及RSA的钥匙对KeyPair
package top.fate.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import top.fate.component.JwtTokenEnhancer;
import top.fate.service.impl.SysUserServiceImpl;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
/**
- 认证服务器配置
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/13 11:36
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final SysUserServiceImpl userDetailsService;
private final AuthenticationManager authenticationManager;
private final JwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-app")
.secret(passwordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService) //配置加载用户信息的服务
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
@Bean
public KeyPair keyPair() {
//从classpath下的证书中获取秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}
}
## 1.7 获取RSA公钥接口
package top.fate.controller;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
/**
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/13 17:48
*/
@RestController
@RequestMapping(value = "rsa")
public class KeyPairController {
@Autowired
private KeyPair keyPair;
@GetMapping(value = "publicKey")
public Map<String, Object> getKey(){
RSAPublicKey aPublic = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(aPublic).build();
return new JWKSet(key).toJSONObject();
}
}
## 1.8 配置Spring Security,允许获取公钥接口的访问
package top.fate.config;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
- SpringSecurity配置
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/14 15:36
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
## 1.9 初始化用户权限demo
package top.fate.service.impl;
import cn.hutool.core.collection.CollUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import top.fate.model.SysConstant;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/14 10:33
*/
@Service
public class ResourceServiceImpl {
private Map<String, List<String>> resourceRolesMap;
@Resource
private RedisTemplate<String,Object> redisTemplate;
@PostConstruct
public void initData() {
resourceRolesMap = new TreeMap<>();
resourceRolesMap.put("/user/tb-user/list", CollUtil.toList("ADMIN"));
resourceRolesMap.put("/order/order/getUserService", CollUtil.toList("ADMIN", "ROOT"));
redisTemplate.opsForHash().putAll("oauth2:oauth_urls", resourceRolesMap);
}
}
## 1.10 Redis相关配置
package top.fate.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
- Redis相关配置
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/16 14:29
*/
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
# 2. gateway
## 2.1 网关依赖
<!--网关依赖gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<!-- Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
<version>${spring-cloud-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>${alibaba.nacos.version}</version>
</dependency>
<!--SpringBoot2.4.x之后默认不加载bootstrap.yml文件,需要在pom里加上依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 加入 log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-dubbo</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- oauth2 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.9.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
<dependency>
<groupId>top.fate</groupId>
<artifactId>fate-common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
## 2.2 applicaion.yaml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9999/rsa/publicKey'
secure:
ignore:
urls: #配置白名单路径
- "/actuator/**"
- "/oauth2-server/oauth/token"
- "/oauth2-server/**"
- "/order/**"
## 2.3 bootstrap.yaml
logging:
file:
# 配置日志的路径,包含 spring.application.name
path: ${spring.application.name}
url:
nacos: localhost:8848
spring:
application:
name: gateway #实例名
profiles:
active: dev
cloud:
nacos:
discovery:
#集群环境隔离
cluster-name: shanghai
#命名空间
namespace: ${spring.profiles.active}
#持久化实例 ture为临时实例 false为持久化实例 临时实例发生异常直接剔除, 而持久化实例等待恢复
ephemeral: true
#注册中心地址
server-addr: ${url.nacos}
config:
namespace: ${spring.profiles.active}
file-extension: yaml
#配置中心地址
server-addr: ${url.nacos}
extension-configs[0]:
data-id: gateway.yaml
group: DEFAULT_GROUP
refresh: false
extension-configs[1]:
data-id: sentinel.yaml
group: DEFAULT_GROUP
refresh: false
extension-configs[2]:
data-id: log.properties
group: DEFAULT_GROUP
refresh: false
extension-configs[3]:
data-id: redis.yaml
group: DEFAULT_GROUP
refresh: false
### gateway.yaml
server:
port: 30001
spring:
cloud:
gateway:
enabled: true
discovery:
locator:
lower-case-service-id: true
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- StripPrefix=1
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- StripPrefix=1
- id:
uri: lb://oauth2-server
predicates:
- Path=/oauth2-server/**
filters:
- StripPrefix=1
### sentinel.yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
### log.properties
logging.level.root=info
### redis.yaml
spring:
redis:
host: localhost
port: 6379
## 2.4 鉴权管理器
package top.fate.authorization;
import cn.hutool.core.convert.Convert;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;
/**
- 鉴权管理器,用于判断是否有资源的访问权限
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/7 9:27
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
//从Redis中获取当前路径可访问角色列表
URI uri = authorizationContext.getExchange().getRequest().getURI();
Object obj = redisTemplate.opsForHash().get("oauth2:oauth_urls", uri.getPath());
List<String> authorities = Convert.toList(String.class, obj);
authorities = authorities.stream().map(i -> i = "ROLE_" + i).collect(Collectors.toList());
//认证通过且角色匹配的用户可访问当前路径
return mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}
## 2.5 自定义返回结果:没有登录或token过期时
package top.fate.component;
import cn.hutool.json.JSONUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import top.fate.api.R;
import top.fate.api.ResultCode;
import java.nio.charset.Charset;
/**
- 自定义返回结果:没有登录或token过期时
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/15 19:37
*/
@Component
public class RestAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
String body= JSONUtil.toJsonStr(R.fail(ResultCode.UN_AUTHORIZED.getCode(),ResultCode.UN_AUTHORIZED.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}
## 2.6 自定义返回结果:没有权限访问时
package top.fate.component;
import cn.hutool.json.JSONUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import top.fate.api.R;
import top.fate.api.ResultCode;
import java.nio.charset.Charset;
/**
- 自定义返回结果:没有登录或token过期时
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/15 19:37
*/
@Component
public class RestAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
String body= JSONUtil.toJsonStr(R.fail(ResultCode.UN_AUTHORIZED.getCode(),ResultCode.UN_AUTHORIZED.getMessage()));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
return response.writeWith(Mono.just(buffer));
}
}
## 2.7 网关白名单配置
package top.fate.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
- 网关白名单配置
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/16 14:27
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix="secure.ignore")
public class IgnoreUrlsConfig {
private List<String> urls;
}
## 2.8 Redis相关配置
package top.fate.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
- Redis相关配置
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/16 14:29
*/
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
## 2.9 资源服务器配置
package top.fate.config;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import top.fate.authorization.AuthorizationManager;
import top.fate.component.RestAuthenticationEntryPoint;
import top.fate.component.RestfulAccessDeniedHandler;
import top.fate.filter.IgnoreUrlsRemoveJwtFilter;
/**
- 资源服务器配置
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/16 14:31
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
//对白名单路径,直接移除JWT请求头
http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
.and().csrf().disable();
return http.build();
}
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
## 2.10 将登录用户的JWT转化成用户信息的全局过滤器
package top.fate.filter;
import cn.hutool.core.util.StrUtil;
import com.nimbusds.jose.JWSObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.text.ParseException;
/**
- 将登录用户的JWT转化成用户信息的全局过滤器
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/16 12:31
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
try {
//从token中解析用户信息并设置到Header中去
String realToken = token.replace("Bearer ", "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
e.printStackTrace();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
## 2.11 白名单路径访问时需要移除JWT请求头
package top.fate.filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import top.fate.config.IgnoreUrlsConfig;
import java.net.URI;
import java.util.List;
/**
- 白名单路径访问时需要移除JWT请求头
- @auther:Wangxl
- @Emile:18335844494@163.com
- @Time:2022/5/16 16:42
*/
@Component
public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
PathMatcher pathMatcher = new AntPathMatcher();
//白名单路径移除JWT请求头
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
request = exchange.getRequest().mutate().header("Authorization", "").build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
}
return chain.filter(exchange);
}
}
# 测试
> 访问 [http://localhost:30001/oauth2-server/oauth/token](http://localhost:30001/oauth2-server/oauth/token)
> 密码模式
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/d5ee4cef0f1b481081eb2aa2f2f51674.png)
> 刷新token
> ![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/9fe1bb39a9a841e385b0740f0a96fb51.png)