
User entity에 email,password, email 필트를 추가한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String name;
private String email;
private String password;
private String role;
@OneToMany(mappedBy = "user")
private List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "user")
private List<Reply> replies = new ArrayList<>();
public void update(String name) {
this.name = name;
}
}
마찬가지로 UserRequestDTO의 JoinDTO에 email, password를 추가하자
public class UserRequestDTO {
@Getter
public static class JoinDTO {
private String name;
private String email;
private String password;
}
@Getter
public static class UpdateUserDTO {
private String name;
}
}
그런 다음 spring security 의존성을 추가 하자
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
그 후 실행하여 기존 URL을 들어가 보면

이렇게 로그인 페이지가 나오는데 Username은 user, Password는 빌드할 때 나오는 비번을 쳐서 로그인 해주면

다음과 같이 정보가 나온다.

의존성만 추가했을 뿐인데 어떻게 로그인이 되는 것일까?
위와 같이 어떻게 로그인 페이지가 나오고 로그인이 가능한 지 흐름을 살펴보자
먼저 요청이 들어오면 DelegatingFilterProxy가 해당 요청을 가로채 FilterChainProxy에게 위임하게 된다. 그때 FilterChainProxy는 빈으로 등록된 여러 개의 Security FilterChain 중 하나를 골라서 위임하게 되고 security filterchain을 돌면 다시 돌아가서 다음 필터로 넘어가 반복하는 구조이다.

위와 같이 기본으로 filterChain에 등록되어있는 필터들을 확인할 수 있다.
필터들의 순서는 신경쓰지 않고 로그인이 어떻게 되는지 살펴보자
AuthorizationFilter는 기본적인 인가를 처리하는 필터인데, 필터 체인을 돌다가 여기로 들어오면 AuthorizationManager의 도움을 받아서 들어온 URL의 보안, 여부를 판단하게 된다.

현재는 모두 보안 url로 되어있는데 SpringBootWebSecurityConfiguration class의 defaultSecurityFilterChain이 있어서 설정을 하지 않아도 기본적으로 설정되는 필터 체인이 있는데 여기서 기본값이 모든 request에 대해 보안이 필요하다고 적혀있기 때문이다.


밑에는 리다이렉트 되는 흐름인데 FilterSecurityIntercepter는 지금의 Authorization 필터라고 보면 된다.

출처 : Spring Security Docs (https://docs.spring.io/spring-security/reference/5.7/servlet/authentication/passwords/form.html#servlet-authentication-usernamepasswordauthenticationfilter)
DefaultLoginFilter는 LoginPage를 만드는 역할을 한다. 아까 본 로그인 창을 만드는 필터인데, 위에서 본 흐름에 따라 보안 url일 때 GET/login으로 리다이렉트 되어 필터체인으로 다시 들어오게 되면 필터 체인을 따라가다가 필터를 만날 때 이 필터가 작동하게 된다. 필터 체인에 다른 로그인 폼을 설정하거나 form 로그인을 사용하지 않는다고 하면 이 filter는 해당되는 필터 체인에서 사라지게 된다. 밑의 dofilter의 if문을 보면 해당되는 url이 아닐 시 그냥 다음 필터로 넘기는 doFilter를 확인할 수 있다.


위에서 만들어준 로그인 페이지를 통해 폼 로그인 정보가 들어오면 UsernamePasswordToken을 만들어주는 필터이다. 만약 폼 로그인 방식이 아닌 Json방식이나 OAuth 방식이면 AbstractAuthenticationProcessingFilter에서 다른 filter를 만들어서 사용해야 한다.

폼 형식의 로그인 정보가 들어오면 UsernamePasswordToken 객체를 만들어서 Token에 아이디와 패스워드를 저장하고 AuthenticationManager에 보낸다.

위의 사진을 보면 UsernamePasswordAuthenticationFilter로 들어와서 request로 부터 username과 password를 빼내고 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager로 authenticate을 통해 만들어진 Token을 보내는 것을 확인할 수있다.
AuthenticationManager는 모든 AuthenticationProvider를 살펴보는 클래스이다.

위의 코드는 AuthenticatoinManager의 구현체인 ProviderManager의 authentication의 일부 코드인데, 반복문을 돌려서 모든 AuthenticationProvider를 통해 인증을 확인하는 것을 볼 수 있다.
여기서 AuthenticationManager의 authenticate은 Authentication을 받는데 우리는 UserpasswordToken을 보내지 않았나? 할 수있지남
UsernamePasswordAuthenticationToken은 AbstractAuthenticationToken을, 그리고 Authentication 클래스를 상속 받았기 때문에 가능하다고 볼 수 있다.
AuthenticationProvider는 각각 특정 유형의 인증을 수행하는데, 여러개를 구현할 수도 있다. DaoAuthenticationProvider는 AuthenticationProvider의 대표적인 구현체 중 하나이다.

위에 있는 것은 AbstractUserDetailsAuthenticationProvider의 authenticate인데, 여기서 세가지 메소드를 유심히 보자(retrieveUser, additionalAuthenticationChecks, createSuccessAuthentication)
각각 DaoAuthenticationProvider에서 구현된 메소드를 보면 retrieveUser 메소드는 등록된 UserDetailsService의 loadUserByUsername을 사용하여 UserDetails를 생성하는 역할을 맡고있다.

additionalAuthentication 메소드는 Authentication.getCredentials()와 UserDetails.getPassword()를 비교하는 로직이 들어가고

createSuccessAuthentication 메소드는 Authentication을 만들어서 리턴해주는 역할을 맡고 있다.

createSuccessAuthentication 에서 Authentication이 생성이 되고 리턴이 되면 AuthenticationManager로 가서 UsernamePasswordAuthenticationFilter로 가서 Authentication을 SecurityContextHolder에 저장한다. 그 후 securityFilterchain의 다름 filter로 가게 되고, 나중에 요청이 끝날 때 SecurityContext는 초기화 된다.
여기서 잠시 Security Context를 잠시 설명하자면 SecurityFilterChain들의 FIlter들이 일을 처리하기 위해서는 앞에서 처리한 정보가 필용할 때가 있는데, 정보를 받는 방법이 없다. 예를 들어 인가 처리를 위해서 AuthorizationFIlter까지 정보가 넘어가야 하는데 로그인 필터에서 저장한 정보를 가져올 방법이 없다. 그래서 이를 가져오기 위해서 Security Context가 나오게 되었다.
인증 후 Authentication을 저장하는데 저장 정보는 Security Context에 남기고 Security Context는 SecurityContextHolder에 0개 이상 존재할 수 있다.(사용자 수 만큼)

그리고 SecurityContextHolder는 static으로 설정하여 전역적으로 사용 가능하기 때문에 다른 곳에서도 관련 정보를 받을 수 있다.

UserDeatilsService는 user를 불러오고 그것을 이용하여 UserDetails를 만들어주는 역할을 하는데, UserDetailsManager는 UserDetailsService를 상속받은 것으로 User의 생성, 업데이트, 삭제 등이 가능하게 하는 클래스이다. 여기서 원하는 방식을 빈등록해서 사용할 수 있다.
InMemory에 저장하는 방식이다.

JDBC로 DB에 저장하는 방식이다. 권장되는 테이블만 적용이 가능하다.

우리는 인메모리에 사용하지 않을 것이고 원하는 테이블 모형을 사용할 것이기 때문에 직접 UserDetailsService를 구현하자
PrincipalDetailsService를 만들고 UserDetailsService를 구현시키고 loadUserByUsername을 오버라이드 하자.

그 후 직접 UserDetails를 만들어서 리턴 시켜주자.

UserDetails에서 구현시킨 User를 그냥 사용해도 되지만 UserDetails를 직접 만들어 보자.
밑에서 처럼 UserDetails를 구현시켜서 각 요소에 맞는 정보들을 리턴시켜주면 된다.

여기서 사용하는 getPassword는 아까 봤었던 AuthenticationProvider의 additionalAuthenticationChecks에서 비밀번호를 확인하는 데 사용된다.
하지만 이렇게만 하면 로그인시에 PasswordEncorder가 설정이 안되어있기 때문에 오류가 발생할 것이다.
PasswordEncoder에는 여러 종류가 존재한다. (주로 hash 알고리즘이 사용) 암호화 할 때나 로그인 할 때 맞는 password인지 확인 할 때 사용핟나. 대표적으로 6가지의 PasswordEndcoder가 존재한다.
안전한 PasswordEncoder들은 추가적으로 자원을 사용하기 때문에 해커가 무작정 대입으로 비밀번호를 찾기 어렵게 된다. 신 우리 서버에서도 자원이 사용되기 때문에 서버의 성능을 고려하는 것이 좋다.
여기서는 가장 많이 사용하는 BCryptPasswordEncoder를 사용하겠다.
먼저 SecurityConfig를 만들어주고 빈등록이 가능하게끔 @Configuration을 붙여주고 그 안에서 PasswordEncoder를 빈 등록 시켜주자

아까 봤던 SecurityFilterChainConfiguration에 있는 defaultSecurityFilterChain을 그냥 복사해서 써보자. defaultSecurityFilterChain함수를 SecurityConfig로 가져와서 빈 등록을 시켜주겠다.

빈으로 등록이 되면 하나의 필터 체인이 생긴 것입니다. 위 필터 체인에 있는 필터들을 수정할 수 있다. 예를 들어 밑과 같이 cors필터를 없애는 설정을 했다고 하면

원래 csrfFilter위에 있는 CorsFilter가 없는 모습을 볼 수 있다.

이번에는 허용 url을 설정해보겠다.
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); 부분이 모든 요청에 대해 인등을 하게끔 하겠다를 설정한 것이다.

위 사진처럼 바꾸면 /api/v1/replies/은 /api/v1/replies가 앞으로 붙어있는 모든 주소에 대해 허용시키겠다는 거다.
그리고 /api/v1/users/은 /api/v1/users가 앞으로 붙어있는 모든 주소에 대해 admin 역할을 가진 user에게 허용하겠다는 뜻이다.
anyRequest.authenticated는 나머지 모든 request에 대해 인증을 받겠다는 것이다.
실행 후

로그인 정보 없이 정보를 받아올 수 있는 것을 볼 수 있다.
이제 회원가입을 짜보자
먼저 아까 빈 등록 시켰던 passwordEncoder를 UserService에 DI 시키자

서비스에서 비밀번호를 encode 시키기도 하고 converter에서 하기도 하는데 여기서는 converter안에서 encode를 시켜보겠다.


로그인 하기 전에 회원가입을 해야하기 때문에 회원가입 url을 풀자

그리고 테스트 하기 전 swagger url도 풀자
실행을 하고 테스트를 해보면 혀용을 했는데 로그인을 하라고 나온다.
csrf때문에 발생하는 일인데 다음시간에 살펴보고 우선 disable해놓자


DB를 확인해 보면 비밀번호가 암호화 되어있는 것을 볼 수 있다.

이 정보로 로그인을 진행하면 로그인이 정상 작동하는 것을 확인할 수 있다.

