Contents

[Mastering Spring 5.0] 6.6 Spring Security - OAuth 2.0

μŠ€ν”„λ§ 5.0 λ§ˆμŠ€ν„° μŠ€ν„°λ””
μŠ€ν”„λ§ 5.0 λ§ˆμŠ€ν„° μŠ€ν„°λ”” ν•™μŠ΅ λ‚΄μš© μ •λ¦¬μž…λ‹ˆλ‹€.

OAuth2 인증

OAuth 2λŠ” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜κ³Ό Facebook, GitHub 및 DigitalOcean κ³Ό 같은 HTTP μ„œλΉ„μŠ€μ˜μ‚¬μš©μž 계정에 λŒ€ν•œ μ œν•œλœ μ•‘μ„ΈμŠ€ κΆŒν•œμ„ 얻을 수있게 ν•΄μ£ΌλŠ” 인증 ν”„λ ˆμž„ μ›Œν¬μ΄λ‹€. μ΄λŠ” μ‚¬μš©μž 계정을 ν˜ΈμŠ€νŒ…ν•˜λŠ” μ„œλΉ„μŠ€μ— μ‚¬μš©μž 인증을 μœ„μž„ν•˜κ³  타사 μ‘μš© ν”„λ‘œκ·Έλž¨μ— μ‚¬μš©μž 계정에 λŒ€ν•œ μ•‘μ„ΈμŠ€ κΆŒν•œμ„ λΆ€μ—¬ν•˜μ—¬ μž‘λ™ν•˜κ²Œ λœλ‹€. OAuth 2λŠ” μ›Ή 및 λ°μŠ€ν¬ν†± μ‘μš© ν”„λ‘œκ·Έλž¨ 및 λͺ¨λ°”일 μž₯μΉ˜μ— λŒ€ν•œ 인증 흐름을 μ œκ³΅ν•˜κ²Œ λœλ‹€.

OAuth2 주체
  • λ¦¬μ†ŒμŠ€ μ†Œμœ μž (μ‚¬μš©μž) : λ¦¬μ†ŒμŠ€ μ†Œμœ μžλŠ” μžμ‹ μ˜ 계정에 μ•‘μ„ΈμŠ€ν•˜κΈ° μœ„ν•΄ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ 을 μΈμ¦ν•˜λŠ” μ‚¬μš©μž 이닀. μ‘μš© ν”„λ‘œκ·Έλž¨μ˜ μ‚¬μš©μž κ³„μ •μ˜ μ•‘μ„ΈμŠ€ κΆŒν•œμ€ λΆ€μ—¬ 된 κΆŒν•œ (예 : 읽기 λ˜λŠ” μ“°κΈ° κΆŒν•œ) 의 “λ²”μœ„"둜 μ œν•œλœλ‹€.
  • λ¦¬μ†ŒμŠ€ μ„œλ²„ : λ¦¬μ†ŒμŠ€ μ„œλ²„λŠ” μ‚¬μš©μžμ˜ 계정을 ν˜ΈμŠ€νŠΈν•˜λ©° λ³΄μ•ˆ μœ μ§€κ°€ ν•„μš”ν•œ λ¦¬μ†ŒμŠ€κ°€ μžˆλŠ” μ„œλ²„μ΄λ‹€.
  • ν΄λΌμ΄μ–ΈνŠΈ : ν΄λΌμ΄μ–ΈνŠΈλŠ” μ‚¬μš©μž 계정(μ‚¬μš©μž 계정을 ν˜ΈμŠ€νŠΈν•˜λŠ” λ¦¬μ†ŒμŠ€ μ„œλ²„) μ•‘μ„ΈμŠ€ν•˜λ €λŠ” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ΄λ‹€.
  • κΆŒν•œ μ„œλ²„ : OAuth μ„œλΉ„μŠ€λ₯Ό μ œκ³΅ν•˜λ©°, μ‚¬μš©μžμ˜ 신원을 μΈμ¦ν•˜λ©° ν΄λΌμ΄μ–ΈνŠΈκ°€ λ¦¬μ†ŒμŠ€ μ„œλ²„μ— μ•‘μ„ΈμŠ€ ν•  수 μžˆλŠ” κΆŒν•œμ„ λΆ€μ—¬ν•œλ‹€.
  1. μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ‚¬μš©μžμ—κ²Œ λ¦¬μ†ŒμŠ€ μ„œλ²„ μžμ›μ— λŒ€ν•œ μ•‘μ„ΈμŠ€ κΆŒν•œμ„ μš”μ²­ν•œλ‹€.
  2. μ‚¬μš©μžκ°€ μ•‘μ„ΈμŠ€ κΆŒν•œμ„ μ œκ³΅ν•˜λ©΄ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ€ κΆŒν•œμ„ λΆ€μ—¬ λ°›λŠ”λ‹€.
  3. μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ‚¬μš©μž κΆŒν•œ λΆ€μ—¬ 및 자체 ν΄λΌμ΄μ–ΈνŠΈ κΆŒν•œ 정보λ₯Ό κΆŒν•œ μ„œλ²„μ— μ œκ³΅ν•œλ‹€.
  4. 인증에 μ„±κ³΅ν•˜λ©΄ 인증을 μœ„ν•œ μ•‘μ„ΈμŠ€ 토큰을 μ œκ³΅ν•œλ‹€.
  5. μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ€ 인증을 μœ„ν•΄ μ•‘μ„ΈμŠ€ 토큰을 μ œκ³΅ν•˜λŠ” λ¦¬μ†ŒμŠ€ μ„œλ²„λ₯Ό ν˜ΈμΆœν•œλ‹€.
  6. μ•‘μ„ΈμŠ€ 토큰이 μœ νš¨ν•˜λ©΄ λ¦¬μ†ŒμŠ€ μ„œλ²„λŠ” λ¦¬μ†ŒμŠ€ μ„ΈλΆ€ 정보λ₯Ό λ°˜ν™˜ν•œλ‹€.

Springboot OAuth 2 인증 κ΅¬ν˜„ν•˜κΈ°

μ˜μ‘΄μ„± μΆ”κ°€

spring-security-oauth2 λŠ” μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°μ— OAuth2 지원을 μ œκ³΅ν•˜κΈ° μœ„ν•œ λͺ¨λ“ˆμ΄λ‹€. pom.xml λ˜λŠ” build.gradle 에 μΆ”κ°€ν•œλ‹€.

pom.xml

1
2
3
4
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

build.gradle

1
implementation('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE')

κΆŒν•œ 및 λ¦¬μ†ŒμŠ€ μ„œλ²„ μ„€μ •ν•˜κΈ°

일반적으둜 κΆŒν•œ μ„œλ²„μ™€ λ¦¬μ†ŒμŠ€ μ„œλ²„λ₯Ό λΆ„λ¦¬ν•˜μ§€λ§Œ, 예제 μ†ŒμŠ€λŠ” κΆŒν•œ μ„œλ²„μ™€ λ¦¬μ†ŒμŠ€ μ„œλ²„λ₯Ό λ™μΌν•˜κ²Œ μ§€μ •ν•˜μ˜€λ‹€.

1
2
3
4
5
6
7
8
@EnableResourceServer
@EnableAuthorizationServer
@SpringBootApplication
public class Chapter06Application {
	public static void main(String[] args) {
		SpringApplication.run(Chapter06Application.class, args);
	}
}
  • @EnableResourceServer : λ“€μ–΄μ˜€λŠ” OAuth 2 토큰을 톡해 μš”μ²­μ„ μΈμ¦ν•˜λŠ” μŠ€ν”„λ§ μ‹œνλ¦¬ν‹°λ₯Ό μ‚¬μš©ν•˜λŠ” OAuth2 λ¦¬μ†ŒμŠ€ μ„œλ²„μ— λŒ€ν•œ μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€.
  • @EnableAuthorizationServer : DispatcherServlet μ½˜ν…μŠ€νŠΈμΈ ν˜„μž¬ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μ½˜ν…μŠ€νŠΈμ—μ„œ AuthorizationEndpoint 및 TokenEndPoint λ₯Ό μ‚¬μš©ν•΄ κΆŒν•œ λΆ€μ—¬ μ„œλ²„λ₯Ό μ‚¬μš©ν•  수 μžˆλ„λ‘ ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€.

κΆŒν•œ μ„œλ²„ μ„ΈλΆ€ 정보 κ΅¬μ„±ν•˜κΈ°

예제 μ†ŒμŠ€μ—λŠ” application.properties λ₯Ό 톡해 μ„ΈλΆ€ ꡬ성을 섀정을 ν•˜μ˜€μ§€λ§Œ, μžλ™κ΅¬μ„± 섀정이 잘 λ˜μ§€ μ•Šμ•„ @Configuration 을 ν†΅ν•˜μ—¬ μˆ˜λ™ κ΅¬μ„±μœΌλ‘œ μž‘μ„±ν•˜μ˜€λ‹€.

/src/main/java/com/mastering/spring/springboot/config/OAuthConfiguration.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Autowired
    private TestUserDetailService clientDetailsService;

    @Override
    public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
        configurer.inMemory()
                .withClient("clientId")
                .secret("{noop}clientSecret")
                .authorizedGrantTypes("authorization_code", "refresh_token", "password")
                .scopes("openid")
                .authorities("ROLE_MY_CLIENT");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenServices(getDefaultTokenServices())
                 .authenticationManager(authenticationManager)
                 .userDetailsService(clientDetailsService);
    }

    @Bean
    @Primary
    public DefaultTokenServices getDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(new InMemoryTokenStore());
        return tokenServices;
    }
}
  • void configure(ClientDetailsServiceConfigurer configurer) : ν΄λΌμ΄μ–ΈνŠΈ ID 와 Secret 자격증λͺ… 정보λ₯Ό λ©”λͺ¨λ¦¬μ— μ„€μ •ν•œλ‹€. Spring Security 5.0.0.RC1 이후 μ•”ν˜Έλ³€ν™˜μ •μ±…μ΄ λ³€κ²½λ˜μ—ˆμœΌλ―€λ‘œ, 기본값인 DelegatingPasswordEncoder μ—λŠ” νŒ¨λ“œμ›Œλ“œ μ•”ν˜Έν™” λ©”μ†Œλ“œ 접두어가 ν•„μš”ν•˜λ‹€. (bcrypt/noop/pbkdf2/scrypt/sha256) 쀑 ν•˜λ‚˜λ₯Ό μ‚¬μš©ν•  수 있으며, μ˜ˆμ œλŠ” λ”°λ‘œ 인코딩을 μ§€μ •ν•˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— {noop} 접두어λ₯Ό 섀정함.
  • void configure(AuthorizationServerEndpointsConfigurer endpoints) : /oauth/token μ—”λ“œν¬μΈνŠΈμ— λŒ€ν•œ 상세 μ„œλΉ„μŠ€λ₯Ό 지정할 수 μžˆλ‹€.
  • spring-security 5.0 μ—μ„œ 달라진 μ•”ν˜Έλ³€ν™˜μ •μ±…, DelegatingPasswordEncoder
  • Password Encoding

/src/main/java/com/mastering/spring/springboot/config/WebSecurityConfigurer.java

1
2
3
4
5
6
7
8
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

/src/main/java/com/mastering/spring/springboot/service/TestUserDetailService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestUserDetailService implements UserDetailsService {
    @Value("${spring.security.user.name}")
    private String myUserName;

    @Value("${spring.security.user.password}")
    private String myUserPassword;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if(!myUserName.equals(username)) {
            throw new UsernameNotFoundException("UsernameNotFound [" + username + "]");
        }

        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        User.UserBuilder userBuilder = User.builder().passwordEncoder(encoder::encode);

        UserDetails user = userBuilder
                .username(myUserName)
                .password(myUserPassword)
                .roles("USER")
                .build();

        return user;
    }
}
  • UserDetailsService : μ‹€μ œ DB λ‚˜ ν˜Ήμ€ μ‚¬μš©μž 정보λ₯Ό μ‘°νšŒν•˜μ—¬ λ¦¬ν„΄ν•œλ‹€. μ•„λž˜λŠ” UserDetailsService λ₯Ό μ‚¬μš©μž μ •μ˜λ‘œ κ΅¬ν˜„ν•˜μ—¬, 이전 μ˜ˆμ œμ—μ„œ μ„€μ •ν•œ application.yml 에 μžˆλŠ” νšŒμ›μ •λ³΄λ₯Ό 가져와 κ²€μ¦ν•˜λ„λ‘ μž‘μ„±ν•˜μ˜€λ‹€.

OAuth μš”μ²­ μ‹€ν–‰

API 에 μ•‘μ„ΈμŠ€ ν•˜λ €λ©΄ λ‹€μŒ 2단계 ν”„λ‘œμ„ΈμŠ€κ°€ ν•„μš”ν•˜λ‹€.
  1. μ•‘μ„ΈμŠ€ 토큰을 μ–»λŠ”λ‹€.
  2. μ•‘μ„ΈμŠ€ 토큰을 μ‚¬μš©ν•΄ μš”μ²­μ„ μ‹€ν–‰ν•œλ‹€.

μ•‘μ„ΈμŠ€ 토큰 μ–»κΈ°

μ•‘μ„ΈμŠ€ν† ν°μ„ μ΄μš©ν•œ μš”μ²­ μ‹€ν–‰

ν†΅ν•©ν…ŒμŠ€νŠΈ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private OAuth2RestTemplate getOAuthTemplate() {
    ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
    resource.setUsername("user-name");
    resource.setPassword("user-password");
    resource.setAccessTokenUri(createUrl("/oauth/token"));
    resource.setClientId("clientId");
    resource.setClientSecret("clientSecret");
    resource.setGrantType("password");
    OAuth2RestTemplate oauthTemplate = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext());
    return oauthTemplate;
}

@Test
public void retrieveTodo() throws Exception {
    String expected = "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}";
    ResponseEntity<String> response = getOAuthTemplate().getForEntity(createUrl("/users/Jack/todos/1"), String.class);
    JSONAssert.assertEquals(expected, response.getBody(), false);
}
  • ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails() : μ‚¬μš©μž 자격증λͺ…κ³Ό ν΄λΌμ΄μ–ΈνŠΈ 자격증λͺ…μœΌλ‘œ ResourceOwnerPasswordResourceDetails μ„€μ •ν•œλ‹€.

  • resource.setAccessTokenUri(createUrl("/oauth/token")) : μΈμ¦μ„œλ²„μ˜ URL 을 κ΅¬μ„±ν•œλ‹€.

  • OAuth2RestTemplate oauthTemplate = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext()) : OAuth2RestTemplate 은 OAuth2 ν”„λ‘œν† μ½œμ„ μ§€μ›ν•˜λŠ” Resttemplate 의 ν™•μž₯이닀.

μ°Έκ³