paulwong

OAUTH2 - SPRING SECURITY + KEYCLOAK

根据OAUTH2协议,如果需要用户协助的,则使用authorization_code流程,此时需要用户登录页面、CLIENT SERVER、RESOURCE SERVER和AUTHORIZATION SERVER,其中CLIENT SERVER是通过http调用RESOURCE SERVER的api,AUTHORIZATION SERVER使用现成的KEYCLOAK。

如果不需要用户协助的,即SERVER对SERVER的,则适用client_credentials流程,此时需要CLIENT SERVER、RESOURCE SERVER和AUTHORIZATION SERVER。

通常HTTP请求会在HEADER中夹带ACCESS_TOKEN,格式为JWT。

RESOURCE SERVER是如何知道该次的HTTP请求是合法的呢,只需用AUTHORIZATION SERVER提供的PUBLIC KEY能正常解密ACCESS_TOKEN,且不过期,则是合法的请求。

如果需要做授权认证,则检查JWT中的ROLE字符是否是事先定义的字符,如是则认为有权访问,否则拒绝。

RESOURCE SERVER

pom.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 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.5.6</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.paul</groupId>
    <artifactId>test-oauth2-department-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test-oauth2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </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-oauth2-resource-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>ZIP</layout>
                    <excludes>
                        <exclude>
                            <groupId>*</groupId>
                            <artifactId>*</artifactId>
                        </exclude>
                    </excludes>
                    <includes>
                        <include>
                            <groupId>com.paul</groupId>
                        </include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.yaml
server:
   port: 8281
   
spring:
  security:
    oauth2:
      #check OAuth2ResourceServerProperties
      resourceserver:
        jwt:
          #jwk-set-uri: "${rest.security.issuer-uri}/protocol/openid-connect/certs"
          issuer-uri: "${rest.security.issuer-uri}"
          #public-key-location: "classpath:/key.pub"


#Logging Configuration

logging:
   level:
      org.springframework.boot.autoconfigure.logging: INFO
      org.springframework.security: DEBUG
      com.paul: DEBUG
      root: INFO

java配置文件
package com.paul.testoauth2.config;

import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.paul.testoauth2.oauth2.converter.KeycloakRealmRoleConverter;
import com.paul.testoauth2.oauth2.converter.UsernameSubClaimAdapter;

@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .authorizeRequests(
                a -> a.antMatchers("/", "/error", "/webjars/**")
                      .permitAll()
                      .antMatchers(HttpMethod.GET, "/protected/**").hasRole("READ_DEPARTMENT")
                      .anyRequest()
                      .authenticated()
             )
            .exceptionHandling(
                e -> e//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                      .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
//                      .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            )
            .oauth2ResourceServer(
                o -> o.jwt(
                          j -> j.jwtAuthenticationConverter(jwtAuthenticationConverter(null))
                                  .decoder(jwtDecoder(null))
                       )
             )
//            .decoder(jwtDecoder(null))
            ;
        // @formatter:on
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter(ObjectMapper objectMapper) {
        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter(objectMapper));
        
        return jwtConverter;
    }
    
    //below is auto configured at OAuth2ResourceServerJwtConfiguration
    @Bean
    public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
        String issuerUri = properties.getJwt().getIssuerUri();
        // Use preferred_username from claims as authentication name, instead of UUID subject
        NimbusJwtDecoder jwtDecoder = 
                JwtDecoders.<NimbusJwtDecoder>fromIssuerLocation(issuerUri);
        jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
        return jwtDecoder;
    }

}


CLIENT SERVER

pom.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 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.5.6</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.paul</groupId>
    <artifactId>test-oauth2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test-oauth2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </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-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>ZIP</layout>
                    <excludes>
                        <exclude>
                            <groupId>*</groupId>
                            <artifactId>*</artifactId>
                        </exclude>
                    </excludes>
                    <includes>
                        <include>
                            <groupId>com.paul</groupId>
                        </include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.yaml
server:
   port: 8280
   
spring:
  security:
    oauth2:
      #check OAuth2ResourceServerProperties
      resourceserver:
        jwt:
          #jwk-set-uri: "${rest.security.issuer-uri}/protocol/openid-connect/certs"
          issuer-uri: "${rest.security.issuer-uri}"
          #public-key-location: "classpath:/key.pub"
               
      #check OAuth2ClientProperties

      client:
        registration:
          my-client-1:
            client-id: "test-employee-service"
            client-secret: ${client-secret.my-client-1}
            client-name: "test-employee-service"
            provider: "keycloak-provider"
            scope: "openid"
            #redirect-uri: "https://my-redirect-uri.com"
            client-authentication-method: "basic"
            authorization-grant-type: "client_credentials"
          app-springboot-confidential:
            client-id: "app-springboot-confidential"
            client-secret: ${client-secret.app-springboot-confidential}
            client-name: "app-springboot-confidential"
            provider: "keycloak-provider"
            scope: "openid"
            redirect-uri: "https://my-redirect-uri.com"
            client-authentication-method: "basic"
            authorization-grant-type: "authorization_code"

        provider:
          keycloak-provider:
            issuer-uri : "${rest.security.issuer-uri}"
            user-name-attribute: "preferred_username"


#Logging Configuration
logging:
   level:
      org.springframework.boot.autoconfigure.logging: INFO
      org.springframework.security: DEBUG
      com.paul: DEBUG
      root: INFO

SecurityConfigurer.java
package com.paul.testoauth2.config;

import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.paul.testoauth2.oauth2.converter.KeycloakRealmRoleConverter;
import com.paul.testoauth2.oauth2.converter.UsernameSubClaimAdapter;

@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurer extends WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .authorizeRequests(
                a -> a
                        .antMatchers("/", "/error", "/webjars/**")
                      .permitAll()
                      .antMatchers(HttpMethod.GET, "/protected/**").hasRole("USER")
                      .antMatchers(HttpMethod.GET, "/api/employees/**").hasRole("READ_EMPLOYEE")
                      .anyRequest()
                      .authenticated()
             )
            .exceptionHandling(
                e -> e//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                      .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
//                      .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
             )
            .oauth2Client(
                c -> c.authorizedClientService(
                          authorizedClientService(nullnull)
                       )
             )
            .oauth2ResourceServer()
            .jwt(
                c -> c.jwtAuthenticationConverter(
                          jwtAuthenticationConverter(null)
                       )
                      .decoder(jwtDecoder(null))
             )
//            .decoder(jwtDecoder(null))
            ;
        // @formatter:on
    }
    
    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
        JdbcOperations jdbcOperations, ClientRegistrationRepository clientRegistrationRepository
    ) {
        return new JdbcOAuth2AuthorizedClientService(jdbcOperations, clientRegistrationRepository);
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter(ObjectMapper objectMapper) {
        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setPrincipalClaimName("preferred_username");
        jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter(objectMapper));
        return jwtConverter;
    }
    
    //below is auto configured at OAuth2ResourceServerJwtConfiguration
    @Bean
    public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
        String issuerUri = properties.getJwt().getIssuerUri();
        // Use preferred_username from claims as authentication name, instead of UUID subject
        NimbusJwtDecoder jwtDecoder = 
                JwtDecoders.<NimbusJwtDecoder>fromIssuerLocation(issuerUri);
        jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
        return jwtDecoder;
    }

}


WebClientConfig.java
package com.paul.testoauth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(ClientRegistrationRepository clientRegistrations,
            OAuth2AuthorizedClientRepository authorizedClients) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth = 
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
        oauth.setDefaultClientRegistrationId("my-client-1");
        return WebClient.builder().filter(oauth).build();
    }

}

DepartmentRestClient.java
package com.paul.testoauth2.employee.service;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class DepartmentRestClient {
    
//    @NotNull
    @Value("${department-service.url}")
    private String endpoint;

    @Autowired
    private WebClient webClient;

    public String getDepartmentName() {
        return webClient.get()
                        .uri(endpoint + "/protected")
                        .retrieve()
                        .bodyToMono(String.class)
                        .block()
                        ;
    }
}


Reference:
https://docs.spring.io/spring-security/site/docs/5.5.x/reference/html5/#oauth2

Sample Project:
https://github.com/spring-projects/spring-security-samples/tree/5.5.x

!相当不错的教程
https://wstutorial.com/index.html
https://wstutorial.com/rest/spring-security-oauth2-keycloak.html
https://wstutorial.com/rest/spring-security-oauth2-keycloak-roles.html






posted on 2021-11-03 16:58 paulwong 阅读(676) 评论(0)  编辑  收藏 所属分类: OAUTH2


只有注册用户登录后才能发表评论。


网站导航: