티스토리 뷰

Spring Security를 사용하여 로그인과 회원가입 기능을 구현하는 방법을 알아보겠습니다.

 

간단하게 Spring Security를 적용한 프로젝트는 다음 깃 레포지토리를 통해 확인할 수 있습니다. 해당 프로젝트는 현재 설명하는 내용과 예제가 조금 다르지만 스프링 시큐리티에 대한 내용을 담고 있으니 참고하시기 바랍니다.

 

https://github.com/hanbee1005/lec-spring-security

 

hanbee1005/lec-spring-security

"스프링 시큐리티" 인프런 강좌 따라하기. Contribute to hanbee1005/lec-spring-security development by creating an account on GitHub.

github.com

 

  • Java 11
  • SpringBoot 2.3.0
  • Gradle 6.5

 

예제를 통해 살펴보겠습니다. 예제에서는 Spring MVC를 사용하여 컨트롤러, 서비스, 레포지토리를 만들 것이고 데이터베이스 접근은 JPA 방식을 사용하겠습니다. Spring MVC나 JPA에 대해서는 특별히 언급하지 않겠습니다... (다른 포스팅에서 다뤄볼게요...ㅎㅎ)

 

먼저 컨트롤러를 통해 아래와 같은 요청을 받을 수 있다고 가정해보겠습니다.

 

  • 로그인: /auth/login
  • 회원가입: /auth/signup
  • 대시보드: /dashboard (로그인한 사용자만 접근이 가능)
  • 관리자: /admin (로그인한 사용자 중 ADMIN 권한을 가진 사용자만 접근이 가능)

 

위와 같은 요청을 받을 수 있는 AuthController, HomeController를 생성합니다. 각각의 요청은 별도의 화면 없이 바로 String 데이터를 반환하는 방식으로 구현하였습니다.

 

package com.example.securityspring.controller;

import com.example.securityspring.dto.JwtRequestDto;
import com.example.securityspring.dto.MemberSignupRequestDto;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping(value = "login", produces = MediaType.APPLICATION_JSON_VALUE)
    public String login(@RequestBody JwtRequestDto request) {
        return "login";
    }

    @PostMapping(value = "signup", produces = MediaType.APPLICATION_JSON_VALUE)
    public String signup(@RequestBody MemberSignupRequestDto request) {
        return "signup";
    }
}

 

package com.example.securityspring.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {

    @GetMapping("dashboard")
    public String dashboard() {
        return "dashboard";
    }

    @GetMapping("admin")
    public String admin() {
        return "admin";
    }
}

 

로그인 시 입력되는 데이터는 이메일과 패스워드로 추후 JWT 방식을 적용할 것이기 때문에 이름이 JwtRequestDto인 객체를 생성하여 받도록 하겠습니다. 회원가입 시 입력되는 데이터는 이메일, 패스워드, 이름 정도의 데이터로 하겠습니다. 이 데이터는 MemberSignupRequestDto 객체를 통해 받도록 하겠습니다.

 

package com.example.securityspring.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class JwtRequestDto {

    private String email;
    private String password;
}

 

package com.example.securityspring.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberSignupRequestDto {

    private String email;
    private String password;
    private String name;
}

 

❗️참고

@Getter, @Setter 애노테이션은 lombok 패키지를 추가하면 사용이 가능합니다. build.gradle 파일에 추가하면 됩니다.

 

implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

 

아직 Spring Security가 적용되어 있지 않기 때문에 모든 요청에 다 접근이 가능합니다.

 


 

이제 Spring Security를 적용해보도록 하겠습니다.

 

1. build.gradle 파일에 spring security에 대한 의존을 추가합니다.

 

implementation 'org.springframework.boot:spring-boot-starter-security'

 

이 부분만 추가해주더라도 spring security가 동작하면서 기존에 그냥 접근이 가능했던 모든 페이지에서 인증을 요청하게 됩니다. 기본적으로 Spring Security가 제공하는 계정이 있는데 아이디는 user, 비밀번호는 애플리케이션을 실행할 때 콘솔창에 출력됩니다.

 

2. WebSecurityConfigurerAdapter를 상속받는 설정 파일을 생성합니다.

 

package com.example.securityspring.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 정적인 파일에 대한 요청들
    private static final String[] AUTH_WHITELIST = {
            // -- swagger ui
            "/v2/api-docs",
            "/v3/api-docs/**",
            "/configuration/ui",
            "/swagger-resources/**",
            "/configuration/security",
            "/swagger-ui.html",
            "/webjars/**",
            "/file/**",
            "/image/**",
            "/swagger/**",
            "/swagger-ui/**",
            // other public endpoints of your API may be appended to this array
            "/h2/**"
    };

    @Bean
    public BCryptPasswordEncoder encodePassword() {  // 회원가입 시 비밀번호 암호화에 사용할 Encoder 빈 등록
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // login 없이 접근 허용 하는 url
                .antMatchers("/auth/**").permitAll()
                // '/admin'의 경우 ADMIN 권한이 있는 사용자만 접근이 가능
                .antMatchers("/admin").hasRole("ADMIN")
                // 그 외 모든 요청은 인증과정 필요
                .anyRequest().authenticated();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 정적인 파일 요청에 대해 무시
        web.ignoring().antMatchers(AUTH_WHITELIST);
    }
}

 

이 파일을 통해 어떤 url에 대해 요청을 허용할지, 권한 확인은 어떻게 할지 결정하게 됩니다. 기본적으로 제공되는 로그인, 로그아웃 페이지를 사용하도록 설정할 수도 있습니다. 추가적인 내용에 대해서는 공식 자료를 참고하시기 바랍니다...

 

3. 회원가입 기능을 구현합니다.

패스워드는 암호화를 한 다음 저장하도록 하겠습니다. 데이터베이스 관련 JPA 코드는 살펴보지 않겠습니다. 아무 관계형 데이터베이스나 다 사용이 가능합니다.

 

package com.example.securityspring.controller;

import com.example.securityspring.dto.JwtRequestDto;
import com.example.securityspring.dto.MemberSignupRequestDto;
import com.example.securityspring.service.AuthService;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth")
@AllArgsConstructor
public class AuthController {

    private final AuthService authService;

    ...

    @PostMapping(value = "signup", produces = MediaType.APPLICATION_JSON_VALUE)
    public String signup(@RequestBody MemberSignupRequestDto request) {
        return authService.signup(request);
    }
}

 

package com.example.securityspring.service;

import com.example.securityspring.domain.Member;
import com.example.securityspring.dto.MemberSignupRequestDto;
import com.example.securityspring.repository.MemberRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@AllArgsConstructor
public class AuthService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public String signup(MemberSignupRequestDto request) {
        boolean existMember = memberRepository.existsById(request.getEmail());

        // 이미 회원이 존재하는 경우
        if (existMember) return null;

        Member member = new Member(request);
        member.encryptPassword(passwordEncoder);

        memberRepository.save(member);
        return member.getEmail();
    }
}

 

package com.example.securityspring.domain;

import com.example.securityspring.dto.MemberSignupRequestDto;
import com.example.securityspring.model.Role;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;

@Getter
@Entity
@NoArgsConstructor
public class Member {

    @Id
    private String email;

    private String password;

    private String name;

    @Enumerated(EnumType.STRING)
    private Role role;

    public Member(MemberSignupRequestDto request) {
        email = request.getEmail();
        password = request.getPassword();
        name = request.getName();
        role = Role.USER; // 회원가입하는 사용자 권한 기본 USER (임시)
    }

    public void encryptPassword(PasswordEncoder passwordEncoder) {
        password = passwordEncoder.encode(password);
    }
}

 

public interface MemberRepository extends JpaRepository<Member, String> {
}

 

저는 테스트 코드를 작성하여 구현된 기능을 확인하였는데 테스트 코드를 작성하지 않는 경우에는 Postman 등을 통해 요청을 보냄으로써 확인이 가능합니다. 만약 Postman으로 테스트를 하는 경우에는 csrf 에러가 발생할 수 있으므로 security config 설정을 변경해주어야 합니다.

 

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().authorizeRequests()
        ...
}

 

이렇게 회원가입을 하면 비밀번호가 암호화된 상태로 데이터가 저장되는 것을 확인할 수 있습니다.

 

4. UserDetailsService 및 UserDetails 구현을 통해 로그인 처리를 하고 인증된 사용자를 어떤 형태로 반환할지 정하는 객체를 생성합니다.

 

package com.example.securityspring.security;

import com.example.securityspring.domain.Member;
import com.example.securityspring.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findById(username)
                .orElseThrow(()-> new UsernameNotFoundException("등록되지 않은 사용자 입니다"));
        
        return new UserDetailsImpl(member);
    }
}

 

package com.example.securityspring.security;

import com.example.securityspring.domain.Member;
import com.example.securityspring.model.Role;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {
    private static final String ROLE_PREFIX = "ROLE_";
    private final Member member;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Role role = member.getRole();
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(ROLE_PREFIX + role.toString());
        Collection<GrantedAuthority> authorities = new ArrayList<>(); //List인 이유 : 여러개의 권한을 가질 수 있다
        authorities.add(authority);

        return authorities;
    }

    @Override
    public String getPassword() {
        return member.getPassword();
    }

    @Override
    public String getUsername() {
        return member.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

5. 로그인 기능을 구현합니다.

 

package com.example.securityspring.controller;

import com.example.securityspring.dto.JwtRequestDto;
import com.example.securityspring.dto.MemberSignupRequestDto;
import com.example.securityspring.service.AuthService;
import lombok.AllArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth")
@AllArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping(value = "login", produces = MediaType.APPLICATION_JSON_VALUE)
    public String login(@RequestBody JwtRequestDto request) {
        try {
            return authService.login(request);
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    ...
}

 

package com.example.securityspring.service;

import com.example.securityspring.domain.Member;
import com.example.securityspring.dto.JwtRequestDto;
import com.example.securityspring.dto.MemberSignupRequestDto;
import com.example.securityspring.repository.MemberRepository;
import com.example.securityspring.security.UserDetailsImpl;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@AllArgsConstructor
public class AuthService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    private final AuthenticationManager authenticationManager;

    public String login(JwtRequestDto request) throws Exception {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        UserDetailsImpl principal = (UserDetailsImpl) authentication.getPrincipal();
        return principal.getUsername();
    }

    ...
}

 

package com.example.securityspring.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    ...
}

 

이렇게 하면 Spring Security를 통한 로그인, 회원가입 기능 구현이 완료되었습니다. 로그인을 하고 나면 /dashboard 에 접근할 수 있습니다. 기본적으로 회원가입되는 사용자는 USER 권한을 가지기 때문입니다.

 

위에서 설명한 내용은 아래 프로젝트를 통해 확인할 수 있습니다. Spring Security의 인증 과정은 많은 filter들로 이루어져 있는데 이 부분을 구체적으로 학습하면 더 도움이 될 것 같습니다 :) (인프런 백기선 강사님의 Spring Security 강의 추천!!)

 

https://github.com/hanbee1005/security-spring

 

hanbee1005/security-spring

Spring Security 로그인 구현 예제. Contribute to hanbee1005/security-spring development by creating an account on GitHub.

github.com

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함