포스트

240223 기록(3)

UserService

Controller
  • 파일명 UserController
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
import com.example.userservice.dto.JoinDto;
import com.example.userservice.dto.ResponseUserDto;
import com.example.userservice.dto.loginDto;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;


@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {

  private final UserService userService;
  private ModelAndView mav;

  // 로그인 페이지
  @GetMapping("/login")
  public ModelAndView loginP(){
    mav = new ModelAndView("login");
    return mav;
  }

  @PostMapping("/loginProc")
  public ModelAndView loginProccess(LoginDto loginDto){
    ResponseUserDto responseUserDto = userService.loginProccess(loginDto);

    mav = new ModelAndView("userMain");
    return mav;
  }

  // 회원가입 페이지
  @GetMapping("/join")
  public ModelAndView joinP(){
    mav = new ModelAndView("join");
    return mav;
  }

  // 회원가입
  @PostMapping("/joinProc")
  public ModelAndView joinProccess(JoinDto joinDto){

    userService.joinProccess(joinDto);

    mav = new ModelAndView("login");
    return mav;
  }
}
  • SpringSecurity 공부를 위해 적용했다.
  • SpringSecurity를 적용하니 포스트맨으로는 유저서비스로 접근이 어려웠다.
  • SpringSecurity 적용 때문에 페이지 이동을 해야했는데, 나의 경우에는 RestController를 적용하다 보니 메소드에서 반환타입을 String으로 하면
  • 페이지 이동이 아닌 String 값 자체가 리턴되었다.
  • 이를 해결하기 위해 ModelAndView를 사용하였다.
  • 그 후 html파일을 resources의 static 폴더 밑에 생성하였는데 페이지 이동 맵핑이 되지 않았다.
  • 따로 맵핑하는 방법이 있나 찾아봤지만 기본적으로 알아서 맵핑이 되는 듯 했다.
  • html로 해결방안을 찾지 못하여 mustache를 dependency하고 mustache를 이용하였다.
  • resource의 templates 폴더 밑에 파일을 생성하였고, 맵핑이 되며 페이지 이동이 되었다.
SecurityConfig
  • 파일명 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
import com.example.userservice.service.MyUserDetailsService;
import lombok.AllArgsConstructor;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {

  private final MyUserDetailsService myUserDetailsService;

  // 스프링 시큐리티에서 제공하는 인증, 인가를 위한 필터 모음
  // Application Context 초기화가 이루어 지면서 HttpSecurity 객체가 설정한 filterChain 형성

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http
              .authorizeHttpRequests(
                      (authorizeRequest) -> authorizeRequest
                              .requestMatchers("/", "/api/users/login", "/api/users/loginProc", "/api/users/join" , "/api/users/joinProc").permitAll()
                              .requestMatchers("/api/boards").hasRole("USER")
                              .anyRequest().authenticated()
              );

      http
              .csrf(AbstractHttpConfigurer::disable);
              // == .csrf((auth) -> auth.disable());
              // CSRF(Cross-Site-Request Forgery 보호를 비활성화하는 메서드 호출
              // AbstractHttpConfigurer::disable -> AbstractHttpConfigurer에 정의된 disable 메소드에 대한 참조

      http
              .formLogin((auth) -> auth.loginPage("/api/users/login")
                      .loginProcessingUrl("/loginProc")
                      .permitAll());

      return http.build();
  }

  @Bean
  public BCryptPasswordEncoder bCryptPasswordEncoder(){
    return new BCryptPasswordEncoder();
  }
}
  • 스프링 시큐리티 설정을 하는 클래스이다.
  • .requestMatchers()에 url을 넣고, permitAll()를 통해 해당 url의 접근은 모두 허용으로 바꾸었다.
  • 또는 hasRole(“USER”)를 통해 역할이 있는 사람만 접근하도록 하였다.
  • .anyRequest()는 지정한 url 외의 나머지 url 접근을 나타내며 authenticated()를 통해 로그인 즉 인증이 된 사람만 접근이 가능하도록 하였다.
  • 연습삼아 해본 것이지만, 나의 경우 MSA이다 UserService에서의 시큐리티 적용이 Board까지 영향을 주지 못한다. 왜냐하면 Board 서비스도 결국은 Gateway에서 접근을 하기 때문에 UserService에서의 시큐리티가 영향을 주지 못하기 때문이라고 생각된다.(확실하지는 않다)
  • 시큐리티에 대해 익숙해지고 알게되면 Gateway에 적용을 해봐야하지 않을까 싶다.
  • csrf는 사이트 위변조 방지에 대한 보안처리 인데, 기본값은 enable이다.
  • 이것은 form 로그인 시 csrf 토큰을 같이 보내주어 처리를 해줘야 한다.
  • 개발단계에서는 이 부분을 설정하지 않으면 올바른 값으로 로그인을 진행하여도 로그인이 되지 않기 때문에 disable로 바꾸었다.
  • formLogin()은 form 양식을 통해 username, password를 입력받아 로그인한다. 둘의 name값은 스프링 시큐리티에서 캐치를 해야하기 때문에 그대로 사용해야 한다
  • 나중에 jwt를 공부하면 토큰방식으로 session을 STATELESS서버로 관리하게 되면 csrf는 disable로 유지하여도 된다.
  • 왜냐하면 csrf 토큰이 아니라 jwt(json web token)을 사용하기 때문이다.
dto
  • 파일명 JoinDto
1
2
3
4
5
6
7
8
9
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class JoinDto {
  private String email;
  private String password;
}
  • 회원 가입을 위해 필요한 데이터를 모아놓은 dto
  • 개념 공부라서 간단히 처리하였다.

  • 파일명 LoginDto
1
2
3
4
5
6
7
8
9
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginDto {
  private String username;
  private String password;
}
  • 로그인을 위한 필요 데이터를 모아놓은 dto
  • 중요한 것은 front 페이지의 name값도 username, password로 해야한다
  • 이유는 스프링 시큐리티에서 두 개의 키값으로 인증 및 인가 처리를 하기 때문이다
  • 처음에 username을 email로 바꿨다가 시큐리티에 계속 인증처리에 대한 결과가 null로 나왔다
  • 생각해보니 username이 진짜 username이라기보다는 시큐리티에서 인증 아이디로 사용되기 때문에 name값을 일치시켜줘야 한다

  • 파일명 MyUserDetails
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
import com.example.userservice.model.User;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

// Spring Security에서 유저의 정보를 담는 객체
@Getter
@Setter
public class MyUserDetails implements UserDetails {

    private User user;

    public MyUserDetails(User user){
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //return List.of(new SimpleGrantedAuthority("user"));
        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });

        return collection;
    }
    // 스프링 시큐리티에서 권한을 나타내는 객체를 생성하는 코드
    // List.of : 자바 9에서 추가된 정적 팩토리 메소드
    // "user"라는 권한을 가진 사용자를 나타내는 객체를 생성하고 List에 담아 반환

    @Override
    public String getPassword() {
        return user.getPassword();
    }
    // 사용자의 패스워드 반환
    // 반드시 암호화해서 저장한다.

    @Override
    public String getUsername() {
        return user.getEmail();
    }
    // 사용자를 식별할 수 있는 것. 사용되는 값은 반드시 고유해야 한다

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 계정이 만료되었는지 확인. true면 만료되지 않음

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 계정이 잠금되었는지 확인. true면 잠금되지 않음

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 패스워드가 만료되었는지 확인. true면 만료되지 않음

    @Override
    public boolean isEnabled() {
        return true;
    }
    // 계정이 사용 가능한지 확인. true면 사용 가능
}
  • MyUserDetails은 UserDetails 인터페이스를 구현해야한다
  • UserDetails는 시큐리티에서 사용하기 위한 인터페이스이다.
  • 따라서 시큐리티에서 username과 password를 가지고 DB에 가서 유저에 해당하는 정보들을 불러오고 불러온 정보를 바탕으로 UserDetails를 상속받은 클래스에서 구현해줘야 시큐리티에서 사용할 수 있다.

  • 파일명 ResponseUserDto
1
2
3
4
5
6
7
8
9
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ResponseUserDto {
  private String email;
  private String role;
}
  • 회원 조회 시 반환되는 데이터에 대한 정보를 담고 있는 dto
model
  • 파일명 User
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
import com.example.userservice.dto.JoinDto;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class User {
  @Id
  private String email;

  @Column(nullable = false)
  private String password;

  @Column
  private String role;

  public User(JoinDto joinDto){
    this.email = joinDto.getEmail();
    this.password = joinDto.getPassword();
    this.role = "ROLE_USER";
  }
}
  • user정보를 담고 있는 entity
  • 비밀번호의 경우는 서비스에서 암호화하여 넘겨줘야 한다
repository
  • 파일명 UserRepository
1
2
3
4
5
6
7
8
9
10
import com.example.userservice.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, String> {
  boolean existsByEmail(String email);

  User findByEmail(String email);
}
  • jpa를 사용하려고 하기 때문에 인터페이스로 만들어준다.
  • JpaRepository<Entity명, Id의 타입>를 상속받아야 한다
service
  • 파일명 MyUserDetailsService
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
import com.example.userservice.dto.MyUserDetails;
import com.example.userservice.model.User;
import com.example.userservice.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.security.core.userdetails.UserDetailsService;

@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService{

  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user =  userRepository.findByEmail(username);

    if(user == null){
      return null;
    }

    return new MyUserDetails(user);
  }
}
  • 시큐리티에서 인증 및 인가 확인을 위해 사용하는 서비스
  • UserDetailsService 인터페이스를 상속받아야 한다.(인터페이스이기 때문에 상속은 아니지만 그냥 상속이라고 부르겠다)
  • 그리고 시큐리티에서 인증 및 인가 확인을 위해 사용할 UserDetails를 구현한 MyUserDetails를 반환하고 있다
  • DB에서 아이디(username)를 바탕으로 유저 정보를 받아와서 반환한다.. 여기서 의문은 그렇다면 비밀번호는 언제? 누가? 검증과정을 하느냐 였다.

  • 간단히 설명하면
  • 시큐리티는 username과 password를 받으면 ‘미인증’ 객체인 UsernamePasswordAuthenticationToken 을 생성하고, (Authentication 구현체)
  • UsernamePasswordAuthenticationToken을 ProviderManager(AuthenticationManager 구현체)에게 인증 처리를 위임한다
    • 시큐리티의 authFilter -> ProviderManager(+UsernamePasswordAuthenticationToken)
  • ProviderManager는 UsernamePasswordAuthenticationToken을 처리할 수있는 provider를 찾아 인증 처리를 위임한다
    • authFilter -> ProviderManager -> provider
  • provider는 DaoAuthenticationProvider 로 AbstractUserDetailsAuthenticationProvider 추상 클래스를 상속받아 authenticate() 템플릿 메서드에서 사용하는 인증 메서드를 구현한 클래스이다
    • authenticate() 내부에서는 조회된 UserDetails 정보와 UsernamePasswordAuthenticationToken을 이용하여 비밀번호 검증을 수행
    • 여기서는 기본적으로 PasswordEncoder타입의 구현 객체가 스프링 컨테이너에 등록되어 있다면 객체를 주입 받아 검증 처리한다
  • 정리하면
  • 언제? : 시큐리티 필터(=UsernamePasswordAuthenticationFilter)가 실행되는 시점
  • 어디서? : provider 객체에 정의된 PasswordEncoder 구현 객체에 의해 비밀번호 검증이 이루어 진다는 것!

  • 파일명 UserService
1
2
3
4
5
6
7
8
9
import com.example.userservice.dto.JoinDto;
import com.example.userservice.dto.ResponseUserDto;
import com.example.userservice.dto.LoginDto;

public interface UserService {
  void joinProccess(JoinDto joinDto);

  ResponseUserDto loginProccess(LoginDto loginDto);
}

  • 파일명 UserServiceImpl
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
import com.example.userservice.dto.JoinDto;
import com.example.userservice.dto.ResponseUserDto;
import com.example.userservice.dto.LoginDto;
import com.example.userservice.model.User;
import com.example.userservice.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

  private final UserRepository userRepository;
  private final BCryptPasswordEncoder bCryptPasswordEncoder;

  @Override
  public void joinProccess(JoinDto joinDto) {
    boolean check = userRepository.existsByEmail(joinDto.getEmail());
    if(check) return;

    joinDto.setPassword(bCryptPasswordEncoder.encode(joinDto.getPassword()));
    User user = new User(joinDto);

    userRepository.save(user);
  }

  @Override
  public ResponseUserDto loginProccess(LoginDto loginDto) {
    User user = userRepository.findById(loginDto.getUsername()).orElseThrow(
            () -> new IllegalArgumentException("user not found")
    );

    String providerPassword = loginDto.getPassword();
    if(bCryptPasswordEncoder.matches(providerPassword, user.getPassword())){
      try{
        ResponseUserDto responseUserDto = new ResponseUserDto();
        responseUserDto.setEmail(user.getEmail());
        responseUserDto.setRole(user.getRole());
        return responseUserDto;
      } catch (Exception e){
        e.printStackTrace();
      }
    }

    return null;
  }
}

Front

  • 파일명 join.mustache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/api/users/joinProc" method="POST" name="joinForm">
    <input type="text" name="email" placeholder="Email"/>
    <input type="password" name="password" placeholder="Password"/>
    <input type="submit" value="Join"/>
</form>
</body>
</html>
  • 파일명 login.mustache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/api/users/loginProc" method="POST" name="loginForm">
    <input type="text" name="username" placeholder="Email"/>
    <input type="password" name="password" placeholder="Password"/>
    <input type="submit" value="Login"/>
</form>
</body>
</html>
  • 파일명 userMain.mustache
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
userLoginMain
</body>
</html>
깃 주소

GitHub