源代碼鏈接
在SpringCloud中募强,實(shí)現(xiàn)授權(quán)功能有兩種實(shí)現(xiàn)方式:
1.網(wǎng)關(guān)授權(quán)
基于網(wǎng)關(guān)授權(quán)我們又叫【基于路徑匹配器授權(quán)】崇摄,請求在經(jīng)過網(wǎng)關(guān)的時候校驗(yàn)當(dāng)前請求的路徑是否在用戶擁有的資源路徑中逐抑。
主要利用 ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) 方法進(jìn)行校驗(yàn)。
這種方法主要是基于用戶擁有的資源路徑進(jìn)行考量汹粤。
2.微服務(wù)授權(quán)
微服務(wù)授權(quán)我們又叫【基于方法攔截】田晚,在需要進(jìn)行權(quán)限校驗(yàn)的方法上加上SpringSecurity注解贤徒,判斷當(dāng)前用戶是否有訪問此方法的權(quán)限。
1.在secruity的配置類上加上注解 @EnableGlobalMethodSecurity(prePostEnabled = true) 啟用springsecruity前/后注解
2.在需要權(quán)限的方法或者類上添加權(quán)限注解@PreAuthorize("hasAuthority('USER')")
當(dāng)然也可以使用自定義注解或使用AOP進(jìn)行攔截校驗(yàn)踢涌,這幾種實(shí)現(xiàn)方式我們都統(tǒng)稱為基于方法攔截睁壁。
這種方法一般會基于用戶擁有的資源標(biāo)識進(jìn)行考量潘明。
書接上文 oauth2 業(yè)務(wù)模塊鑒權(quán)搭建 實(shí)現(xiàn)了在調(diào)用業(yè)務(wù)模塊的接口時進(jìn)行鑒權(quán)也就是【微服務(wù)授權(quán)】的方式钉疫。
接下來搭建【網(wǎng)關(guān)授權(quán)】
對認(rèn)證服務(wù)器的數(shù)據(jù)庫進(jìn)行修改巢价,添加路徑的配置
數(shù)據(jù)庫Sql結(jié)構(gòu)
搭建認(rèn)證服務(wù)的過程
認(rèn)證服務(wù)器增加配置壤躲,將resource path和角色對應(yīng)的內(nèi)容讀取到redis
gateway服務(wù)將從redis進(jìn)行路徑和角色的判斷
@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");
}
}
創(chuàng)建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可以仍采用 業(yè)務(wù)服務(wù) 里搭建的服務(wù)
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的鑒權(quán)
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("當(dāng)前請求頭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)// 鑒權(quán)管理器配置
.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"
}
//通過網(wǎng)關(guān)訪問project-service的鑒權(quán)方法
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
//通過網(wǎng)關(guān)訪問project-service的鑒權(quán)錯誤的方法
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": ""
}