이전 블로그 포스팅에서 눈치 채셨겠지만, 저는 다음을 좋아합니다. Spring + Java 그리고 Spring + Kotlin. 따라서 OAuth 2.0 인증을 구현해야 할 때마다 스프링-security-oauth2 라이브러리는 자연스럽게 선택됩니다.
그러나 Spring Security와 OAuth2를 함께 사용하는 방법을 보여주는 방법은 거의 없습니다. 스프링 보안 인증 2 를 inMemory 및 JDBC 이외의 다른 데이터 소스와 함께 사용할 수 있습니다. 많은 것을 구성해야 하므로 이 튜토리얼은 사용자 인증 방법의 세 부분으로 나누어 설명하겠습니다, 토큰 저장소를 구성하는 방법 동적 클라이언트를 구성하는 방법에 대해 설명합니다. 그럼 시작해 보겠습니다!
먼저, 최신 버전 중 하나를 사용하고 있다고 가정합니다. 스프링 보안 인증 2:
1 2 3 4 5 |
<dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>스프링 보안 인증 2</artifactId> <version>2.3.3.릴리스</version> </dependency> |
둘째, 저는 스프링 데이터와 함께 카우치베이스를 사용하고 있습니다. 다른 데이터 소스를 사용 중이더라도 이 블로그 시리즈에서 많은 코드를 재사용할 수 있습니다.
1 2 3 4 5 |
<dependency> <groupId>org.springframework.data</groupId> <artifactId>스프링 데이터 카우치베이스</artifactId> <version>3.0.5.릴리스</version> </dependency> |
또한 다음과 같은 기능을 추가했습니다. 롬복 를 종속성으로 사용하여 Java의 상용구를 줄입니다:
1 2 3 4 5 |
<종속성> <groupId>org.프로젝트 롬복</groupId> <artifactId>롬복</artifactId> <선택 사항>true</선택 사항> </종속성> |
다음과 관련된 Spring 보안 문서에 따라 리소스 서버를 구성해 보겠습니다. 스프링 보안 인증 2: "리소스 서버(권한 서버와 동일하거나 별도의 애플리케이션일 수 있음)는 OAuth2 토큰으로 보호되는 리소스를 제공합니다. Spring OAuth는 이 보호를 구현하는 Spring 보안 인증 필터를 제공합니다. 이를 켜려면 @EnableResourceServer 에 @Configuration 클래스를 생성하고, 필요에 따라 리소스 서버 컨피규레이터를 사용하여 구성합니다."
1 2 3 4 5 6 7 8 9 10 11 |
@Configuration @EnableResourceServer public 클래스 리소스 서버 구성 확장 리소스서버구성자 어댑터 { 비공개 정적 final 문자열 RESOURCE_ID = "resource_id"; 오버라이드 public void 구성(리소스 서버 보안 구성자 리소스) { 리소스.resourceId(RESOURCE_ID).무국적자(false); } } |
이제 다음과 같은 인터페이스를 구현해 보겠습니다. UserDetailsService. 이 인터페이스는 데이터 소스와 Spring Security OAuth 사이의 다리 역할을 담당합니다:
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 35 36 37 38 |
가져오기 com.bc.빠른 작업.독립형.모델.사용자 세부 정보; 가져오기 com.bc.빠른 작업.독립형.모델.보안 그룹; 가져오기 com.bc.빠른 작업.독립형.모델.사용자; 가져오기 com.bc.빠른 작업.독립형.저장소.사용자 저장소; 가져오기 롬복.외부.slf4j.Slf4j; 가져오기 org.스프링 프레임워크.콩.공장.주석.자동 유선; 가져오기 org.스프링 프레임워크.보안.핵심.사용자 세부 정보.사용자 세부 정보; 가져오기 org.스프링 프레임워크.보안.핵심.사용자 세부 정보.사용자 세부 정보 서비스; 가져오기 org.스프링 프레임워크.보안.핵심.사용자 세부 정보.사용자 이름 찾을 수 없음 예외; 가져오기 org.스프링 프레임워크.고정관념.서비스; 가져오기 자바.활용.목록; 가져오기 자바.활용.스트림.수집가; @Slf4j 서비스 public 클래스 사용자 세부 정보 서비스 구현 사용자 세부 정보 서비스 { 오토와이어드 비공개 사용자 저장소 사용자 저장소; 오토와이어드 비공개 보안 그룹 서비스 보안그룹서비스; 오버라이드 public 사용자 세부 정보 사용자 이름으로 로드(문자열 이름) 던지기 사용자 이름 찾을 수 없음 예외 { 목록<User> 사용자 = 사용자 저장소.사용자 이름으로 찾기(이름); 만약(사용자.isEmpty()) { throw new 사용자 이름 찾을 수 없음 예외("사용자를 찾을 수 없습니다."+이름); } 사용자 사용자 = 사용자.get(0); 목록<SecurityGroup> 보안 그룹 = 보안그룹서비스.목록 사용자 그룹(사용자.getCompanyId(), 사용자.getId()); 반환 new 사용자 세부 정보(사용자, 보안 그룹.스트림() .지도(e->e.getId()) .수집(수집가.toList()) ); } } |
위 코드에서는 다음과 같은 유형의 클래스를 반환하고 있습니다. 사용자 세부 정보도 Spring에서 가져온 것입니다. 다음은 그 구현입니다:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
데이터 public 클래스 사용자 세부 정보 구현 사용자 세부 정보 { 비공개 사용자 사용자; 비공개 목록<String> 그룹; public 사용자 세부 정보(사용자 사용자, 목록<String> 그룹) { 이.사용자 = 사용자; 이.그룹 = 그룹; } 오버라이드 public 컬렉션<? 확장 부여된 권한> getAuthorities() { 반환 null; } 오버라이드 public 문자열 getPassword() { 반환 사용자.getPassword(); } 오버라이드 public 문자열 사용자 이름 가져오기() { 반환 사용자.사용자 이름 가져오기(); } 오버라이드 public 부울 isAccountNonExpired() { 반환 true; } 오버라이드 public 부울 isAccountNonLocked() { 반환 true; } 오버라이드 public 부울 isCredentialsNonExpired() { 반환 true; } 오버라이드 public 부울 isEnabled() { 반환 사용자.getIsEnabled(); } } |
User 클래스가 UserDetails를 직접 구현하도록 할 수도 있었습니다. 하지만 제 사용 사례에는 사용자가 속한 그룹 목록도 필요하기 때문에 위에 구현을 추가했습니다.
사용자, 보안 그룹 및 각 리포지토리의 모습은 다음과 같습니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Data public 클래스 사용자 확장 기본 엔티티 구현 직렬화 가능 { @Id @NotNull 비공개 문자열 id; 필드 @NotNull 비공개 문자열 사용자 이름; 필드 @NotNull 비공개 문자열 companyId; 필드 @NotNull 비공개 문자열 비밀번호; @NotNull 비공개 부울 isEnabled; 필드 비공개 부울 isVisible; } |
1 2 3 4 5 6 7 |
@N1qlPrimaryIndexed @인덱싱된 보기(디자인 문서 = "user") public 인터페이스 사용자 저장소 확장 카우치베이스 페이징 및 정렬 저장소<사용자, 문자열> { 목록<사용자> 사용자 이름으로 찾기(문자열 사용자 이름); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
문서 @Data @NoArgsConstructor 빌더 public 클래스 보안 그룹 확장 기본 엔티티 구현 직렬화 가능 { @Id 비공개 문자열 id; @NotNull 필드 비공개 문자열 이름; 필드 비공개 문자열 설명; @NotNull 필드 비공개 문자열 companyId; 필드 비공개 목록<String> 사용자 = new ArrayList<>(); 필드 비공개 부울 제거됨 = false; } |
1 2 3 4 5 6 7 8 9 10 |
@N1qlPrimaryIndexed ViewIndexed(디자인 문서 = "보안 그룹") public 인터페이스 보안 그룹 저장소 확장 카우치베이스 페이징 및 정렬 저장소<보안 그룹, 문자열> { 쿼리("#{#n1ql.selectEntity} 여기서 #{#n1ql.filter} 및 companyId = $1, removed = false " + " AND ARRAY_CONTAINS(users, $2) ") 목록<SecurityGroup> 목록 사용자 그룹(문자열 companyId, 문자열 userId); } |
그리고 기본 엔티티 클래스는 스프링 데이터와 카우치베이스에서 더 잘 작동하기 위한 작은 해킹이기도 합니다:
1 2 3 4 5 6 7 |
public 클래스 기본 엔티티 { Getter(보호됨) 세터(보호됨) 무시 보호됨 문자열 _class; } |
마지막으로 SecurityConfig 클래스의 구현은 다음과 같습니다:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
@Configuration @EnableWebMvc public 클래스 SecurityConfig 확장 웹보안구성자 어댑터 { 오토와이어드 비공개 사용자 세부 정보 서비스 사용자 세부 정보 서비스; @Bean 오버라이드 public 인증 관리자 인증 관리자 빈() 던지기 예외 { 반환 super.인증 관리자 빈(); } 오토와이어드 public void 글로벌 사용자 세부 정보(인증 관리자 빌더 auth) 던지기 예외 { auth.사용자 세부 서비스(사용자 세부 정보 서비스) .암호인코더(인코더()); } 오버라이드 public void 구성( 웹 보안 웹 ) 던지기 예외 { 웹.무시().앤트매처( HttpMethod.옵션, "/**" ); } 오버라이드 보호됨 void 구성(HttpSecurity http) 던지기 예외 { http .csrf().비활성화() .권한 부여 요청() .앤트매처("/oauth/token").permitAll() .앤트매처("/api-docs/**").permitAll() .anyRequest().인증() .그리고().익명().비활성화(); } @Bean public 토큰스토어 토큰 저장소() { 반환 new 인메모리토큰스토어(); } @Bean public 비밀번호 인코더 인코더(){ 반환 NoOpPasswordEncoder.getInstance(); } @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource 출처 = new UrlBasedCorsConfigurationSource(); CorsConfiguration 구성 = new CorsConfiguration(); 구성.설정 허용 자격 증명(true); 구성.추가 허용된 출처("*"); 구성.추가 허용 헤더("*"); 구성.추가 허용 메서드("*"); 출처.registerCorsConfiguration("/**", 구성); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(출처)); bean.setOrder(0); 반환 bean; } } |
Spring-Boot 2.0에서는 더 이상 AuthenticationManager 빈을 직접 주입할 수 없지만 Spring Security 프레임워크에서는 여전히 필요합니다. 따라서 이 객체에 대한 액세스 권한을 얻으려면 작은 해킹을 구현해야 합니다:
1 2 3 4 5 |
@Bean 오버라이드 public 인증 관리자 인증 관리자 빈() 던지기 예외 { 반환 super.인증 관리자 빈(); } |
이 수업을 잘게 나누어 무슨 일이 일어나고 있는지 이해해 보겠습니다:
1 2 3 4 |
@Bean public 비밀번호 인코더 인코더(){ 반환 NoOpPasswordEncoder.getInstance(); } |
사용자의 비밀번호는 일반 텍스트로 되어 있으므로 새 NoOpPasswordEncoder 인스턴스를 반환하면 됩니다. 일반적인 표준은 BCryptPasswordEncoder 클래스의 인스턴스를 반환하는 것입니다.
1 2 3 4 |
@Bean public 토큰스토어 토큰 저장소() { 반환 new 인메모리토큰스토어(); } |
지금은 인메모리 토큰 저장소를 사용하겠지만, 2부에서는 카우치베이스를 토큰 저장소로 사용하는 방법도 살펴보겠습니다.
1 2 3 4 5 |
오토와이어드 public void 글로벌 사용자 세부 정보(인증 관리자 빌더 auth) 던지기 예외 { auth.사용자 세부 서비스(사용자 세부 정보 서비스) .암호인코더(인코더()); } |
여기서 OAuth2와 Spring Boot의 마법이 일어납니다. 구체적으로, 우리는 Spring에 우리의 사용자 세부 정보 서비스 를 사용하여 사용자를 검색할 수 있습니다. 이 코드 블록은 지금까지 수행한 작업의 핵심 부분입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource 출처 = new UrlBasedCorsConfigurationSource(); CorsConfiguration 구성 = new CorsConfiguration(); 구성.설정 허용 자격 증명(true); 구성.추가 허용된 출처("*"); 구성.추가 허용 헤더("*"); 구성.추가 허용 메서드("*"); 출처.registerCorsConfiguration("/**", 구성); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(출처)); bean.setOrder(0); 반환 bean; } |
이 블록을 사용하면 CORS(교차 출처 리소스 공유)를 사용하여 요청할 수 있습니다.
1 2 3 4 |
오버라이드 public void 구성( 웹 보안 웹 ) 던지기 예외 { 웹.무시().앤트매처( HttpMethod.옵션, "/**" ); } |
마지막으로 JQuery를 통해 API를 호출해야 하는 경우 위의 코드도 추가해야 합니다. 그렇지 않으면 "사전 비행에 대한 응답에 HTTP 확인 상태가 없습니다.." 오류입니다.
이제 남은 것은 인증 서버를 추가하는 것뿐입니다:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
가져오기 org.스프링 프레임워크.콩.공장.주석.자동 유선; 가져오기 org.스프링 프레임워크.컨텍스트.주석.구성; 가져오기 org.스프링 프레임워크.보안.인증.인증 관리자; 가져오기 org.스프링 프레임워크.보안.oauth2.구성.주석.구성자.클라이언트 세부 정보 서비스 구성자; 가져오기 org.스프링 프레임워크.보안.oauth2.구성.주석.웹.구성.권한부여서버구성자 어댑터; 가져오기 org.스프링 프레임워크.보안.oauth2.구성.주석.웹.구성.EnableAuthorizationServer; 가져오기 org.스프링 프레임워크.보안.oauth2.구성.주석.웹.구성자.권한 부여 서버 엔드포인트 구성자; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.토큰.토큰스토어; @Configuration 인증 서버 활성화 public 클래스 AuthorizationServerConfig 확장 권한부여서버구성자 어댑터 { 정적 final 문자열 CLIENT_ID = "android-client"; 정적 final 문자열 클라이언트_비밀 = "안드로이드-시크릿"; 정적 final 문자열 부여_유형_비밀번호 = "비밀번호"; 정적 final 문자열 권한 부여 코드 = "authorization_code"; 정적 final 문자열 REFRESH_TOKEN = "refresh_token"; 정적 final 문자열 IMPLICIT = "암시적"; 정적 final 문자열 SCOPE_READ = "read"; 정적 final 문자열 SCOPE_WRITE = "쓰기"; 정적 final 문자열 신뢰 = "신뢰"; 정적 final int 액세스_토큰_유효_초 = 1*60*60; 정적 final int 새로고침_토큰_유효기간_초 = 6*60*60; 오토와이어드 비공개 토큰스토어 토큰 저장소; 오토와이어드 비공개 인증 관리자 인증 관리자; 오버라이드 public void 구성(클라이언트 세부 정보 서비스 구성자 구성자) 던지기 예외 { 구성자 .인메모리() .withClient(CLIENT_ID) .비밀(클라이언트_비밀) .승인된 그랜트 유형(부여_유형_비밀번호, 권한 부여 코드, REFRESH_TOKEN, IMPLICIT ) .범위(SCOPE_READ, SCOPE_WRITE, 신뢰) .액세스 토큰 유효 기간 초(액세스_토큰_유효_초). 새로고침토큰유효기간초(새로고침_토큰_유효기간_초); } 오버라이드 public void 구성(권한 부여 서버 엔드포인트 구성자 엔드포인트) 던지기 예외 { 엔드포인트.토큰 저장소(토큰 저장소) .인증 관리자(인증 관리자); } 오버라이드 public void 구성(권한 부여 서버 보안 구성자 oauthServer) 던지기 예외 { oauthServer.토큰 키 액세스("permitAll()") .토큰 액세스 확인("isAuthenticated()"); } } |
이제 앱을 시작하고 Postman 또는 Jquery를 통해 호출할 수 있습니다:
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 |
var 데이터 = { "grant_type": "비밀번호", "username": "myuser", "비밀번호":"mypassword", "client_id":"android-client", "client_secret":"안드로이드-시크릿" } $.ajax({ 'url': "http://localhost:8080/oauth/token", 'type': 'POST', "crossDomain": true, "헤더": { '권한 부여': '기본 YW5kcm9pZC1jbGllbnQ6YW5kcm9pZC1zZWNyZXQ=', //android-client:android-secret(Base64) '콘텐츠 유형':'application/x-www-form-urlencoded'}, "데이터":데이터, '성공': 함수 (결과) { 콘솔.로그( "내 액세스 토큰 = "+ 결과.액세스 토큰); 콘솔.로그( "내 새로 고침 토큰 = "+ 결과.새로고침_토큰); 콘솔.로그("만료 = "+결과.expires_in) 성공 콜백() }, 'error': 함수 (XMLHttpRequest, textStatus, errorThrown) { 오류 콜백(XMLHttpRequest, textStatus, errorThrown) } }); |
성능 향상
Couchbase를 사용하는 경우 사용자 이름을 문서의 키로 사용하는 것이 좋습니다. 이렇게 하면 키-값 저장소 를 사용하여 N1QL 쿼리를 실행하는 대신 로그인 성능을 크게 향상시킬 수 있습니다.
Couchbase, OAuth 보안 또는 Spring 및 OAuth2 인증 최적화에 대해 궁금한 점이 있으면 다음 주소로 트윗해 주세요. @deniswsrosa
아주 좋은 글입니다. 이 게시물의 코드를 공유해 주시겠어요?