Spring Boot OAuth2 Resource Server 구축

By | 2021년 7월 24일
Table of Contents

Spring Boot OAuth2 Resource Server 구축

참조사이트 : https://www.baeldung.com/spring-security-oauth2-jws-jwk

참조사이트 : https://daddyprogrammer.org/post/1890/spring-boot-oauth2-resourceserver-asymmetric-keys-to-do-the-signing-process/

목표

OAuth2 Resource Server 를 구축합니다.

소스코드

여기 에 전체 소스코드가 올라가 있습니다.

프로젝트 생성

신규 프로젝트를 생성합니다.

프로젝트명은 oauth2resourceserver 로 합니다.

의존성은 DevTools, Lombok, Spring Web 을 선택합니다.

hosts 파일 수정

클라이언트는 localhost 로, 인증서버는 auth.localhost,
리소스서버는 res.localhost 로 각각 할당합니다.

127.0.0.1    auth.localhost
127.0.0.1    res.localhost

build.gradle 수정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.security:spring-security-jwt'

    // 버전을 명시적으로 지정해야 한다(?)
    implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.5.2'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

application.yml 수정

클라이언트 정보와 token-info-uri 만으로 토큰 체크가 가능합니다.

security:
  oauth2:
#    jwt:
#      signkey: 123@#$
    resource:
      token-info-uri: http://localhost:9000/oauth/check_token
    client:
      client-id: foo
      client-secret: bar

server:
  port: 9001

Bearer 토큰방식

Authorization Server 를 JDBC 방식으로 변경합니다.

.checkTokenAccess("isAuthenticated()") 으로
토큰체크 엔드포인트를 활성화 해줍니다.

@RequiredArgsConstructor
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // ......

    // JDBC 방식
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new JdbcTokenStore(dataSource));
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()") //allow check token
                .allowFormAuthenticationForClients();
    }

//    // JWT 방식
//    @Override
//    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//        super.configure(endpoints);
//        endpoints.accessTokenConverter(jwtAccessTokenConverter()).userDetailsService(userDetailService);
//    }
//
//    @Bean
//    public JwtAccessTokenConverter jwtAccessTokenConverter() {
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(signKey);
//        return converter;
//    }
}

ResourceServerConfig.java

user-info-uri 만 설정해 주면 설정이 끝납니다.

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.headers().frameOptions().disable();
        http.authorizeRequests()
                .antMatchers("/api/userinfo").access("#oauth2.hasScope('profile')")
                .anyRequest().authenticated();
    }
}

UserInfoController.java

@RestController
@RequestMapping("/api")
public class UserInfoController {

    @GetMapping("/userinfo")
    public ResponseEntity<?> userInfo(Principal principal,
                                      HttpServletRequest request, @RequestParam HashMap<String,String> paramMap) {
        return ResponseEntity.ok(principal);
    }
}

JWT 토큰방식(signKey 방식)

Authorization Server 를 JWT 방식으로 변경합니다.

인증서버를 거치지 않고, signKey 만으로 토큰체크를 합니다.
인증서버와 리소스서버의 signKey 는 동일해야 합니다.

@RequiredArgsConstructor
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Value("${security.oauth2.jwt.signkey}")
    private String signKey;

    private final DataSource dataSource;

    private final PasswordEncoder passwordEncoder;

    private final CustomUserDetailService userDetailService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
    }

//    // JDBC 방식
//    @Override
//    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//        endpoints.tokenStore(new JdbcTokenStore(dataSource));
//    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()") //allow check token
                .allowFormAuthenticationForClients();
    }

    // JWT 방식
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
        endpoints.accessTokenConverter(jwtAccessTokenConverter()).userDetailsService(userDetailService);
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signKey);
        return converter;
    }
}

Resource Server 의 application.yml 을 아래와 같이 수정합니다.

security:
  oauth2:
    jwt:
      signkey: 123@#$
#    resource:
#      token-info-uri: http://localhost:9000/oauth/check_token
#    client:
#      client-id: foo
#      client-secret: bar

ResourceServerConfig.java

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Value("${security.oauth2.jwt.signkey}")
    private String signKey;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signKey);
        return converter;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.headers().frameOptions().disable();
        http.authorizeRequests()
                .antMatchers("/api/userinfo").access("#oauth2.hasScope('profile')")
                .anyRequest().authenticated();
    }
}

JWT 토큰방식(비대칭키 방식)

공개키/비밀키를 이용한 토큰 암호화를 합니다.

비대칭 키 생성

keytool 을 이용해 암호파일을 생성합니다.

cd $JAVA_HOME/bin

./keytool -genkeypair \
  -alias my-oauth-jwt \
  -keyalg RSA \
  -keypass mypassword \
  -keystore my-oauth-jwt.jks \
  -storepass mypassword

./keytool -list -rfc --keystore my-oauth-jwt.jks | openssl x509 -inform pem -pubkey

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyg0cDZE5BsUkVi6tcq7s
e/L8oZ/deX1wEx8poKCw0L9psnJ94RFPE1TpBO+Y1XoCmI6Y+srtnhOkuen0xgWN
......................
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDWTCCAkGgAwIBAgIEeur0ATANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJL
UjEOMAwGA1UECBMFU2VvdWwxDjAMBgNVBAcTBVNlb3VsMQ0wCwYDVQQKEwRIb21l
MQ0wCwYDVQQLEwRIb21lMRAwDgYDVQQDEwdTYW4gTGVlMB4XDTIxMDcyNTAwNTgw
......................
-----END CERTIFICATE-----

Authorization Server 프로젝트에 추가

Authorization Server 의 resources 폴더에 my-oauth-jwt.jks 를 추가합니다.
mypassword 를 my-oauth-jwt.private 로 생성 후 resources 폴더에 추가합니다.
공개키를 my-oauth-jwt.pub 로 생성 후 resources 폴더에 추가합니다.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyg0cDZE5BsUkVi6tcq7s
e/L8oZ/deX1wEx8poKCw0L9psnJ94RFPE1TpBO+Y1XoCmI6Y+srtnhOkuen0xgWN
..........................
-----END PUBLIC KEY-----

my-oauth-jwt.jks, my-oauth-jwt.private 를 이용해 토큰을 암호화합니다.

AuthorizationServerConfig.java

public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // ......

//    @Bean
//    public JwtAccessTokenConverter jwtAccessTokenConverter() {
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(signKey);
//        return converter;
//    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() throws Exception {

        // 패스워드 저장 파일
        String password = "";
        byte[] buffer = new byte[128];
        ClassPathResource resource = new ClassPathResource("my-oauth-jwt.private");
        InputStreamReader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.US_ASCII);
        StringBuilder builder = new StringBuilder();
        for(int c = reader.read(); c != -1; c = reader.read()){
            builder.append((char) c);
        }
        password = builder.toString();

        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new FileSystemResource("src/main/resources/my-oauth-jwt.jks"), password.toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("my-oauth-jwt"));

        return converter;
    }
}

Resource Server 프로젝트에 추가

공개키를 my-oauth-jwt.pub 로 생성 후 resources 폴더에 추가합니다.
인증서버와 리소스서버의 my-oauth-jwt.pub 는 같은 파일이어야 합니다.

//    @Bean
//    public JwtAccessTokenConverter accessTokenConverter() {
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(signKey);
//        return converter;
//    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("my-oauth-jwt.pub");
        String publicKey = null;
        try {
            // implementation 'commons-io:commons-io:2.6'
            publicKey = IOUtils.toString(resource.getInputStream());
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
        converter.setVerifierKey(publicKey);
        return converter;
    }

JWT 토큰방식(비대칭키 방식, /oauth/token_key 이용)

Authorization Server 프로젝트 수정

tokenKeyAccess("permitAll()") 에 의해 /oauth/token_key 접근을 허용합니다.

AuthorizationServerConfig.java

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()") //allow check token
                .allowFormAuthenticationForClients();
    }

Resource Server 프로젝트 수정

key-uri 를 설정해 줍니다.

token_key 에 암호화 방식과 공개키를 제공받습니다.

security:
  oauth2:
    resource:
      jwt:
        key-uri: http://auth.localhost:9000/oauth/token_key
#    jwt:
#      signkey: 123@#$
#    resource:
#      token-info-uri: http://localhost:9000/oauth/check_token
#    client:
#      client-id: foo
#      client-secret: bar

token_key 에 의해 모든 설정이 적용되므로 별도의 코딩이 필요없습니다.

ResourceServerConfig.java

public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.headers().frameOptions().disable();
        http.authorizeRequests()
                .antMatchers("/api/userinfo").access("#oauth2.hasScope('profile')")
                .anyRequest().authenticated();
    }
}

One thought on “Spring Boot OAuth2 Resource Server 구축

  1. Pingback: Spring Boot 시작하기 – 상구리의 기술 블로그

답글 남기기