[2025백엔드] 스프링인액션 독서 #6 - 4장. 스프링 시큐리티 1

2025. 8. 4. 23:34·CS/백엔드

스프링 시큐리티. 보안에 중요하지

책 초반에 나오는데, 나는 OAuth를 위해 미리 SecurityConfig 를 작성했지만 이를 작성하지 않을경우 기본 HTTP 인증 대화상자가 뜬다.

 

기본 http 인증 대화상자의 사진을 첨부하고 싶은데 나는 OAuth 관련해서 의존성이나 이런게 이것저것 엮여있어서 에러가 뜨므로 실패

 

어쨌든 SecurityConfig 없이 기본으로만 사용했을 경우..

모든 HTTP 요청은 인증되어야하며, 특정 역할/권한이 없고, 로그인페이지가 없고, HTTP 기본인증을 사용하고, 사용자는 하나만 있다.

 

이러면 당연히 웹페이지를 만드는데 제약이 있을수밖에 없다. 따라서 내가 직접 사전에 security 관련 설정을 해두어야한다.


사용자 스토어

스프링 시큐리티는 스프링 6으로 넘어오면서 몇 개의 명령어 수정이 이루어졌다. 이 책은 스프링 5를 기반으로 출판된 책이므로, 몇 개의 내용이 실제 내 시큐리티 문서와는 다르다.

 

우선, 스프링인액션 4장 121쪽을 볼 때 위에 있는 코드가 내가 작성한 권한이나 로그인 이전 설정 등을 해둔 것이고, 아래가 사용자 스토어 설정이다. 책에서는 사용자 스토어 설정을 먼저 다뤘으므로 사용자 스토어에 대해 보면

 

GPT피셜로는 사용자의 정보를 찾아오는 곳이라는데.. 우선 더 알아봐야할 것 같다.


인메모리 사용자 스토어

...
Override
public void configure(AuthenticationManageBuilder auth) throws Exception {
	auth.inMemoryAuthentication()
    .withUser("user1")
    .password("{noop}password1")
    .authorities("ROLE_USER")
    .and()
    .withUser("user2")
    .password("{noop}password2")
    .authorities("ROLE_USER");
}

이런식으로 되어있다.

로그인 버튼을 누르면 메서드인자로 전한 user1을 아이디로, password1을 비밀번호로 누르면 로그인에 성공한다.

.withuser()를 호출하며 사용자의 구성이 시작되고, 차례로 아이디, 비밀번호, 권한을 부여할 수 있다.

 

스프링5부터는 메모리를 암호화하지 않을 경우 에러가 발생하므로 {noop}을 통해 암호화하지 않고 비밀번호를 전달할 수 있게 짜놨다.

 

그런데 그냥 코드를 봐도 알 수 있듯이, 사용자의 정보를 개발자가 미리 저장해놔야 하기 때문에, 추가, 삭제, 변경이 번거롭다. 또한 사용자가 자신의 정보를 변경하기 위해서는 인메모리 방식이 아니라 다른 방식을 사용해야한다.


JDBC 사용자스토어

 

...
import javax.sql.DataSource;
...

@Autowired
Datatsource datasource

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth
    .jdbcAuthentication();
    .dataSource(dataSource);
    
}

예시코드는 다음과 같다.

이렇게 코드를 짜면 스프링 시큐리티는 기본적으로 코드를 수행하며 user 테이블에 사용자 정보를, authorities 테이블에 권한을, 그룹의 사용자는 group_members 테이블에, 그룹의 권한은 group_authorities에 저장한다. 이름, 비밀번호, 활성화 여부를 검색하고, 권한을 검색하고, 그룹을 찾고, 그룹의 권한을 찾는다. (이와 관련한 스키마는 스프링인액션 4장 125~126쪽 참고)

 

또 이처럼 사전 지정된 DB테이블과 쿼리를 활용하려면 사전에 테이블을 생성하고 데이터를 추가해야한다. (아직 회원가입 기능이 없으므로...) 추가하는 SQL 코드는 127쪽에 있고, 나는 회원가입 기능이 미리 만들어져있으므로 (그리고 앞으로도 그럴 예정이므로) 굳이 깊게 읽지는 않을 생각이다. SQL문도 간단하기도 하고

 

다만 비밀번호의 경우 사용자 비밀번호를 암호화하지 않을 경우엔 오류가 발생한다. 

 

기본에서 벗어나려면

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth
    .jdbcAuthentication();
      .dataSource(dataSource);
      .userByUsernameQuery(
      	"select username, password, enabled from users " +
        "where username=?")
       .authoritiesByUsernameQuery(
       	"select username, authority from authorities " +
        "where username=?");    
}

여기는 스프링 기본 스키마를 사용했지만 내가 따로 테이블을 저장하고 관리하고 싶으면 이렇게 코드를 작성해도 된다.

SQL문 내용은 비슷해보이지만, 사용자 정보조회와 권환확인이라는 목적이 다르기 때문에 다른 메서드로 분리가 된거고...

 

WHERE 절에 사용되는 매개변수는 오직 한 개이며, username이어야한다. 그리고 username, password, enabled 값을 반환해야한다.

 

비밀번호 암호화

위에서 짠 코드에 메서드 하나만 추가하면 된다.

AuthenticationManagerBuilder.passwordEncoder(new BCryptPasswordEncoder());

이러면 비밀번호가 암호화되어 넘어간다.

BCryptPasswordEncoder는 bcrypt를 해싱 암호화한다. 이외에도 NoOpPasswordEncoder (암호화하지 않음) / Pbkdf2PasswordEncoder (PBKDF2 암호화) 등이 있다.


LDAP 사용자스토어

LDAP는 계층 구조의 디렉토리이다.

대규묘 조직에서 사용자, 그룹 등의 정보를 중앙집중식으로 관리 / 검색하기 위한 프로토콜이라고 한다.

 

https://yongho1037.tistory.com/796

 

[LDAP] 개념 잡기

LDAP에 대한 여러 자료를 조사하여 정리한 글 입니다. 참조한 레퍼런스들은 하단 링크로 첨부하였습니다. LDAP 이란 ? Lightweight Directory Access Protocol 네트워크 상에서 조직이나 개인정보 혹은 파일이

yongho1037.tistory.com

이 블로그를 참고하면..

 

이런식으로 계층형태로 이루어져있다. uid는 엔트리의 고유한 id이다.

각 노드들을 엔트리라고 칭하고, 엔트리의 종류는 다음과 같다.

출처 : https://hec-ker.tistory.com/319

예시를 들자면..

uid=jpaker는 pisoftwar.com에 속한 도메인의 people이라는 ou에 속한 사용자다.

 

이런 계층구조는 우리가 흔히 쓰는 DNS의 역순으로,

myWebSite.co.kr 이런 도메인이 있다면 LDAP 계층구조는

 

dc=myWebSite

ㄴdc=co

ㄴdc=kr

... 이런식으로 써야한다.

 

LDAP 방식을 사용하면 스프링에서 기본적인 정보를 채워 LDAP서버로 보내고, LDAP서버는 이를 받아서 요청한 연산을 처리한다.

스프링 시큐리티의 LDAP 서버는 로컬호스트 33389 포트로 접속된다.

 

다만 내 컴퓨터가 아니라 다른 컴퓨터에서 접속하려는 경우에는 다음과 같이 LDAP서버의 위치를 직접 지정해줄 수 있다.

.contextSource().url("ldap://tacocloud.com:/389/dc=tacocloud,dc=com")

 

코드

...
@Override
protected void configuer(AuthenticationManagerBuilder auth) throws Exception {
	auth
    	.ldapAutentication()
        .userSearchFilter("(uid={0}")
        .groupSearchFilter("member={0}");
}

기본 코드

이렇게되면 트리는 알아서 최상층부터 찾기시작한다. 

uid={0} 사용자 아이디로 입력한 값을 LDAP 서버에서 찾기 시작한다. 아이디를 kkd로 넣었다면 uid=kkd를 LDAP 서버에서 찾는 식으로.. 별다른 정의가 없으므로 최상층부터 찾기 시작한다.

 

userSearchFilter와 groupSearchFilter는 내용이 서로 같으나 userSearchFilter는 user 개인을, groupSearchFilter는 group을 검사하기에, 즉 역할이 다르기 때문에 다른 메서드를 이용한다.

 

groupSearchFilter는 member 속성에 로그인한 사용자의 DN이 포함된 그룹을 찾으라는 뜻이다.

LDAP는 (속성) = (값)의 쌍으로 저장되며,

uid = 328

이런식으로 저장된다. uid가 속성이름이고 328이 값이다.

 

member: uid=328, ou=people

이런식으로도 정의할 수 있다. 속성이름이 member

 

그렇다면

.groupSearchFilter("member={0}");

여기에는 member 에 값을 속성에 해당하는 값을 전해준다. (정확히는 uid나 ou 등을 포함한 DN값)

DN값은 스프링에서 자동으로 전해준다.

 

 

위의 저 코드가 실행되면 uid든 member든 최상층 트리부터 찾기 시작하는데 어느 노드부터 찾을지 지정하는 방법은

.userSearchBase("ou=people")
.groupSearchBase("ou=groups")

이렇게 지정해줄 수 있다.

group
 └─ member
     ├─ uid=john,ou=people,dc=example,dc=com
     ├─ uid=alice,ou=people,dc=example,dc=com
     └─ uid=bob,ou=people,dc=example,dc=com

member의 노드는 위의 구성으로 이루어져있다.

 

비밀번호 비교하기

LDAP의 기본인증전략은 사용자가 직접 LDAP 서버에서 인증받도록 한다.

방법은 메서드 한 줄이면 된다. 간단간단

.passwordCompare();

이거 한 줄이면 된다.

로그인 폼에 입력된 비밀번호가 LDAP 서버의 userPassword 속성값과 비교된다. 또한 비밀번호 비교는 LDAP 서버에서 처리되므로 암호화/복호화나 직접적인 비밀번호 노출은 되지 않는다.

 

다만 userPassword가 아니라 다른 속성에 비밀번호가 저장되어 있다면

.passwordCompare()
.passwordEncoder (new BCryptPasswordEncoder())
.passwordAttribute(속성명);

으로 할 수 있다.

다만 암호화는 수동으로 해야한다. LDAP 서버에서는 BCrypt 암호화를 진행하므로 LDAP 서버에 보낼 비밀번호도 암호화 시킨 후 보낸다.

 

내장 LDAP 서버 구성하기

LDAP 서버는 직접 만들어서 지정해야하는데, 서버를 만들지 못한 경우 스프링이 지원하는 내장 LDAP 서버를 이용할 수 있다. 다만 테스트용으로는 적합하지만 실제 배포용으로는 부적합하다.

 

우선 내장 LDAP서버를 지정하는 방법은

.root("dc=...")

로 설정할 수 있다. 이 때 원격서버의 루트를 지정하는 contextSource 메서드가 아니라 root 메서드와 그 인자를 통해서 루트를 지정할 수 있다.

 

또한 LDAP 서버가 시작되면 classpath에서 LDIF 파일을 찾고, LDIF는 LDAP 데이터를 나타내는 표준화된 방법이다. classpath를 검색하지 않고 직접 지정하고 싶다면 .ldif() 메서드를 사용하면 된다.

 

LDIF 파일의 예시는 스프링인액션 4장 137쪽에 있다. LDAP 방식에 대해 더 익숙해지고 본다면 더 좋을 것이다. 우선은 cn에 이름을, sn에 성을 붙이고 uid에 아이디, password에 비밀번호를 저장한다. objectclass는 person, organizationalPerson 등이 있는데, 이건 우선 나중에 공부해봅시다.

 

추가로 group에 대해서도 있는데

member: uid=user1, ou=people, dc=..., dc=com
member: uid=user2, ou=people, dc=..., dc=com
adminmember: uid=adminuser1, ou=people, dc=..., dc=com

이런식으로 group이 만들어지고 회원가입 시에 member만 추가해주면 된다.

 

결국 

.groupSearchFilter("member={0}");

이 코드는 group에서 스프링이 전해주는 로그인한 사용자의 uid, ou 등의 값을 조회하는 기능을 한다.

 

우선은 이 뒤로도 내용이 있지만 너무 길어지니 한 번 끊기로..

LDAP 방식이 처음보기도하고 좀 어려워서 시간이 좀 걸렸다. 4장도 읽으려면 꽤 걸리겠는걸

'CS > 백엔드' 카테고리의 다른 글

[백엔드] slf4j로 로그 남기기  (0) 2025.10.27
[2025백엔드] 스프링인액션 독서 #7 - 4장. 스프링 시큐리티 2  (2) 2025.08.05
[2025백엔드] 스프링인액션 독서 #5 - 3장. 데이터로 작업하기 #3  (2) 2025.08.04
[2025백엔드] 스프링인액션 독서 #4 - 3장.데이터로 작업하기 2  (2) 2025.08.01
[2025백엔드] 스프링인액션 독서 #3 - 3장.데이터로 작업하기 1  (3) 2025.07.29
'CS/백엔드' 카테고리의 다른 글
  • [백엔드] slf4j로 로그 남기기
  • [2025백엔드] 스프링인액션 독서 #7 - 4장. 스프링 시큐리티 2
  • [2025백엔드] 스프링인액션 독서 #5 - 3장. 데이터로 작업하기 #3
  • [2025백엔드] 스프링인액션 독서 #4 - 3장.데이터로 작업하기 2
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[2025백엔드] 스프링인액션 독서 #6 - 4장. 스프링 시큐리티 1
상단으로

티스토리툴바