Spring Boot OAuth2 Resource Server 구축
참조사이트 : https://www.baeldung.com/spring-security-oauth2-jws-jwk
목표
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();
}
}
Pingback: Spring Boot 시작하기 – 상구리의 기술 블로그