묠니르묘묘
꾸준히 성장하는 개발자스토리
묠니르묘묘
전체 방문자
오늘
어제
  • 분류 전체보기 (188)
    • 프로그래밍 (48)
      • 디자인패턴 (4)
      • 예외,에러 (4)
      • Java (29)
      • Kotlin (3)
      • React.js (4)
      • JavaScript (2)
      • Apache Kafka (2)
    • Spring (49)
      • Spring (21)
      • Spring Cloud (3)
      • JPA (25)
    • 코딩테스트 (31)
      • 알고리즘 (5)
      • Java - 백준 (26)
      • Java - 프로그래머스 (0)
    • AWS (7)
    • 데이터베이스 (6)
    • 개발 etc (23)
    • 도서 (5)
    • 회고록 (4)
    • 데브코스-데이터엔지니어링 (15)

인기 글

최근 글

hELLO · Designed By 정상우.
묠니르묘묘

꾸준히 성장하는 개발자스토리

스프링부트 시큐리티 - 일반 로그인과 회원가입
Spring/Spring

스프링부트 시큐리티 - 일반 로그인과 회원가입

2022. 3. 17. 10:54

  • Spring Boot DevTools
  • Lombok
  • Spring Web
  • Spring Data JPA
  • H2 Database
  • OAuth2 Client
  • Thymeleaf

위 라이브러리들을 추가하여 스프링 시큐리티 예제를 만들어보자.

build.gradle

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

아 그리고 제일 중요한 Security를 추가하지 않았는데 이 때는 build.gradle에 위 코드를 추가하여 시큐리티를 설치하자.

 

스프링 시큐리티가 제공하는 로그인 페이지

설치가 끝나고 스프링을 시작하여 http://localhost:8080으로 바로 들어가면 스프링 시큐리티가 제공하는 로그인페이지가 나온다.

 

프로젝트 구조

이렇게 패키지를 만들어두고 이 구조를 기준으로 설명하겠다.

먼저 domain 패키지에 User 엔티티를 생성해보자.

 

 

User 클래스

/domain/User

@Entity
@Data
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;
    private String email;
    private String role; // ROLE_USER, ROLE_ADMIN

    private String provider;
    private String providerId;
    @CreationTimestamp // INSERT 쿼리 시 현재 시간으로 생성
    private Timestamp createDate;

    @Builder
    public User(String username, String password, String email, String role, String provider, String providerId, Timestamp createDate) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
        this.provider = provider;
        this.providerId = providerId;
        this.createDate = createDate;
    }
}

User 클래스를 간단히 설명하자면 빠르게 사용하려고 롬복의 @Data를 추가하여 Setter,Getter,ToString 등을 사용할 수 있다.

role 에는 역할을 지정해둘 컬럼을 만든 것이고, @CreationTimestamp는 주석처럼 INSERT 시 현재 시간으로 생성된다.

Builder패턴 생성자를 사용했지만 그냥 생성자를 사용해도 무방하다.

 

 

UserRepository 인터페이스

/repository/UserRepository

public interface UserRepository extends JpaRepository<User, Integer> {
    public User findByUsername(String username);
}

스프링 데이터 JPA를 사용하여 손쉽게 CRUD 메서드를 사용할 수 있다.

Interface로 생성 후 JpaRepository<클래스, PK타입> 을 상속받으면 된다.

그 후 데이터 JPA 문법에 따라 findBy 를 적고 Username을 적었는데 이것은 "select * from user where username = 값" 이 된다.

 

 

 

SecurityConfig 클래스

/config/SecurityConfig

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록 된다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); //csrf 비활성화
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있는 주소
                .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/loginForm")
                .loginProcessingUrl("/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해준다.
                .defaultSuccessUrl("/");
    }
}

WebSecurityConfigureAdapter를 상속받아서 시큐리티를 커스터마이징하면 된다.

현재는 BCryptPasswordEncoder로 BCrypt 암호화를 사용하게된다. 추후 암호화를 커스터마이징할 때 변경하게 된다.

configure 메서드안에 antMatchers로 경로를 적고, access로 권한을 설정하게 된다.

formLogin() 메서드만 쓰면 스프링 시큐리티가 제공하는 로그인 페이지만 사용하는 것이고,

그 후 loginPage메서드로 경로를 설정하면 내가 만든 커스텀 페이지로 로그인 페이지를 변경하게 된다.

loginProcessingUrl은 로그인페이지에서 "/login" 경로로 post신호를 보내면 시큐리티쪽으로 username과 password를 가져온다.

defaultSuccessUrl은 성공 시 이동할 Url이다.

여기서 따로 로그아웃은 설정하지 않았으므로 기본값인 "/logout"이 로그아웃을 하는 경로이다.

 

 

 

IndexController 클래스

/controller/IndexController

@Controller
@RequiredArgsConstructor
public class IndexController {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @GetMapping({"", "/"})
    public String index() {
        return "index"; // src/main/resources/templates/index.html
    }

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

    @GetMapping("/manager")
    public @ResponseBody String manager() {
        return "manager";
    }

    @GetMapping("/loginForm")
    public String loginForm() {
        return "loginForm";
    }

    @GetMapping("/joinForm")
    public String joinForm() {
        return "joinForm";
    }

    @PostMapping("/join")
    public String join(User user) {
        System.out.println(user);
        user.setRole("ROLE_USER");
        String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword);
        user.setPassword(encPassword);
        userRepository.save(user);
        return "redirect:/loginForm";
    }
}

컨트롤러에서 권한마다 들어갈 수 있는 경로들을 설정했다.

admin은 전부다, manager는 manager까지, user는 admin과 manager를 들어갈 수 없다.

왜냐하면 SecurityConfig에서 경로를 들어갈 수 있는 권한들을 설정했기 때문이다.

UserRepository와 BCryptPasswordEncoder를 생성자 주입하여 사용했다.

 

 

 

이제 페이지를 설정해보자.

페이지는 전부 resources/templates 폴더 안에다가 생성한다.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>인덱스페이지</title>
</head>
<body>
<h1>인덱스페이지입니다.</h1>
</body>
</html>

joinForm.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr>
<form action="/join" method="POST">
    <input type="text" name="username" placeholder="Username"><br>
    <input type="password" name="password" placeholder="Password"><br>
    <input type="email" name="email" placeholder="email"><br>
    <button>회원가입</button>
</form>
</body>
</html>

loginForm.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr>
<form action="/login" method="POST">
    <input type="text" name="username" placeholder="Username"><br>
    <input type="password" name="password" placeholder="Password"><br>
    <button>로그인</button>
</form>
<a href="/oauth2/authorization/google">구글 로그인</a>
<a href="/oauth2/authorization/facebook">페이스북 로그인</a>
<a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>

 

application.yml

설정 파일도 만들어준다.

server:
  port: 8080
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true

spring:
  datasource:
    driver-class-name: org.h2.Driver # 해당 DB 드라이버
    url: jdbc:h2:tcp://localhost/~/h2데이터베이스 경로
    username: DB 아이디
    password: DB 비밀번호

  jpa:
    hibernate:
      ddl-auto: create 
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true

이 때 h2 Database를 사용해서 저렇게 적는데 오라클이나 MySQL은 따로 드라이버를 변경해서 설정해주면 된다.

 

 

 

 

프로젝트 구조

 

 

 

 

localhost:8080

이 상태로 스프링을 시작하여 localhost8080 으로 들어가면 인덱스 페이지가 나온다.

 

localhost:8080/user

localhost:8080/user 를 들어가려고하니 로그인을 하라고 http://localhost:8080/loginForm 경로로 자동으로 들어가진다.

회원가입을 하지 않았으므로 회원가입을 한다.

 

http://localhost:8080/joinForm

회원가입을 클릭하면 위 경로로 들어가서 회원가입을 하게 된다.

그 후 로그인페이지로 가서 로그인을 하려고하는데 여기서 넘어가지 않는다.

로그인 부분을 살펴보자.

 

시큐리티 로그인

UserDetails는 시큐리티에서 사용자 정보를 담는 인터페이스이다.

로그인 진행이 완료되면 시큐리티 세션을 만들어주는데 이 안에는 Authentication 타입만 들어올 수 있다.

Authentication 안에는 User 정보가 있어야하는데 이 때 UserDetails와 OAuth2User 타입이 들어올 수 있다.

이 때 일반 로그인과 OAuth 로그인(SNS 소셜로그인)으로 나뉘는데 이러면 각각 구현해야하므로 이 두 타입을 구현한 클래스를 생성한다.

 

 

PrincipalDetails 클래스

/config/auth/PrincipalDetails

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private User user;
    private Map<String, Object> attributes;

    // 일반 로그인 생성자
    public PrincipalDetails(User user) {
        this.user = user;
    }

    // OAuth 로그인 생성자
    public PrincipalDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { // User 권한을 리턴하는 메서드
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collect;
    }

    @Override
    public String getPassword() { // User 비밀번호 리턴
        return user.getPassword();
    }

    @Override
    public String getUsername() { // User PK 또는 고유한 값을 리턴
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() { // User 계정 만료 여부 리턴
        return true; // true : 만료 안됨
    }

    @Override
    public boolean isAccountNonLocked() { // User 계정 잠김 여부 리턴
        return true; // treu : 잠기지 않음
    }

    @Override
    public boolean isCredentialsNonExpired() { // User 비밀번호 만료 여부 리턴
        return true; // true : 만료 안됨
    }

    @Override
    public boolean isEnabled() { // User 계정 활성화 여부 리턴
        return true; // true : 활성화 됨
    }

    // === OAuth2User 메서드 === //
    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public String getName() {
        return null;
    }
}

OAuth2User 메서드에서 attributes가 Map<String, Object> 타입인데 이것은 구글, 네이버처럼 소셜 로그인을 시도하면 정보가

{ "id" : "1234",

  "email" : "user@naver.com"}

이런 식으로 오기 때문이다.

이 클래스는 시큐리티가 사용자의 정보를 담는 클래스라고 보면 된다.

 

 

 

PrincipalDetailsService 클래스

/config/auth/PrincipalDetailsService

@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if(userEntity != null) {
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}

시큐리티에서 유저의 정보를 가져오는 클래스이다.

SecurityConfig에서 loginProcessingUrl("/login")을 설정했는데 이 /login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC가 되어있는 loadUserByUsername 함수가 실행된다. 

loadUserByUsername 메서드로 로그인한 유저가 DB에 있는지 찾는 것이다.

찾으면 UserDetails와 OAuth2User를 구현한 PrincipalDetails를 반환하여 SecurityContextHolder에 저장할 수 있게 한다.

즉, 시큐리티 세션(내부 Authentication(내부 PrincipalDetails)) 인 것이다.

메서드가 종료되면 @AuthenticationPrincipal 어노테이션이 만들어진다.

 

 

 

IndexController 코드 추가

@GetMapping("/user")
    public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
        System.out.println("principalDetails : " + principalDetails.getUser());
        return "user";
    }

 

지금까지 만든 것으로 스프링을 시작하여 일반 회원가입을 하면 인덱스페이지로 들어가지고

localhost:8080/user 까지는 들어가지지만 manager와 admin은 들어갈 수 없을 것이다.

일단 회원가입으로 admin과 manager를 만들어주고 DB에서 값을 변경하자.

update user set role = 'ROLE_ADMIN' where username = 'admin';
update user set role = 'ROLE_MANAGER' where username = 'manager';

이렇게 role의 값을 변경해준다음에 각각 로그인하여 localhost:8080/admin과 /manager로 들어가보자.

로그아웃은 localhost:8080/logout 이다.

 

지금까지 일반로그인과 회원가입을 해보았다. 다음에는 소셜 로그인을 구현해보자.

저작자표시 비영리 (새창열림)

'Spring > Spring' 카테고리의 다른 글

[Lombok] @NoArgsConstructor, @ToString  (0) 2022.04.11
스프링부트 시큐리티 - SNS 로그인과 SNS 회원가입  (0) 2022.03.17
IntelliJ 파일 업로드 주의사항  (2) 2022.03.16
Entity 보다는 DTO로 반환하자.  (0) 2022.03.16
스프링부트 View 환경설정  (0) 2022.01.24
    'Spring/Spring' 카테고리의 다른 글
    • [Lombok] @NoArgsConstructor, @ToString
    • 스프링부트 시큐리티 - SNS 로그인과 SNS 회원가입
    • IntelliJ 파일 업로드 주의사항
    • Entity 보다는 DTO로 반환하자.
    묠니르묘묘
    묠니르묘묘

    티스토리툴바