[쇼핑몰만들기] 2. 자체 로그인을 만들어보자
지난 시간에 이어서 이번에는 자체 로그인을 만들어보았다.
하지만 이번에도 꽤나 문제점이 존재했다.
우선, 구글 로그인과 같은 소셜 로그인은 스프링 부트에서 제공하는 Security를 사용하기 마련인데, 얘를 사용하기 위해서는 꽤나 까다로운 틀을 갖춰야한다는 점이다.
또 OAuth 경로로 자동으로 이동한다는 문제가 발생
<div class="login-container">
<form action="/login" method="post">
<label for="username">아이디</label>
<input type="text" id="username" name="username" required>
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" required>
<input type="submit" value="로그인">
</form>
<a href="/register">회원가입</a>
<br>
<a href="/login/oauth2/code/google">google login</a>
</div>
다음과 같이 로그인 화면을 구성하였다.
이 때, "회원가입" 버튼을 클릭하게 되면 "/register"으로 이동하는 것이 당연하다고 생각했다.
하지만 "/login/oauth2/code/google"로 자동으로 OAuth 화면으로 이동하였다.
흠 .. 정말 정말 이상했다.
당연히, 스프링 부트가 문제가 있는 것이 아니라, 내가 문제일 것이라고 생각했다.
이전에 코드를 작성할 때, "/login" 경로가 아닌 구글 로그인 화면으로 이동한 것처럼 스프링부트 내에서 자동으로 설정한 무언가가 있을 것이라고 생각했다.
따라서 SecurityConfig 파일을 손보기로 했다.
.authorizeHttpRequests((authorizeRequest) -> authorizeRequest
.requestMatchers("/posts/new", "/comments/save").hasRole(Role.USER.name())
.requestMatchers("/login").permitAll()
.requestMatchers("/oauth2/authorization/google").permitAll()
.requestMatchers("/", "/css/**", "images/**", "/js/**", "/login", "/logout/*", "/posts/**", "/comments/**").permitAll()
.anyRequest().authenticated()
)
.logout( // 로그아웃 성공 시 / 주소로 이동
(logoutConfig) -> logoutConfig.logoutSuccessUrl("/")
)
문제는 다음과 같았다.
당연한 이야기이지만, 보안을 위해서 Spring Boot Security에서는 Authorization과 Authentication을 제공한다.
이 때, "/register"라는 경로는 어느 곳에서도 허가하지 않은 경로였기 때문에, 인증이 필요해서 계속 로그인 화면으로 이동한 것이었다.
따라서, 아래와 같이 "/register" 경로도 모두에게 허가 가능한 경로로 추가하여 문제를 해결할 수 있었다.
.authorizeHttpRequests((authorizeRequest) -> authorizeRequest
.requestMatchers("/posts/new", "/comments/save").hasRole(Role.USER.name())
.requestMatchers("/login", "/register", "/login").permitAll()
.requestMatchers("/oauth2/authorization/google").permitAll()
.requestMatchers("/", "/css/**", "images/**", "/js/**", "/login", "/logout/*", "/posts/**", "/comments/**").permitAll()
.anyRequest().authenticated()
)
.logout( // 로그아웃 성공 시 / 주소로 이동
(logoutConfig) -> logoutConfig.logoutSuccessUrl("/")
)
추가적으로 소셜 로그인이 아닌 자체 로그인으로 생성한 엔티티 관리를 위해서 이를 Normal_User라고 정의하고 그에 맞는 서비스와 레포지토리 코드도 작성하였다.
@Getter
@Setter
@Entity
@ToString
public class Normal_User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String username;
private String password;
private String name;
}
@Service
public class NormalUserService {
private final NormalUserRepository normalUserRepository;
public NormalUserService(NormalUserRepository normalUserRepository) {
this.normalUserRepository = normalUserRepository;
}
public Long create(Normal_User normal_user) {
normalUserRepository.save(normal_user);
return normal_user.getId();
}
public Optional<Normal_User> findbyIDandPassword(String id, String password) {
return normalUserRepository.findByUsernameAndPassword(id, password);
}
}
@Repository
public interface NormalUserRepository extends JpaRepository<Normal_User, String> {
Optional<Normal_User> findByUsernameAndPassword(String username, String password);
}
스프링 부트의 막강한 장점, JPA를 몸소 느끼다
이전에 스프링 부트의 기능을 익히기 위해 만들었던 게시판 프로젝트에서는 EntityManager를 사용해서 하나하나 Query문을 작성하여 DB에 접근하였다.
하지만 이번을 통해서 스프링 부트의 JPA 기능을 좀 더 알 수가 있었다.
레포지토리에 메서드명을 입력할 시, 기존에 작성하지 않았음에도 "findBy .."를 입력하면 테이블에 있는 변수로 자동으로 입력되는 것을 확인할 수가 있다.
마치 인공지능처럼 일정 형식에 맞게 메서드명을 작성하면, 자동으로 변수에 맞는 쿼리문을 작성해준다는 것이다. (여기서 좀 감탄했다.)
게다가, 복잡한 쿼리문은 개발자가 직접 작성할 수 있다는 점이 매우 놀라웠다.
이렇게 하면 JDBC와 다르게 실수할 경우도 줄고 무엇보다 편의성이 매우 올라간다는 장점이 있는데, JPA 공부도 좀 더 깊게 해야겠다는 생각이 들었다.
문제 2 : 세션 충돌 문제
앞서 구글 로그인을 통한 User 엔티티는 자체 로그인인 Normal_User의 엔티티와 구성 요소(변수)가 사뭇 다르다.
예를 들어, User에만 Role, Picture, Email 등이 있다는 점이다.
그러므로 HomeController에서 세션을 저장 시에 계속 오류가 발생하였다.
완벽한 해결방안은 아니지만, 우선은 세션을 따로 분리하여, 소셜 로그인이 아니라면 자체 로그인을 했다고 생각하고 각 세션을 저장하도록 하였다. (아마 추후에 엄청난 후폭풍을 가져올 해결방법이지 않나 생각이 든다.)
@GetMapping("/")
public String home(Model model) {
SessionUser user = (SessionUser) httpSession.getAttribute("user");
Normal_User nuser = (Normal_User) httpSession.getAttribute("nuser");
if (user != null) {
model.addAttribute("userName", user.getName());
} else if (nuser != null) {
model.addAttribute("userName", nuser.getName());
}
return "home";
}
마무리
오늘은 이렇게 소셜 로그인 뿐만 아니라, 자체 로그인도 함께 구현해보았다.
솔직히 되게 간단하게 끝낼 수 있을 줄 알았다.
하지만, 간단한 API임에도 불구하고 디테일하게 고려해야할 부분이 많아서 꽤나 전전긍긍했다.
무엇보다 스프링 부트의 업데이트로 인해 기존의 블로그 자료로도 해결하지 못한 부분을 해결하느라고 가장 애를 먹었던 것 같다.
이번 시간을 통해서 스프링 부트에서는 어떤 흐름으로 보안을 관리하는 지 알 수 있는 시간이었다. (이는 추후에 따로 '백엔드 지식' 카테고리에 업로드할 예정이다.)
다음 시간에는 판매자가 상품을 등록하고 이를 메시지 브로커를 통해서 엘라스틱 서치에 저장하는 기능을 구현해보도록 하겠다.