-
REST API 보안 적용 - 2백기선(인프런 강의)/스프링 기반 REST API 개발 2021. 1. 29. 10:46반응형
위 GIT주소에 개발 step 별로 commit 해두었다.
진행 과정
이벤트 조회 및 수정 REST API 개발
- Account 도메인 추가
- 스프링 시큐리티 적용
- 예외 테스트
- 스프링 시큐리티 기본 설정
- 스프링 시큐리티 폼 인증 설정
- 스프링 시큐리티 OAuth2 인증 서버 설정
- 리소스 서버 설정
- 문자열을 외부 설정으로 빼내기
- 이벤트 API 점검
- 현재 사용자 조회
- 출력값 제한하기
5. 스프링 시큐리티 폼 인증 설정
@Override public void configure(HttpSecurity http) throws Exception { http .anonymous() //익명 사용자 허용 .and() .formLogin() // form인증 허용 .and() .authorizeRequests() .mvcMatchers(HttpMethod.GET, "/api/**").authenticated() // 인증 걸림 //.mvcMatchers(HttpMethod.GET, "/api/**").anonymous() //인증 안걸림 .anyRequest().authenticated(); }
5-1. 익명 사용자 사용 활성화
- 폼 인증 방식 활성화
- 스프링 시큐리티가 기본 로그인 페이지 제공
5-2. 요청에 인증 적용
- /api 이하 모든 GET 요청에 인증이 필요함. (permitAll()을 사용하여 인증이 필요없이 익명으로 접근이 가능케 할 수 있음)
- 그밖에 모은 요청도 인증이 필요함.
6. 스프링 시큐리티 OAuth2 인증 서버 설정
6-1. 의존성 추가
<dependency> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.2.0</version> </dependency>
6-2. GrantType
Spring OAuth2.0이 인증하는 6가지 인증 방법중 하기 2가지 인증방식을 사용
Password
- 최초 oauth 토큰 발급
- 다른 GrantType과 다르게 홉이 한번(요청과 응답이 한쌍)
- 인증을 제공하는 서비스 오너가 만든 클라이언트에서만 사용
- 우리 서비스에 가입되어있는 토큰
- https://developer.okta.com/blog/2018/06/29/what-is-the-oauth2-password-grant
refreshToken
- 인증된 정보를 계속 요청할 수 있는 토큰(권한)
6-3. AuthorizationServer
@Configuration @EnableAuthorizationServer public class AutoServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired PasswordEncoder passwordEncoder; @Autowired AuthenticationManager authenticationManager; @Autowired AccountService accountService; @Autowired TokenStore tokenStore; /** * 패스워드 설정 * clientSecret를 사용할 때 사용 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.passwordEncoder(passwordEncoder); } /** * client 설정 * id 설정 * password 설정 * grant_type 설정 * scope 설정 * accessTokenValiditySeconds(액세스 토큰 유지 시간) * refreshTokenValiditySeconds(리프레시 토큰 유지 시간) */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("myApp") .authorizedGrantTypes("password", "refresh_token") .scopes("read", "write") // 정의 한대로 .secret(this.passwordEncoder.encode("pass")) .accessTokenValiditySeconds(10 * 60) .refreshTokenValiditySeconds(6 * 10 * 60); } /** * thentiacationManager 설정 * UserDetailsService 설정 * TokenStored 설정 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .userDetailsService(accountService) .tokenStore(tokenStore); } }
6-4. 응답 결과
7. 리소스 서버 설정
7-1. 리소스 서버 구현 기준
- 리소스 서버는 이벤트리소스를 제공하는 서버와 같이있는게 맞고
- 인증서버는 분리하는게 맞다(작은 서비스에서는 합쳐도 상관없음)
7-2. ResourceServer 설정
EventControllerTest를 돌리게 된다면 Get만 빼고 다 테스트 실패
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("event"); } @Override public void configure(HttpSecurity http) throws Exception { http .anonymous() .and() .authorizeRequests() .antMatchers("/h2-console/**").permitAll() .mvcMatchers(HttpMethod.GET, "/api/**") .permitAll() .anyRequest() // GET을 제외한 그밖의 요청은 인증 필요 .authenticated() .and() .exceptionHandling() .accessDeniedHandler(new OAuth2AccessDeniedHandler()); } }
Get말고 다른 테스트 성공을 하기 위해서는
@Before public void setUp(){ /* 테스트를 도는 중에는 데이터를 공유하기때문에 이와같이 삭제를 해줘야 한다. */ this.eventRepository.deleteAll(); this.accountRepository.deleteAll(); }
Header에 token을 넣어야 한다.
private String getBearerToken() throws Exception { return "Bearer " + getAccessToken(); } private String getAccessToken() throws Exception { String userName = "JinSeok"; String password = "jinSeok"; Account jinSeok = Account.builder() .email(userName) .password(password) .roles(Set.of(AccountRole.ADMIN, AccountRole.USER)) .build(); this.accountService.saveAccount(jinSeok); String clientId = "myApp"; String clientSecret = "pass"; ResultActions perform = this.mockMvc.perform(post("/oauth/token") .with(httpBasic(clientId, clientSecret)) .param("username", userName) .param("password", password) .param("grant_type", "password")) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("access_token").exists()); var responseBody = perform.andReturn().getResponse().getContentAsString(); Jackson2JsonParser parser = new Jackson2JsonParser(); return parser.parseMap(responseBody).get("access_token").toString(); }
8. 문자열을 외부 설정으로 빼내기
8-1. 의존성 추가
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
8-2. Java code 추가
@Component @ConfigurationProperties(prefix = "my-app") @Getter @Setter public class AppProperties { @NotEmpty private String adminUserName; @NotEmpty private String adminPassword; @NotEmpty private String userUserName; @NotEmpty private String userPassword; @NotEmpty private String clientId; @NotEmpty private String clientSecret; }
8-3 properties 추가
my-app.admin-username=admin@test.com my-app.admin-password=admin my-app.user-username=user@test.com my-app.user-password=user my-app.client-id=myApp my-app.client-secret=pass
9. 이벤트 API 점검
9-1. 토큰 발급 받기
토큰 발급 받기
- POST /oauth/token
- BASIC authentication 헤더
- client Id(myApp) + client secret(pass)
요청 본문 폼
- username: admin@email.com
- password: admin
- grant_type: password
9-2. 토큰 갱신하기
POST /oauth/token
- BASIC authentication 헤더
- client Id(myApp) + client secret(pass)
요청 본문 폼
- token: 처음에 발급받았던 refersh 토큰
- grant_type: refresh_token
9-3. 이벤트 목록 조회 API
로그인 했을 때
- 이벤트 생성 링크 제공
9-4. 이벤트 조회
로그인 했을 때
- 이벤트 Manager인 경우에는 이벤트 수정 링크 제공
10. 현재 사용자 조회
10-1. SecurityContext
User principal = (User)authentication.getPrincipal(); 에서 꺼낸 객체는 아래 메소드에서 리턴된 값을 꺼낸다.
package study.accounts.service; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account = accountRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException(username)); return new User(account.getEmail(), account.getPassword() ,authorities(account.getRoles())); }
10-2. @AuthenticationPrincipal spring.security.User user
- 인증 안한 경우에 null
- 인증 한 경우에는 username과 authorities 참조 가능
@GetMapping public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler, @AuthenticationPrincipal User user) { Page<Event> page = this.eventRepository.findAll(pageable); // PagedModel<EntityModel<Event>> pageResource = assembler.toModel(page, e -> new EventResource(e)); var pageResource = assembler.toModel(page, e -> new EventResource(e)); pageResource.add(Link.of("/docs/index.html#resources-events-list").withRel("profile")); if(user != null){ pageResource.add(linkTo(EventController.class).withRel("create-event")); } return ResponseEntity.ok(pageResource); }
10-2. @AuthenticationPrincipal
10-1, 10-2의 코드가 너무 길다면 @AuthenticationPrincipal을 이용하면 된다. 사용하게 되면 user의 정보를 알 수 있기 때문이다.
@GetMapping public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler, @AuthenticationPrincipal(expression = "account ") Account account) { Page<Event> page = this.eventRepository.findAll(pageable); // PagedModel<EntityModel<Event>> pageResource = assembler.toModel(page, e -> new EventResource(e)); var pageResource = assembler.toModel(page, e -> new EventResource(e)); pageResource.add(Link.of("/docs/index.html#resources-events-list").withRel("profile")); if(account != null){ pageResource.add(linkTo(EventController.class).withRel("create-event")); } return ResponseEntity.ok(pageResource); }
SpEL을 사용
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @AuthenticationPrincipal(expression = "account'") public @interface CurrentUser { }
@GetMapping public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler, @CurrentUser Account account) { Page<Event> page = this.eventRepository.findAll(pageable); // PagedModel<EntityModel<Event>> pageResource = assembler.toModel(page, e -> new EventResource(e)); var pageResource = assembler.toModel(page, e -> new EventResource(e)); pageResource.add(Link.of("/docs/index.html#resources-events-list").withRel("profile")); if(account != null){ pageResource.add(linkTo(EventController.class).withRel("create-event")); } return ResponseEntity.ok(pageResource); }
하지만 위와같이 사용하게 되면 오류가 발생이 된다. 그 이유는 account가 인증이 안되었기 때문에 아래와같이 수정해야 한다.
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account") public @interface CurrentUser { // 현재 인증 정보가 anonymousUse 인 경우에는 null을 보내고 아니면 “account”를 꺼내준다. }
11. 출력값 제한하기
11-1. JsonSerializer<Account> 구현
public class AccountSerializer extends JsonSerializer<Account> { @Override public void serialize(Account account, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartObject(); gen.writeNumberField("id", account.getId()); gen.writeEndObject(); } }
11-2. @JsonSerialize(using) 설정
11-3. 설정 전후 비교
설정전 데이터
설정 후 데이터
반응형'백기선(인프런 강의) > 스프링 기반 REST API 개발' 카테고리의 다른 글
REST API 보안 적용 - 1 (0) 2021.01.27 이벤트 조회 및 수정 REST API 개발 (0) 2021.01.27 REST API 보안 적용 (0) 2021.01.24 이벤트 생성 API 개발 -3 (0) 2021.01.24 이벤트 생성 API 개발 -2 (0) 2021.01.23