simDev1234
심플하고 차분하게
simDev1234
전체 방문자
오늘
어제
  • 분류 전체보기
    • Computer Science
      • Basic Math
      • Data Structure
      • Algorithm
      • Database
      • OS
    • Language
      • Java
      • Kotlin
      • SQL
    • Framework
      • Spring
      • Orm&Mapper
      • 프로젝트로 스프링 이해하기
      • 스프링 라이브러리
    • Infra
      • Cloud
      • Docker
      • Redis
      • AWS, Azure
      • Device
    • Etc
      • CleanCoding
    • Git,Github

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 참조타입
  • 컨트롤러
  • null
  • 자바프로그램
  • 404
  • 스프링
  • 자바
  • controllerTest
  • 자바메모리구조
  • JVM메모리구조
  • 참조변수
  • scanner #next() #nextLine()
  • 자바프로그래밍

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
simDev1234

심플하고 차분하게

[LMS 만들기] 스프링 시큐러티를 이용한 로그인/로그아웃
Framework/프로젝트로 스프링 이해하기

[LMS 만들기] 스프링 시큐러티를 이용한 로그인/로그아웃

2022. 10. 3. 06:29

|  스프링 시큐러티란?

- 스프링 시큐러티란, 스프링 기반의 애플리케이션의 보안을 담당하는 하위 프레임워크로,

   Filter를 통해 인증과 권한을 처리한다.

- HTTP URI를 통해 접근 가능한 경로를 제한할 수 있으며, 제한된 경로로 접속 시, user/password로 로그인을 함으로써 인증(Authentication) 후, 인가(Ahthorization)을 한다.

- 보다 자세한 사항에 대해서는 아래의 블로그가 잘 설명하고 있는 것 같다. 

https://catsbi.oopy.io/c0a4f395-24b2-44e5-8eeb-275d19e2a536

 

스프링 시큐리티 기본 API및 Filter 이해

목차

catsbi.oopy.io

 

|  스프링 시큐러티 사용해보기

1. dependency에 의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

- 의존성을 추가한 후에 application을 실행하면, default password가 콘솔에 uuid 코드로 나타난다.

* default username은 참고로 user이며, password는 restart할때마다 랜덤으로 생성되서 나타난다.

Using generated security password: d3ca14ad-22bd-4243-8432-c0874f82a693

- 이 상태에서 application 홈으로 가면 아래와 같이 나타난다. 여기에 user / password를 작성해서 들어가면 된다.

- 그런데 위와 같이 하게 되면 콘솔에 매번 새롭게 password가 뜨기 때문에 불필요할 뿐더러 보안상 이슈가 발생한다.

- 그렇기에 하나의 방법으로 application.yml에 username과 password를 저장해줄 수도 있다.

   그러나 이 방법 또한 기본적인 보안 외에는 세부적인 보안이 포함되지 않으므로, 사용자 정의 보안 기능을 구현하는 것이 좋다고 한다.

spring.security.user.name=user
spring.security.user.password=1234

 

2. 사용자 정의 보안 기능 구현

- 수업을 따라서 WebSecurityConfugurerAdapter를 사용하려하니, deprecated되었다고 떴다. 

- 블로그와 spring 공식문서에 있는 내용에 따라가보고, 다른 벨둥 내용도 확인해보았는데, 사용법은 비슷하지만

  아래와 같이 약간의 차이가 있는 것 같았다.

- 수업 내용이 많다보니 시간상 구버전으로 먼저 배우는 걸로 하고, 신규 버전은 차후에 다시 시도해보기로 했다.

  5.7.0-M2 이전 5.7.0-M2 이후
클래스 @Configuration, @EnableSecurity 추가
WebSecurityDonfigurerAdapter을 상속 (상속 없음)
httpSecurity 설정 configure(http) : void를 @Override filterChain(http) : SecurityFilterChain
를 통해 반환 인스턴스를 @Bean으로 등록
LDAP authentication configure(AuthenticationManagerBuilder) : void
를 @Override
@Bean으로 아래 두가지 등록
EmbeddedLdapServerContextSourceFactoryBean
AuthenticationManager

설정 클래스에 @Configuration/@EnableWebSecurity 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   

}
더보기

[ 공식 문서의 내용 ]

 

(1) HttpSecurity Configuring

// deprecated 버전
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}

// 최신 버전
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

 

(2) LDAP authentication

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .ldapAuthentication()
            .userDetailsContextMapper(new PersonContextMapper())
            .userDnPatterns("uid={0},ou=people")
            .contextSource()
            .port(0);
    }

}
@Configuration
public class SecurityConfiguration {
    @Bean
    public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
        EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean =
            EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
        contextSourceFactoryBean.setPort(0);
        return contextSourceFactoryBean;
    }

    @Bean
    AuthenticationManager ldapAuthenticationManager(
            BaseLdapPathContextSource contextSource) {
        LdapBindAuthenticationManagerFactory factory = 
            new LdapBindAuthenticationManagerFactory(contextSource);
        factory.setUserDnPatterns("uid={0},ou=people");
        factory.setUserDetailsContextMapper(new PersonContextMapper());
        return factory.createAuthenticationManager();
    }
}

 

 

(1) HttpSecurity Configuring 

- 말그대로 HTTP 보안 설정을 하는 것을 말한다.

- Client가 URI를 통해 HTTP 요청을 보낼 때, 권한 없는 이를 거르고, CSRF와 같은 해킹을 방지하는 조치를 취한다.

- 처음 개발을 할 때는 csrf().disabled()를 한 상태에서 개발 후, WebSecurity를 추가할 때 삭제하는게 좋다.

  해보니 이걸 안 한 상태로 개발을 먼저 시도하면 계속 401 에러 (authentication exception)가 나타났다.

HttpSecurity API

* 출처의 내용에 추가 : https://yeonyeon.tistory.com/185

method description
csrf().disabled() CSRF(cross-site request forgery) 방지 해제

* @EnableWebSecurity에 CSRF 방지 기능이 지원된다.
* CRSF : 사이트간 요청 위조를 말하는 것으로, 사용자의 의지와 무관하게 의도된 행위를 요청한다.
  (옥션의 개인정보 유출 사건, 2008)
* CSRF는 사이트간 요청이 잦은 경우 필요하며, 불필요한 경우 CSRF를 해제한다.
authorizeRequests() HttpServletRequest 요청 URL에 따라 접근 권한 설정
antMatchers("url") 요청 URL 경로 패턴 지정
authenticated() 인증 유저만 접근 허용
permitAll() 모든 유저에게 접근 허용
anonymous() 인증 안한 유저만 접근 허용
denyAll() 모든 유저의 접근 불가
and() HttpSecurity로 반환타입 변경
formLogin() form login 설정
loginPage("url") 커스텀 로그인 페이지 경로와 로그인 인증 경로 등록
failureHandler(handler) 로그인 실패 시 처리해줄 핸들러
loginProcessingUrl("url") 사용자 이름과 암호를 제출할 URL
defaultSuccessUrl("url") 로그인 성공 시 이동 페이지 (디폴트 페이지)
logout() 로그아웃
logoutUrl("url") 사용자 정의 로그 아웃
logoutRequestMatcher(
new AntPathRequestMatcher("url"))
로그아웃 요청 처리 경로
logoutSuccessUrl("url") 로그아웃 성공 시 이동 경로
invalidHttpSession(boolean) 로그아웃 후 세션 전체 삭제 여부

 

[1] URL 접속 권한 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * Http Security
     * 모두 접근 가능 : 홈, 회원 가입, 메일 인증
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(
                        "/",
                        "/member/register",
                        "/member/email-auth"
                )
                .permitAll();
                
        super.configure(http);
    }

}

 

[2] 사용자 지정 로그인 & 로그인 실패 핸들러 추가

[2-1]  SimpleUrlAuthenticationFailureHandler를 상속한 FailureHandler 생성

- onAuthenticationFailure() 메소드를 Override하여 인증 실패 시 처리할 내용을 작성한다.

public class UserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {

        String msg = "로그인에 실패하였습니다.";

        // 이메일 비활성화시
        if (exception instanceof InternalAuthenticationServiceException) {
            msg = exception.getMessage();
        }

        setUseForward(true);
        setDefaultFailureUrl("/member/login?error=true");
        request.setAttribute("errorMessage", msg);

        System.out.println("로그인에 실패하였습니다.");

        super.onAuthenticationFailure(request, response, exception);

    }
}

 

[2-2]  설정 클래스에 팩토리 메소드로 FailureHandler @Bean 등록

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    UserAuthenticationFailureHandler getFailureHandler() {
        return new UserAuthenticationFailureHandler();
    }

    /**
     * Http Security
     * 모두 접근 가능 : 홈, 회원 가입, 메일 인증
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(
                        "/",
                        "/member/register",
                        "/member/email-auth"
                )
                .permitAll();

        http.formLogin()
                .loginPage("/member/login")
                .failureHandler(getFailureHandler())
                .permitAll();

        super.configure(http);
    }

}

 

[2-3]  컨트롤러를 통해 로그인 프론트 페이지 매핑 & 로그인 프론트 페이지 작성

(이 부분은 생략)

 

(2) LDAP authentication

LDAP란? Lightweight Directory Access Protocol로, 경량화된 DAP를 말한다.

- LDAP는 핵심적인 사용자 정보를 저장한다. (core user identities)
- LDAP는 CRUD 중에서도 검색(조회)에 최적화되어 있으며, 바이너리 프로토콜이면서, 비동기 프로토콜이다.

rf. 디렉토리 서비스란, 이름을 기준으로 대상을 찾아 조회하거나 편집할 수 있는 서비스를 말하며,
    그 예로는 DNS 프로토콜, DAP/LDAP 가 있다.
  
LDAP authentication이 작동하는 방식
(1) client에서 LDAP database에 credentials와 함께 정보를 request
(2) 만약 client가 보낸 crendentials가 database의 core user identities와 일치하면 LDAP access 허가

- LDAP authentication은 LDAP database에 대한 접근 권한이 있는 자만 사용자정보를 조회하도록 하는 인증 단계이다.

 

[1] 회원 서비스에서 UserDetailService 상속하기

- 회원 서비스에서 UserDetailService를 상속한뒤 loadUserByUsername 메소드를 override한다.

- loadUserByUsername() 은 LDAP 데이터베이스에 로그인 정보를 로드하겠다는 의미이다.

- 차후 로그인 정보를 Principal의 getName() 통해 가져올 때 여기서 저장한 User 정보를 가져온다.

// 1. UserDetailDervice를 상속
public interface MemberService extends UserDetailsService {

}

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    // 2. loadUserByUsername(String username) : userDetails 
    // -- username은 user id(pk)를 말한다. 
    // -- User객체 안에 (id, password, Role)을 넣어 반환한다.
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        Optional<Member> optionalMember = memberRepository.findById(username);

        if (!optionalMember.isPresent()) {
            throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
        }

        Member member = optionalMember.get();

        if (!member.isEmailAuthYn()) {
            throw new MemberNotEmailAuthException("이메일 활성화 이후 로그인 해주세요.");
        }

        // Role
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

        grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));

        return new User(member.getUserId(), member.getPassword(), grantedAuthorities);
    }
}
더보기

- 이메일 활성화에 관해서는 사용자 지정 예외를 만들었었다.

package com.example.fastlms.member.exception;

public class MemberNotEmailAuthException extends RuntimeException {
    public MemberNotEmailAuthException(String error) {
        super(error);
    }
}

 

[2] BCrypt를 통해 비밀번호 암호화하기

- LDAP authentication 과정에서 BCripty를 사용하면 비밀번호를 암호화할 수 있다.

- 이 과정을 하는 이유는, 본래 HTTP가 Text 기반으로 데이터를 주고 받기 때문에 해킹에 취약하기 때문이다.

  (우리는 HashMap을 배울 때, 해싱 기법을 배웠었다. 여기서도 해싱을 사용해서 암호화를 한다.)

- 만약, BCrypt로 데이터를 인코딩 해서 변환하였다면, 그걸 조회할 때도 마찬가지로 동일한 변환과정을 통해 조회한다.

- 따라서, BCrypt를 사용할 때는, Spring Security 설정 클래스와 더불어, 회원 서비스 객체의 register() 메소드 안에도 동일하게 비밀번호를 BCrypt로 인코딩해주는 것이 필요하다. 

  * 등록 시에 암호화를 걸지 않으면, BCrypt로 비밀번호를 가져올 때에 동일한 비밀번호로 인식하지 못한다.

 

[2-1] 회원 가입 메소드에서 비밀번호를 BCriyt로 암호화한다.

String encPassword = BCrypt.hashpw(request.getPassword(), BCrypt.gensalt());
더보기
@Override
public boolean register(MemberRegister.Request request) {

    Optional<Member> optionalMember = memberRepository.findById(request.getUserId());

    if (optionalMember.isPresent()) {
        return false;
    }

    // 추가한 encPassword
    String encPassword = BCrypt.hashpw(request.getPassword(), BCrypt.gensalt());

    String uuid = UUID.randomUUID().toString().replace("-","");

    memberRepository.save(
            Member.builder()
                    .userId(request.getUserId())
                    .password(request.getPassword())
                    .userName(request.getUserName())
                    .phoneNumber(request.getPhoneNumber())
                    .emailAuthYn(false)
                    .emailAuthKey(uuid)
                    .build()
    );

    // 인증메일
    String email = request.getUserId();
    String subject = "fastlms 사이트 가입을 축하드립니다.";
    String text = "<div><p>fastlms 사이트 가입을 축하드립니다.</p>" +
            "<p>아래 링크를 클릭하시어, 가입을 완료하세요.</p>" +
            "<a href = 'http://localhost:8080/member/email-auth?id="
            + uuid +"'>링크</a></div>";
    mailComponents.sendMail(email, subject, text);

    return true;
}

 

[2-2] Spring Security 설정 클래스에서 회원서비스를 주입하고, 팩토리 메소드로 BCripyPasswordEncoder를 @Bean으로 등록한다. 

- 등록 후에는 configure 메소드를 override 하면 된다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final MemberService memberService;

    @Bean
    UserAuthenticationFailureHandler getFailureHandler() {
        return new UserAuthenticationFailureHandler();
    }

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * LDAP authentication
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(memberService)
                .passwordEncoder(getPasswordEncoder());

        super.configure(auth);
    }


}

 

|  정리

- 스프링 시큐러티란, 애플리케이션 보안을 담당하는 하위 프레임워크로, Filter를 통해 인증 및 권한을 처리하는 프레임워크다.

- 스프링 시큐러티는 HttpSecurity, WebSecurity, LDAP authentication, JDBC Authentication.. 등등 인증에 관한 다양한 하위 기능들을 가지고 있다.

- 사용자 정의 보안 기능을 구현하기 위해서는, 기본적으로
  (1) HttpSecurity를 통해 사용자 요청 URL에 대해 로그인을 전제할 것인지를 설정하고,
  (2) LDAP authentication을 통해 로그인 시 username과 password가 valid한 것인지를 확인하는게 필요하다.

- 절차
  (1) Spring Security 설정 클래스에 @Configuration과 @EnableWebSecurity를 달아준다.
  (2) configure(HttpSecurity http) 메소드에서 URL 접속권한/사용자 지정 로그인 및 로그인 실패 핸들러를 추가한다.
  (3) configure(AuthenticationManagerBuilder auth) 메소드를 통해 user 정보를 가져올 서비스 객체를 호출하고,  BCrypt 인코더를 통해서 비밀번호 조회 시 암호화된 상태의 비밀번호를 조회할 수 있도록 한다.

 

 

[ 출처 및 참조 ]

부트캠프 수업 내용을 들은 후 정리한 내용

spring security 사용기 https://yeonyeon.tistory.com/185
change default username and password https://www.yawintutor.com/spring-boot-security-step-by-step-2/

LDAP란? https://yongho1037.tistory.com/796

LDAP와 LADP authentication https://jumpcloud.com/blog/what-is-ldap-authentication

 

 

 

'Framework > 프로젝트로 스프링 이해하기' 카테고리의 다른 글

[LMS 만들기] 관리자 로그인 구현  (2) 2022.10.04
[LMS 만들기] 비밀번호 초기화 요청 및 메일 링크를 통한 초기화  (1) 2022.10.04
[LMS 만들기] 회원가입 페이지 만들기  (0) 2022.10.02
[LMS 만들기] 스프링 컨트롤과 주소 매핑  (0) 2022.09.30
[LSM 만들기] Maven 프로젝트 환경 보기  (0) 2022.09.29
    'Framework/프로젝트로 스프링 이해하기' 카테고리의 다른 글
    • [LMS 만들기] 관리자 로그인 구현
    • [LMS 만들기] 비밀번호 초기화 요청 및 메일 링크를 통한 초기화
    • [LMS 만들기] 회원가입 페이지 만들기
    • [LMS 만들기] 스프링 컨트롤과 주소 매핑
    simDev1234
    simDev1234
    TIL용 블로그. * 저작권 이슈가 있는 부분이 있다면 댓글 부탁드립니다.

    티스토리툴바