Răsfoiți Sursa

feat:网关-鉴权

wangbo 3 ani în urmă
părinte
comite
890da28a4e

+ 32 - 0
pom.xml

@@ -81,6 +81,38 @@
             <groupId>org.springframework.security</groupId>
             <artifactId>spring-security-oauth2-jose</artifactId>
         </dependency>
+        <!--启用 Spring Security 对 OAuth 2.0 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-oauth2-client</artifactId>
+        </dependency>
+        <!--激活TokenRelay-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-core</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-jose</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.3.2</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 9 - 4
src/main/java/com/inspur/smsb/gateway/SmsbGatewayApplication.java

@@ -2,16 +2,21 @@ package com.inspur.smsb.gateway;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
-import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.WebSession;
+import reactor.core.publisher.Mono;
 
 /**
  * 网关服务
  *
  * @author liangke
  */
-@EnableDiscoveryClient
-@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
+@SpringBootApplication(exclude = ReactiveUserDetailsServiceAutoConfiguration.class)
+@RestController
 public class SmsbGatewayApplication {
     public static void main(String[] args) {
         SpringApplication.run(SmsbGatewayApplication.class, args);

+ 54 - 0
src/main/java/com/inspur/smsb/gateway/config/AuthorizationManager.java

@@ -0,0 +1,54 @@
+package com.inspur.smsb.gateway.config;
+
+import cn.hutool.core.convert.Convert;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+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 org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+import reactor.core.publisher.Mono;
+
+import java.util.*;
+
+/**
+ * 鉴权管理器
+ */
+@Component
+@AllArgsConstructor
+@Slf4j
+public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
+    @Override
+    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
+        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
+        String path = request.getURI().getPath();
+        PathMatcher pathMatcher = new AntPathMatcher();
+        // todo 资源权限角色关系列表,需要初始化到容器中
+        Map<Object, Object> resourceRolesMap = new HashMap<>();
+        resourceRolesMap.put("/token/**","force");
+        resourceRolesMap.put("/**/orchestration/calendarRelease/**","delete-force");
+        Iterator<Object> iterator = resourceRolesMap.keySet().iterator();
+        List<String> authorities = new ArrayList<>();
+        while (iterator.hasNext()) {
+            String pattern = (String) iterator.next();
+            if (pathMatcher.match(pattern, path)) {
+                authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
+            }
+        }
+        Mono<AuthorizationDecision> authorizationDecisionMono = mono
+            .filter(Authentication::isAuthenticated)
+            .flatMapIterable(Authentication::getAuthorities)
+            .map(GrantedAuthority::getAuthority)
+            .any(roleId -> {
+                return authorities.contains(roleId);
+            })
+            .map(AuthorizationDecision::new)
+            .defaultIfEmpty(new AuthorizationDecision(false));
+        return authorizationDecisionMono;
+    }
+}

+ 32 - 0
src/main/java/com/inspur/smsb/gateway/config/CustomServerAccessDeniedHandler.java

@@ -0,0 +1,32 @@
+package com.inspur.smsb.gateway.config;
+
+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.access.AccessDeniedException;
+import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.Charset;
+
+/**
+ * 无权访问自定义响应
+ */
+@Component
+public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
+    @Override
+    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
+        ServerHttpResponse response=exchange.getResponse();
+        response.setStatusCode(HttpStatus.FORBIDDEN);
+        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+        response.getHeaders().set("Access-Control-Allow-Origin","*");
+        response.getHeaders().set("Cache-Control","no-cache");
+        String body= "没权限访问!";
+        DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
+        return response.writeWith(Mono.just(buffer));
+    }
+}

+ 37 - 0
src/main/java/com/inspur/smsb/gateway/config/RealmRoleConverter.java

@@ -0,0 +1,37 @@
+package com.inspur.smsb.gateway.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class RealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>>{
+
+    @Override
+    public Collection<GrantedAuthority> convert(Jwt jwt) {
+        final Map<String, List<String>> realmAccess = (Map<String, List<String>>) jwt.getClaims().get("realm_access");
+        List<GrantedAuthority> realmList = realmAccess.get("roles")
+            .stream()
+            .map(roleName -> roleName)
+            .map(SimpleGrantedAuthority::new)
+            .collect(Collectors.toList());
+        final Map<String, List<String>> resourceAccess = (Map<String, List<String>>) jwt.getClaims().get("resource_access");
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode token = mapper.convertValue(resourceAccess, JsonNode.class);
+        Set<String> rolesWithPrefix = new HashSet<>();
+        token.elements()
+            .forEachRemaining(e -> e.path("roles")
+                .elements()
+                .forEachRemaining(r -> rolesWithPrefix.add(r.asText())));
+        final List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(rolesWithPrefix.toArray(new String[0]));
+        realmList.addAll(authorityList);
+        return realmList;
+    }
+
+}

+ 50 - 0
src/main/java/com/inspur/smsb/gateway/config/ResourceServerConfig.java

@@ -0,0 +1,50 @@
+package com.inspur.smsb.gateway.config;
+
+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.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.ReactiveJwtAuthenticationConverterAdapter;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import reactor.core.publisher.Mono;
+import static org.springframework.security.config.Customizer.withDefaults;
+
+/**
+ * 资源服务器配置
+ */
+@AllArgsConstructor
+@Configuration
+@EnableWebFluxSecurity
+public class ResourceServerConfig {
+
+    private AuthorizationManager authorizationManager;
+
+    private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
+
+    @Bean
+    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
+        http.oauth2Login(withDefaults());
+        http.oauth2ResourceServer().jwt()
+            .jwtAuthenticationConverter(jwtAuthenticationConverter());
+        http.authorizeExchange()
+            .pathMatchers("").permitAll()
+            .anyExchange().access(authorizationManager)
+            .and()
+            .exceptionHandling()
+            .accessDeniedHandler(customServerAccessDeniedHandler) // 处理未授权
+            .and().csrf().disable();
+
+        return http.build();
+    }
+    @Bean
+    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
+        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new RealmRoleConverter());
+        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
+    }
+}

+ 41 - 0
src/main/java/com/inspur/smsb/gateway/filter/AuthorizationClient.java

@@ -0,0 +1,41 @@
+package com.inspur.smsb.gateway.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.rotation.AdapterTokenVerifier;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * 校验token
+ * @author wangbo13
+ */
+@Slf4j
+@Service
+public class AuthorizationClient {
+    @Value("${keycloak.realm}")
+    private String realm;
+    @Value("${keycloak.resource}")
+    private String resource;
+    @Value("${keycloak.auth-server-url}")
+    private String authServerUrl;
+
+    public boolean verifyToken(String token) {
+        AccessToken accessToken = null;
+        try {
+            AdapterConfig adapterConfig = new AdapterConfig();
+            adapterConfig.setRealm(realm);
+            adapterConfig.setResource(resource);
+            adapterConfig.setAuthServerUrl(authServerUrl);
+            adapterConfig.setDisableTrustManager(true);
+            KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(adapterConfig);
+            accessToken = AdapterTokenVerifier.verifyToken(token, deployment);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return accessToken != null ? false : true;
+    }
+}

+ 12 - 0
src/main/java/com/inspur/smsb/gateway/filter/WebFluxUserRequestInfoFilter.java

@@ -3,9 +3,12 @@ package com.inspur.smsb.gateway.filter;
 import com.google.common.base.Strings;
 import com.nimbusds.jose.JWSObject;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
 import org.springframework.cloud.gateway.filter.GlobalFilter;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.stereotype.Component;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
@@ -20,6 +23,10 @@ import java.text.ParseException;
 @Slf4j
 @Component
 public class WebFluxUserRequestInfoFilter implements GlobalFilter {
+
+    @Autowired
+    private AuthorizationClient authorizationClient;
+
     @Override
     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
         try {
@@ -28,6 +35,11 @@ public class WebFluxUserRequestInfoFilter implements GlobalFilter {
                 return chain.filter(exchange);
             }
             String realToken = token.replace("Bearer ", "");
+            ServerHttpResponse response = exchange.getResponse();
+            if(authorizationClient.verifyToken(realToken)){
+                response.setStatusCode(HttpStatus.UNAUTHORIZED);
+                return response.setComplete();
+            }
             JWSObject jwsObject = JWSObject.parse(realToken);
 
             ServerHttpRequest request = exchange.getRequest()

+ 6 - 0
src/main/resources/bootstrap.yml

@@ -4,6 +4,8 @@ server:
 spring:
   application:
     name: smsb-gateway
+  profiles:
+    active: test
   cloud:
     nacos:
       server-addr: 10.180.88.84:8060
@@ -12,3 +14,7 @@ spring:
       config:
         file-extension: yml
         refresh-enabled: true
+        namespace: 1365e7bc-51b7-44fe-a902-7327e4c9ed4a
+      discovery:
+        namespace: 1365e7bc-51b7-44fe-a902-7327e4c9ed4a
+