-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/soeunuhm step1 #2
base: soeunuhm
Are you sure you want to change the base?
Changes from all commits
73befb0
a3066af
49ec5a6
d098e1f
eec41fb
4481ff0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
## Step 1 | ||
> 회원가입 & 로그인 구현 | ||
|
||
### 회원가입 | ||
- `user/signup` | ||
- h2 database 사용 | ||
|
||
### 로그인 | ||
- `user/login` | ||
- id(PK), userId, username, email, password 를 저장했다. | ||
- 여기서 userId 와 email 은 unique 값으로 설정했다. 만약 이미 존재하는 userId 와 email을 입력하면 `DataIntegrityViolationException` 이 뜨도록 했다. | ||
|
||
----- | ||
### Spring Security | ||
- 스프링 시큐리티는 스프링 기반 어플리케이션의 인증과 권한을 담당하는 스프링 하위 프레임워크이다. | ||
- 스프링 시큐리티의 세부 설정은 SecurityFilterChain 빈을 생성하여 설정했다. | ||
|
||
### 프론트 | ||
- 프론트는 bootstrap 사용 | ||
|
||
---- | ||
### 구현 화면 | ||
- 회원가입 | ||
|
||
![image](https://github.com/ddoddii/ddoddii.github.io/assets/95014836/04ad9802-5a5b-46f3-8561-f5024dfd9cf3) | ||
|
||
- 로그인 | ||
|
||
![image](https://github.com/ddoddii/ddoddii.github.io/assets/95014836/a9c6545a-3ddb-43f1-93c9-fc08d05ac2c5) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.cokepoke.app; | ||
|
||
import org.springframework.stereotype.Controller; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
|
||
@Controller | ||
public class MainController { | ||
@GetMapping("/") | ||
public String root() { | ||
return "redirect:/user/login"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.cokepoke.app; | ||
|
||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
|
||
@SpringBootApplication | ||
public class SbbApplication { | ||
public static void main(String[] args) { | ||
SpringApplication.run(SbbApplication.class, args); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package com.cokepoke.app; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.authentication.AuthenticationManager; | ||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; | ||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
public class SecurityConfig { | ||
@Bean | ||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
http | ||
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests | ||
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll()) | ||
.csrf((csrf) -> csrf | ||
.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**"))) | ||
.headers((headers) -> headers | ||
.addHeaderWriter(new XFrameOptionsHeaderWriter( | ||
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))) | ||
.formLogin((formLogin) -> formLogin | ||
.loginPage("/user/login") | ||
.defaultSuccessUrl("/")) | ||
.logout((logout) -> logout | ||
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout")) | ||
.logoutSuccessUrl("/") | ||
.invalidateHttpSession(true)) | ||
; | ||
return http.build(); | ||
} | ||
|
||
@Bean | ||
PasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
|
||
@Bean | ||
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { | ||
return authenticationConfiguration.getAuthenticationManager(); | ||
} | ||
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. spring security 의 default authentication 방식을 채택하셨군요..! 그럼 얘는 어떤 인증 방식인가요? token 기반인가요? session 기반인가요? |
||
|
||
|
||
} |
becooq81 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 엔티티에 @Setter 어노테이션을 지양하는 편이에요! public으로 객체를 마음대로 조작하는 것보다는 필요한 경우에 public update 메서드를 쓰면 어떨까요? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import jakarta.persistence.*; | ||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
@Entity | ||
public class SiteUser { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ddl 문을 따로 정의하지 않고 auto-ddl 옵션을 사용하시는 것 같은데 index 는 필요 없을까요? |
||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Column(unique = true) | ||
private String userId; | ||
|
||
@Column(unique = true) | ||
private String email; | ||
|
||
@Column | ||
private String password; | ||
|
||
@Column | ||
private String userName; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] |
||
|
||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 필수적인건 아니지만 UserResponse 같은 dto를 만들어서 리턴해주면 어떨까싶어요! 멤버를 조회하는 api 만들 때 활용도 높을 것 같습니당 +_+ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.dao.DataIntegrityViolationException; | ||
import org.springframework.stereotype.Controller; | ||
import org.springframework.validation.BindingResult; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
|
||
@RequiredArgsConstructor | ||
@Controller | ||
@RequestMapping("/user") | ||
public class UserController { | ||
private final UserService userService; | ||
|
||
@GetMapping("/signup") | ||
public String signup(UserCreateForm userCreateForm){ | ||
return "signup_form"; | ||
} | ||
|
||
@PostMapping("/signup") | ||
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult){ | ||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프론트까지 만들어주셔도 괜찮긴 하지만 이후 step 들까지 프론트를 만들긴 어려우니까 그냥 api-server 의 형태로만 구현해도 될 것 같아요. 따로 view controller 를 사용하신 이유가 있으신가요? 👀 |
||
if (bindingResult.hasErrors()){ | ||
return "signup_form"; | ||
} | ||
if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())){ | ||
bindingResult.rejectValue("password2","PasswordIncorrect","Password Does Not Match"); | ||
return "signup_form"; | ||
} | ||
|
||
try{ | ||
userService.createUser(userCreateForm.getUserId(), userCreateForm.getUserName(), userCreateForm.getEmail(), userCreateForm.getPassword1()); | ||
} | ||
catch (DataIntegrityViolationException e) { | ||
e.printStackTrace(); | ||
bindingResult.reject("signupFailed", "Already Exsisting User"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오타난듯..! |
||
return "signup_form"; | ||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
bindingResult.reject("signupFailed", e.getMessage()); | ||
return "signup_form"; | ||
} | ||
|
||
|
||
return "redirect:/"; | ||
} | ||
|
||
@GetMapping("/login") | ||
public String login(){ | ||
return "login_form"; | ||
} | ||
|
||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. public 수준의 @Setter를 쓸거면 @AllArgsConstructor 사용하는 것도 추천해요!! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.NotEmpty; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.Getter; | ||
import lombok.Setter; | ||
|
||
@Getter | ||
@Setter | ||
public class UserCreateForm { | ||
@Size(min = 3, max = 25, message = "User Id must be over 3 and under 25 characters") | ||
@NotEmpty(message = "UserID is necessary") | ||
private String userId; | ||
|
||
@NotEmpty(message = "Password is necessary") | ||
private String password1; | ||
|
||
@NotEmpty(message = "Check password again") | ||
private String password2; | ||
|
||
@NotEmpty(message = "Email is necessary") | ||
private String email; | ||
|
||
@NotEmpty(message = "User name is necessary") | ||
private String userName; | ||
|
||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
import java.util.Optional; | ||
|
||
public interface UserRepository extends JpaRepository<SiteUser, Long> { | ||
Optional<SiteUser> findByUserId(String userId); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import lombok.Getter; | ||
|
||
@Getter | ||
public enum UserRole { | ||
ADMIN("ROLE_ADMIN"), | ||
USER("ROLE_USER") ; | ||
|
||
UserRole(String value){ | ||
this.value = value; | ||
} | ||
private String value; | ||
|
||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 보안과 관련된 코드는 auth 라는 패키지(폴더)로 따로 빼도 괜찮을 것 같아요. user 에 관한 정보긴 하지만 비즈니스 도메인 로직과 함께 있을 필요는 없는 것 같아서요. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.userdetails.User; | ||
import org.springframework.security.core.userdetails.UserDetails; | ||
import org.springframework.security.core.userdetails.UserDetailsService; | ||
import org.springframework.security.core.userdetails.UsernameNotFoundException; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
@RequiredArgsConstructor | ||
@Service | ||
public class UserSecurityService implements UserDetailsService { | ||
|
||
private final UserRepository userRepository; | ||
@Override | ||
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException{ | ||
Optional<SiteUser> _siteUser = this.userRepository.findByUserId(userId); | ||
if (_siteUser.isEmpty()){ | ||
throw new UsernameNotFoundException("Cannot Find User"); | ||
} | ||
SiteUser siteUser = _siteUser.get(); | ||
List<GrantedAuthority> authorities = new ArrayList<>(); | ||
if ("admin".equals(userId)){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 우리의 프로젝트에서는 큰 상관이 없겠지만 관리자의 계정의 id 를 "admin", "administrator", "root" 등과 같이 뻔한 것으로 짓는건 보안적으로 금기긴 합니다. 더 자세한 이유는 한번 찾아보셔도 좋을 것 같아요~ |
||
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue())); | ||
} | ||
else { | ||
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue())); | ||
} | ||
Comment on lines
+30
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용자의 ROLE 을 id 를 보고 판단하면 새로운 관리자가 추가될 때마다 코드를 고쳐줘야 할 것 같은데 비효율적이지 않을까요? 지금은 어드민 아니면 모두 일반 유저지만 만약에 중간에 어드민보다는 조금 낮은 권한의 ROLE 을 만들어야 한다면 어떻게 처리하는게 좋을까요? |
||
return new User(siteUser.getUserId(), siteUser.getPassword(), authorities); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.cokepoke.app.user; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.stereotype.Service; | ||
|
||
@RequiredArgsConstructor | ||
@Service | ||
public class UserService { | ||
private final UserRepository userRepository; | ||
private final PasswordEncoder passwordEncoder; | ||
|
||
|
||
public SiteUser createUser(String userId, String userName, String email, String password){ | ||
SiteUser user = new SiteUser(); | ||
user.setUserId(userId); | ||
user.setUserName(userName); | ||
user.setEmail(email); | ||
user.setPassword(passwordEncoder.encode(password)); | ||
this.userRepository.save(user); | ||
Comment on lines
+15
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것 역시 크게 문제되는 코드는 아니지만 요즘 자바에서는 setter 대신 builder pattern 이나 constructor 를 사용하는 추세로 가고 있습니다. 많은 차이점이 있지만 가장 큰 이유는 immutable object 를 생성하기 위함인데 이것도 한번 찾아보시는 것 추천드려요! |
||
return user; | ||
} | ||
|
||
|
||
} |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<div th:fragment="formErrorsFragment" class="alert alert-danger" | ||
role="alert" th:if="${#fields.hasAnyErrors()}"> | ||
<div th:each="err : ${#fields.allErrors()}" th:text="${err}" /> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<!doctype html> | ||
<html lang="ko"> | ||
<head> | ||
<!-- Required meta tags --> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||
<!-- Bootstrap CSS --> | ||
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}"> | ||
<!-- sbb CSS --> | ||
<link rel="stylesheet" type="text/css" th:href="@{/style.css}"> | ||
<title>Welcome to Coke Poke 🥤</title> | ||
</head> | ||
<body> | ||
<!-- 네비게이션바 --> | ||
<nav th:replace="~{navbar :: navbarFragment}"></nav> | ||
<!-- 기본 템플릿 안에 삽입될 내용 Start --> | ||
<th:block layout:fragment="content"></th:block> | ||
<!-- 기본 템플릿 안에 삽입될 내용 End --> | ||
<!-- Bootstrap JS --> | ||
<script th:src="@{/bootstrap.min.js}"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<html layout:decorate="~{layout}"> | ||
<div layout:fragment="content" class="container my-3"> | ||
<div class="my-3 border-bottom"> | ||
<div> | ||
<h4>Log In</h4> | ||
</div> | ||
</div> | ||
<form th:action="@{/user/login}" method="post"> | ||
<div th:if="${param.error}"> | ||
<div class="alert alert-danger"> | ||
Check Id or Password | ||
</div> | ||
</div> | ||
<div class="mb-3"> | ||
<label for="username" class="form-label">ID</label> | ||
<input type="text" name="username" id="username" class="form-control"> | ||
</div> | ||
<div class="mb-3"> | ||
<label for="password" class="form-label">Password</label> | ||
<input type="password" name="password" id="password" class="form-control"> | ||
</div> | ||
<button type="submit" class="btn btn-primary">LogIn</button> | ||
</form> | ||
</div> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom" | ||
xmlns:sec="http://www.w3.org/1999/xhtml"> | ||
<div class="container-fluid"> | ||
<a class="navbar-brand" href="/">Coke Poke 👉🥤👈</a> | ||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" | ||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> | ||
<span class="navbar-toggler-icon"></span> | ||
</button> | ||
<div class="collapse navbar-collapse" id="navbarSupportedContent"> | ||
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> | ||
<li class="nav-item"> | ||
<a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">Log In</a> | ||
<a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">Log Out</a> | ||
</li> | ||
<li class="nav-item"> | ||
<a class="nav-link" th:href="@{/user/signup}">Sign Up</a> | ||
</li> | ||
</ul> | ||
</div> | ||
</div> | ||
</nav> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이러면 모든 api 주소가 public 한거 아닌가요?