이전 블로그 게시물에서는 간단한 OAuth2 인증 구성 방법. 하지만 저희 구현에는 인메모리 토큰 저장소를 사용하고 있다는 큰 결함이 있습니다.
인메모리 토큰 저장소는 노드 간에 쉽게 공유할 수 없고 서버가 다시 시작될 경우 저장소의 모든 액세스 토큰을 잃게 되므로 개발 중이거나 애플리케이션에 단일 서버가 있는 경우에만 사용해야 합니다.
스프링 보안 인증 2 에는 이미 JDBC 및 JWT에 대한 기본 지원 기능이 있습니다. 그러나 토큰을 다른 곳에 저장해야 하는 경우에는 자체 스프링 보안 토큰 저장소를 만들어야 합니다. 안타깝게도 이러한 작업을 구현하는 것은 사소한 작업이 아니며 다음 레시피를 통해 몇 시간의 작업을 절약할 수 있기를 바랍니다.
액세스 토큰과 새로 고침 토큰을 저장하는 두 개의 엔티티와 각각의 리포지토리를 만드는 것부터 시작하겠습니다:
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 |
가져오기 롬복.데이터; 가져오기 org.스프링 프레임워크.데이터.주석.Id; 가져오기 org.스프링 프레임워크.데이터.카우치베이스.핵심.매핑.문서; 가져오기 org.스프링 프레임워크.보안.oauth2.공통.OAuth2AccessToken; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.OAuth2인증; 문서 @Data public 클래스 카우치베이스 액세스 토큰 { @Id 비공개 문자열 id; 비공개 문자열 토큰아이디; 비공개 OAuth2AccessToken 토큰; 비공개 문자열 인증아이디; 비공개 문자열 사용자 이름; 비공개 문자열 clientId; 비공개 문자열 인증; 비공개 문자열 새로고침 토큰; public OAuth2인증 get인증() { 반환 직렬화 가능 객체 변환기.역직렬화(인증); } public void 설정 인증(OAuth2인증 인증) { 이.인증 = 직렬화 가능 객체 변환기.직렬화(인증); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@N1qlPrimaryIndexed ViewIndexed(디자인 문서 = "couchbaseAccessToken") public 인터페이스 카우치베이스 액세스 토큰 저장소 확장 카우치베이스 페이징 및 정렬 저장소<카우치베이스 액세스 토큰, 문자열> { 목록<CouchbaseAccessToken> findByClientId(문자열 clientId); 목록<CouchbaseAccessToken> 고객 아이디와 사용자 이름으로 찾기(문자열 clientId, 문자열 사용자 이름); 선택 사항<CouchbaseAccessToken> 토큰 ID로 찾기(문자열 토큰아이디); 선택 사항<CouchbaseAccessToken> 새로고침 토큰으로 찾기(문자열 새로고침 토큰); 선택 사항<CouchbaseAccessToken> findByAuthenticationId(문자열 인증아이디); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
가져오기 롬복.데이터; 가져오기 org.스프링 프레임워크.데이터.주석.Id; 가져오기 org.스프링 프레임워크.데이터.카우치베이스.핵심.매핑.문서; 가져오기 org.스프링 프레임워크.보안.oauth2.공통.OAuth2RefreshToken; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.OAuth2인증; 문서 @Data public 클래스 카우치베이스 리프레시 토큰 { @Id 비공개 문자열 id; 비공개 문자열 토큰아이디; 비공개 OAuth2RefreshToken 토큰; 비공개 문자열 인증; public OAuth2인증 get인증() { 반환 직렬화 가능 객체 변환기.역직렬화(인증); } public void 설정 인증(OAuth2인증 인증) { 이.인증 = 직렬화 가능 객체 변환기.직렬화(인증); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
가져오기 org.스프링 프레임워크.데이터.카우치베이스.핵심.쿼리.N1qlPrimaryIndexed; 가져오기 org.스프링 프레임워크.데이터.카우치베이스.핵심.쿼리.인덱싱된 보기; 가져오기 org.스프링 프레임워크.데이터.카우치베이스.저장소.카우치베이스 페이징 및 정렬 저장소; 가져오기 자바.활용.목록; 가져오기 자바.활용.선택 사항; @N1qlPrimaryIndexed ViewIndexed(디자인 문서 = "couchbaseAccessToken") public 인터페이스 카우치베이스 액세스 토큰 저장소 확장 카우치베이스 페이징 및 정렬 저장소<카우치베이스 액세스 토큰, 문자열> { 목록<CouchbaseAccessToken> findByClientId(문자열 clientId); 목록<CouchbaseAccessToken> 고객 아이디와 사용자 이름으로 찾기(문자열 clientId, 문자열 사용자 이름); 선택 사항<CouchbaseAccessToken> 토큰 ID로 찾기(문자열 토큰아이디); 선택 사항<CouchbaseAccessToken> 새로고침 토큰으로 찾기(문자열 새로고침 토큰); 선택 사항<CouchbaseAccessToken> findByAuthenticationId(문자열 인증아이디); } |
참고 OAuth2인증 는 인터페이스이므로 객체를 직렬화하여 데이터베이스에 저장하는 것 외에 다른 옵션이 없습니다. 다음은 직렬화/역직렬화를 담당하는 클래스입니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public 클래스 직렬화 가능 객체 변환기 { public 정적 문자열 직렬화(OAuth2인증 객체) { 시도 { 바이트[] 바이트 = 직렬화 유틸리티.직렬화(객체); 반환 Base64.encodeBase64String(바이트); } catch(예외 e) { e.프린트스택트레이스(); throw e; } } public 정적 OAuth2인증 역직렬화(문자열 encodedObject) { 시도 { 바이트[] 바이트 = Base64.decodeBase64(encodedObject); 반환 (OAuth2인증) 직렬화 유틸리티.역직렬화(바이트); } catch(예외 e) { e.프린트스택트레이스(); throw e; } } |
이제 드디어 사용자 정의 스프링 오아시스2 토큰 저장소입니다. 이를 위해 필요한 것은 토큰 저장소의 긴 메서드 목록인 org.springframework.security.oauth2.provider.token.TokenStore:
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
가져오기 org.스프링 프레임워크.보안.oauth2.공통.OAuth2AccessToken; 가져오기 org.스프링 프레임워크.보안.oauth2.공통.OAuth2RefreshToken; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.OAuth2인증; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.토큰.인증키 생성기; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.토큰.기본 인증 키 생성기; 가져오기 org.스프링 프레임워크.보안.oauth2.공급자.토큰.토큰스토어; 가져오기 자바.io.UnsupportedEncodingException; 가져오기 자바.수학.BigInteger; 가져오기 자바.보안.메시지 다이제스트; 가져오기 자바.보안.NoSuchAlgorithmException; 가져오기 자바.활용.*; public 클래스 카우치베이스토큰스토어 구현 토큰스토어 { 비공개 카우치베이스 액세스 토큰 저장소 cbAccess토큰저장소; 비공개 카우치베이스 리프레시 토큰 저장소 cbRefresh토큰저장소; public 카우치베이스토큰스토어(카우치베이스 액세스 토큰 저장소 cbAccess토큰저장소, 카우치베이스 리프레시 토큰 저장소 cbRefresh토큰저장소){ 이.cbAccess토큰저장소 = cbAccess토큰저장소; 이.cbRefresh토큰저장소 = cbRefresh토큰저장소; } 비공개 인증키 생성기 인증키 생성기 = new 기본 인증 키 생성기(); 오버라이드 public OAuth2인증 읽기 인증(OAuth2AccessToken 액세스 토큰) { 반환 읽기 인증(액세스 토큰.getValue()); } 오버라이드 public OAuth2인증 읽기 인증(문자열 토큰) { 선택 사항<CouchbaseAccessToken> 액세스 토큰 = cbAccess토큰저장소.토큰 ID로 찾기(추출토큰키(토큰)); 만약 (액세스 토큰.isPresent()) { 반환 액세스 토큰.get().get인증(); } 반환 null; } 오버라이드 public void storeAccessToken(OAuth2AccessToken 액세스 토큰, OAuth2인증 인증) { 문자열 새로고침 토큰 = null; 만약 (액세스 토큰.getRefreshToken() != null) { 새로고침 토큰 = 액세스 토큰.getRefreshToken().getValue(); } 만약 (readAccessToken(액세스 토큰.getValue()) != null) { 이.removeAccessToken(액세스 토큰); } 카우치베이스 액세스 토큰 cat = new 카우치베이스 액세스 토큰(); cat.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); cat.setTokenId(추출토큰키(액세스 토큰.getValue())); cat.setToken(액세스 토큰); cat.setAuthenticationId(인증키 생성기.extractKey(인증)); cat.설정 사용자 이름(인증.isClientOnly() ? null : 인증.getName()); cat.setClientId(인증.getOAuth2Request().getClientId()); cat.설정 인증(인증); cat.setRefreshToken(추출토큰키(새로고침 토큰)); cbAccess토큰저장소.저장(cat); } 오버라이드 public OAuth2AccessToken readAccessToken(문자열 토큰값) { 선택 사항<CouchbaseAccessToken> 액세스 토큰 = cbAccess토큰저장소.토큰 ID로 찾기(추출토큰키(토큰값)); 만약 (액세스 토큰.isPresent()) { 반환 액세스 토큰.get().getToken(); } 반환 null; } 오버라이드 public void removeAccessToken(OAuth2AccessToken oAuth2AccessToken) { 선택 사항<CouchbaseAccessToken> 액세스 토큰 = cbAccess토큰저장소.토큰 ID로 찾기(추출토큰키(oAuth2AccessToken.getValue())); 만약 (액세스 토큰.isPresent()) { cbAccess토큰저장소.삭제(액세스 토큰.get()); } } 오버라이드 public void storeRefreshToken(OAuth2RefreshToken 새로고침 토큰, OAuth2인증 인증) { 카우치베이스 리프레시 토큰 crt = new 카우치베이스 리프레시 토큰(); crt.setId(UUID.randomUUID().toString()+UUID.randomUUID().toString()); crt.setTokenId(추출토큰키(새로고침 토큰.getValue())); crt.setToken(새로고침 토큰); crt.설정 인증(인증); cbRefresh토큰저장소.저장(crt); } 오버라이드 public OAuth2RefreshToken readRefreshToken(문자열 토큰값) { 선택 사항<CouchbaseRefreshToken> 새로고침 토큰 = cbRefresh토큰저장소.토큰 ID로 찾기(추출토큰키(토큰값)); 반환 새로고침 토큰.isPresent()? 새로고침 토큰.get().getToken() :null; } 오버라이드 public OAuth2인증 새로고침 토큰에 대한 인증 읽기(OAuth2RefreshToken 새로고침 토큰) { 선택 사항<CouchbaseRefreshToken> rtk = cbRefresh토큰저장소.토큰 ID로 찾기(추출토큰키(새로고침 토큰.getValue())); 반환 rtk.isPresent()? rtk.get().get인증() :null; } 오버라이드 public void removeRefreshToken(OAuth2RefreshToken 새로고침 토큰) { 선택 사항<CouchbaseRefreshToken> rtk = cbRefresh토큰저장소.토큰 ID로 찾기(추출토큰키(새로고침 토큰.getValue())); 만약 (rtk.isPresent()) { cbRefresh토큰저장소.삭제(rtk.get()); } } 오버라이드 public void 새로고침 토큰을 사용하여 액세스 토큰을 제거합니다.(OAuth2RefreshToken 새로고침 토큰) { 선택 사항<CouchbaseAccessToken> 토큰 = cbAccess토큰저장소.새로고침 토큰으로 찾기(추출토큰키(새로고침 토큰.getValue())); 만약(토큰.isPresent()){ cbAccess토큰저장소.삭제(토큰.get()); } } 오버라이드 public OAuth2AccessToken getAccessToken(OAuth2인증 인증) { OAuth2AccessToken 액세스 토큰 = null; 문자열 인증아이디 = 인증키 생성기.extractKey(인증); 선택 사항<CouchbaseAccessToken> 토큰 = cbAccess토큰저장소.findByAuthenticationId(인증아이디); 만약(토큰.isPresent()) { 액세스 토큰 = 토큰.get().getToken(); 만약(액세스 토큰 != null && !인증아이디.같음(이.인증키 생성기.extractKey(이.읽기 인증(액세스 토큰)))) { 이.removeAccessToken(액세스 토큰); 이.storeAccessToken(액세스 토큰, 인증); } } 반환 액세스 토큰; } 오버라이드 public 컬렉션<OAuth2AccessToken> 찾기토큰별클라이언트아이디및사용자명(문자열 clientId, 문자열 사용자 이름) { 컬렉션<OAuth2AccessToken> 토큰 = new ArrayList<OAuth2AccessToken>(); 목록<CouchbaseAccessToken> 결과 = cbAccess토큰저장소.고객 아이디와 사용자 이름으로 찾기(clientId, 사용자 이름); 결과.forEach(e-> 토큰.추가(e.getToken())); 반환 토큰; } 오버라이드 public 컬렉션<OAuth2AccessToken> 찾기토큰별클라이언트아이디(문자열 clientId) { 컬렉션<OAuth2AccessToken> 토큰 = new ArrayList<OAuth2AccessToken>(); 목록<CouchbaseAccessToken> 결과 = cbAccess토큰저장소.findByClientId(clientId); 결과.forEach(e-> 토큰.추가(e.getToken())); 반환 토큰; } 비공개 문자열 추출토큰키(문자열 값) { 만약(값 == null) { 반환 null; } else { 메시지 다이제스트 다이제스트; 시도 { 다이제스트 = 메시지 다이제스트.getInstance("MD5"); } catch (NoSuchAlgorithmException var5) { throw new 불법 상태 예외("MD5 알고리즘을 사용할 수 없습니다. 치명적(JDK에 있어야 함)."); } 시도 { 바이트[] e = 다이제스트.다이제스트(값.getBytes("UTF-8")); 반환 문자열.형식("%032x", new 개체[]{new BigInteger(1, e)}); } catch (UnsupportedEncodingException var4) { throw new 불법 상태 예외("UTF-8 인코딩을 사용할 수 없습니다. 치명적(JDK에 있어야 함)."); } } } } |
마지막으로 SecurityConfig 클래스에서 이전 글에서 생성한 인스턴스를 반환합니다. 이제 인스턴스를 반환합니다. 카우치베이스토큰스토어 대신 인메모리토큰스토어:
1 2 3 4 5 6 7 8 9 10 11 |
@자동 유선 비공개 카우치베이스 액세스 토큰 저장소 카우치베이스 액세스 토큰 저장소; @자동 유선 비공개 카우치베이스 리프레시 토큰 저장소 카우치베이스 리프레시 토큰 저장소; @Bean public 토큰스토어 토큰 저장소() { 반환 new 카우치베이스토큰스토어(카우치베이스 액세스 토큰 저장소, 카우치베이스 리프레시 토큰 저장소); } |
다음은 전체 버전의 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 61 62 63 64 65 66 |
@Configuration @EnableWebMvc public 클래스 SecurityConfig 확장 웹보안구성자 어댑터 { 오토와이어드 비공개 사용자 세부 정보 서비스 사용자 세부 정보 서비스; 오토와이어드 비공개 카우치베이스 액세스 토큰 저장소 카우치베이스 액세스 토큰 저장소; 오토와이어드 비공개 카우치베이스 리프레시 토큰 저장소 카우치베이스 리프레시 토큰 저장소; 오토와이어드 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; } @Bean 오버라이드 public 인증 관리자 인증 관리자 빈() 던지기 예외 { 반환 super.인증 관리자 빈(); } } |
잘했어요! 그게 우리가 해야 할 전부입니다.
액세스 토큰은 데이터베이스에서 다음과 같이 표시됩니다:
1 2 |
선택 * 에서 테스트 어디 _class = 'com.bc.quicktask.standalone.model.CouchbaseAccessToken' |
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 |
[ { "your_버킷_이름": { "_class": "com.bc.quicktask.standalone.model.CouchbaseAccessToken", "인증": "rO0ABXNyAEFvcmcuc3ByaW5nZnJhbWV3b3JrLnNlY3VyaXR5Lm9hdXRoMi5wcm92aWRlci5PQXV0aDJBdXRoZW50aWNhdGlvbr1ACwIWYlITAgACTAANc3RvcmVkUmVxdWVzdHQAPExvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L29hdXRoMi9wcm92aWRlci9PQXV0aDJSZXF1ZXN0O0wAEnVzZXJBdXRoZW50aWNhdGlvbnQAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3NlY3VyaXR5L2NvcmUvQXV0aGVudGljYXRpb247eHIAR29yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkuYXV0aGVudGljYXRpb24uQWJzdHJhY3RBdXRoZW50aWNhdGlvblRva2Vu06oofm5HZA4CAANaAA1hdXRoZW50aWNhdGVkTAALYXV0aG9yaXRpZXN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247TAAHZGV0YWlsc3QAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAHNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHQAEExqYXZhL3V0aWwvTGlzdDt4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWNxAH4ABHhwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+AAxwc3IAOm9yZy5zcHJpbmdmcmFtZXdvcmsuc2VjdXJpdHkub2F1dGgyLnByb3ZpZGVyLk9BdXRoMlJlcXVlc3QAAAAAAAAAAQIAB1oACGFwcHJvdmVkTAALYXV0aG9yaXRpZXNxAH4ABEwACmV4dGVuc2lvbnN0AA9MamF2YS91dGlsL01hcDtMAAtyZWRpcmVjdFV, "authenticationId": "202d6940ebd428bbe2098530c8de3958", "clientId": "myclient", "refreshToken": "7613ffc6480ae83beb8f0988ef9ecfcf", "토큰": { "_class": "org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", "추가 정보": {}, "만료": 1537420023432, "refreshToken": { "_class": "org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", "만료": 1537420023421, "value": "c44db735-403e-49d6-8a22-cfe0e29f21d3" }, "범위": [ "신뢰", "read", "쓰기" ], "토큰 유형": "무기명", "value": "7759804c-e1c6-4d63-9520-8737f5b46dbf" }, "tokenId": "12b6bd9de380e1dfab348f4c15abb805", "username": "myuser" } } ] |
나는 사용했습니다 caelwinner's 프로젝트에 대해 특별히 감사의 말씀을 전합니다.
질문이 있으시면 언제든지 다음 주소로 트윗해 주세요. @deniswsrosa
안녕하세요 데니스,
우선, 매우 짧지만 간결한 튜토리얼에 감사드립니다. 1년이 넘었지만 잘 설명되어 있으며 훌륭한 선생님이라는 것을 인정해야 합니다.
질문과 요청이 하나 있습니다. 이 구현 OAuth2에 JWT를 추가할 수 있는지, 가능하다면 가이드를 제공해 주실 수 있나요?
회신을 기다리는 중입니다.
정말 감사합니다.