源代码链接
在SpringCloud中,实现授权功能有两种实现方式:
1.网关授权
基于网关授权我们又叫【基于路径匹配器授权】,请求在经过网关的时候校验当前请求的路径是否在用户拥有的资源路径中。
主要利用 ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) 方法进行校验。
这种方法主要是基于用户拥有的资源路径进行考量。
2.微服务授权
微服务授权我们又叫【基于方法拦截】,在需要进行权限校验的方法上加上SpringSecurity注解,判断当前用户是否有访问此方法的权限。
1.在secruity的配置类上加上注解 @EnableGlobalMethodSecurity(prePostEnabled = true) 启用springsecruity前/后注解
2.在需要权限的方法或者类上添加权限注解@PreAuthorize("hasAuthority('USER')")
当然也可以使用自定义注解或使用AOP进行拦截校验,这几种实现方式我们都统称为基于方法拦截。
这种方法一般会基于用户拥有的资源标识进行考量。
书接上文 oauth2 业务模块鉴权搭建 实现了在调用业务模块的接口时进行鉴权也就是【微服务授权】的方式。
接下来搭建【网关授权】
对认证服务器的数据库进行修改,添加路径的配置
认证服务器增加配置,将resource path和角色对应的内容读取到redis
gateway服务将从redis进行路径和角色的判断
@Configuration
public class ResourceApiPathConfig {
@Resource
private DataSource dataSource;
@Resource
RedisTemplate<String, Map<String, List<String>>> redisTemplate;
@Bean
public RedisResourceService resourcePathDetails() throws UnknownHostException {
return new RedisResourceService(dataSource, redisTemplate);
}
}
public class RedisResourceService {
private RedisTemplate<String, Map<String, List<String>>> redisTemplate;
private DataSource dataSource;
static final String AUTH_TO_RESOURCE = "auth_resource:";
public RedisResourceService(DataSource _dataSource,
RedisTemplate<String, Map<String, List<String>>> _redisTemplate) {
this.dataSource = _dataSource;
this.redisTemplate = _redisTemplate;
initData();
}
@SuppressWarnings("unchecked")
private void initData() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
Map<String, List<String>> resourceRolesMap = new TreeMap<>();
List<Map<String, Object>> list = jdbcTemplate.queryForList(
"SELECT CONCAT(\"/\",b.service,b.url) AS url,GROUP_CONCAT(c.name) AS role_names from rbac_role_resource a left join rbac_resource b ON a.resource_id = b.id left join rbac_role c ON a.role_id = c.id GROUP BY b.url");
for (Map<String, Object> m : list) {
System.out.print(m.get("url").toString());
resourceRolesMap.put(m.get("url").toString(), Arrays.asList(m.get("role_names").toString().split(",")));
}
redisTemplate.opsForHash().putAll(AUTH_TO_RESOURCE, resourceRolesMap);
// System.out.print("******************************set_ResourcePath");
}
}
创建gateway
http -d https://start.spring.io/starter.zip javaVersion==17 groupId==com.my.demo artifactId==gatewayService name==gateway-service baseDir==gateway-service bootVersion==2.6.6.RELEASE dependencies==cloud-gateway
修改POM
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.my.demo</groupId>
<artifactId>gatewayService</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
修改application.properties 配置里routes[0]的project-service可以仍采用 业务服务 里搭建的服务
server.port=8501
spring.application.name=gateway-service
spring.cloud.gateway.routes[0].id=project-service
spring.cloud.gateway.routes[0].uri=http://localhost:8601
spring.cloud.gateway.routes[0].predicates[0]=Path=/project/**
spring.cloud.gateway.routes[0].filters=StripPrefix=1
spring.cloud.gateway.routes[1].id=oauth-service
spring.cloud.gateway.routes[1].uri=http://localhost:8509
spring.cloud.gateway.routes[1].predicates[0]=Path=/oauth/**
security.oauth2.client.scope=server
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=123456
security.oauth2.client.access-token-uri=http://localhost:8509/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8509/oauth/authorize
security.oauth2.authorization.check-token-access=http://localhost:8509/oauth/check_token
security.oauth2.resource.token-info-uri=http://localhost:8509/oauth/check_token
security.oauth2.resource.user-info-uri=http://localhost:8509/oauth/check_user
security.oauth2.resource.prefer-token-info=true
secure.ignore.urls="/actuator/**","/oauth/**"
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=2000
配置gateway的鉴权
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private RedisTemplate redisTemplate;
private RedisConnectionFactory redisConnectionFactory;
static final String AUTH_TO_RESOURCE = "auth_resource:";
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerWebExchange exchange = authorizationContext.getExchange();
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String authorizationToken = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); // 从Header里取出token的值
if (StringUtils.isBlank(authorizationToken)) {
authorizationToken = request.getQueryParams().getFirst("access_token");
}
if (StringUtils.isBlank(authorizationToken)) {
log.warn("当前请求头Authorization中的值不存在");
return Mono.just(new AuthorizationDecision(false));
}
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
String token = authorizationToken.replace(OAuth2AccessToken.BEARER_TYPE + " ", "");
OAuth2Authentication oAuth2Authentication = redisTokenStore.readAuthentication(token);
Collection<GrantedAuthority> authorities = oAuth2Authentication.getAuthorities(); // 取到角色
Map<String, List<String>> resourceRolesMap = redisTemplate.opsForHash().entries(AUTH_TO_RESOURCE);
List<String> pathAuthorities = resourceRolesMap.get(path);
for (GrantedAuthority authority : authorities) {
if (pathAuthorities.contains(authority.getAuthority())) {
return Mono.just(new AuthorizationDecision(true));
}
}
return Mono.just(new AuthorizationDecision(false));
}
}
@AllArgsConstructor
@Configuration
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
http.authorizeExchange().pathMatchers(ArrayUtil.toArray(ignoreUrls, String.class)).permitAll()// 白名单配置
.anyExchange().access(authorizationManager)// 鉴权管理器配置
.and().exceptionHandling().and().csrf().disable();
return http.build();
}
}
通过 HTTPie 测试
//获取access_token
D:\Project\oauthService>http http://127.0.0.1:8509/oauth/token client_secret==123456 grant_type==password username==Test password==123456 client_id==client scope==server
HTTP/1.1 200
Cache-Control: no-store
Connection: keep-alive
Content-Type: application/json;charset=UTF-8
Keep-Alive: timeout=60
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
{
"access_token": "ec9055f9-8be7-4c40-98ad-d7f81c31137d",
"expires_in": 43199,
"refresh_token": "c113adb9-e192-49a2-bbf5-6cb11b8a3a3e",
"scope": "server",
"token_type": "bearer"
}
//通过网关访问project-service的鉴权方法
D:\Project\oauthService>http -v --auth-type=bearer --auth=ec9055f9-8be7-4c40-98ad-d7f81c31137d http://127.0.0.1:8501/project/config/set
GET /project/config/set HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer ec9055f9-8be7-4c40-98ad-d7f81c31137d
Connection: keep-alive
Host: 127.0.0.1:8501
User-Agent: HTTPie/3.1.0
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/plain;charset=UTF-8
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
content-length: 69
ec9055f9-8be7-4c40-98ad-d7f81c31137dconfig project have authorization
//通过网关访问project-service的鉴权错误的方法
D:\Project\oauthService>http -v --auth-type=bearer --auth=ec9055f9-8be7-4c40-98ad-d7f81c31137d http://127.0.0.1:8501/project/config/get
GET /project/config/get HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer ec9055f9-8be7-4c40-98ad-d7f81c31137d
Connection: keep-alive
Host: 127.0.0.1:8501
User-Agent: HTTPie/3.1.0
HTTP/1.1 500 Internal Server Error
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 144
Content-Type: application/json
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
{
"error": "Internal Server Error",
"path": "/project/config/get",
"requestId": "b9250aa3-2",
"status": 500,
"timestamp": ""
}
网友评论