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>