[Web_3] STUSSY CLONE PROJECT 2 🐰

08627Β·2022λ…„ 9μ›” 30일
2

Spring

λͺ©λ‘ 보기
10/13
post-thumbnail


πŸ“Œ Spring Boot Security
πŸ‘Ύ Register νšŒμ›κ°€μž…
πŸ“Œ Spring Boot Validation (@Valid / @Validated / BindingResult )


πŸ’‘ Spring Boot Security

μŠ€ν”„λ§ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 인증 및 μ•‘μ„ΈμŠ€ μ œμ–΄λ₯Ό μœ„ν•œ ν”„λ ˆμž„μ›Œν¬μ΄λ‹€.

▫️ dependency 등둝 Β Β ( 버전 λͺ…μ‹œ 해주지 μ•Šμ•„λ„ 됨 )

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­(request) μ‹œ
➑️ Filter 듀을 μ°¨λ‘€λŒ€λ‘œ 거쳐 (β†’ Filter Chain) Dispacher Servlet 둜 κ°€κ²Œ λœλ‹€.
( κ±°κΈ°μ„œ μš”μ²­ url 에 λ§žλŠ” λ©”μ†Œλ“œκ°€ μžˆλŠ” Controller 둜 κ°€κ²Œ 됨 )

Spring SecurityλŠ” DelegatingFilterProxy λΌλŠ” ν•„ν„°λ₯Ό μƒμ„±ν•˜μ—¬ κΈ°μ‘΄ Filter Chain μ‚¬μ΄μ—μ„œ SecurityFilterChain 을 λ™μž‘μ‹œν‚¨λ‹€.

* μ°Έκ³  : Security Filter Chain


▫️ SecurityConfig.java

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean 	// ➑️ μŠ€ν”„λ§ IoC 등둝
    // Security κ°€ 가지고 μžˆλŠ” BCryptPasswordEncoder 클래슀λ₯Ό κ°€μ Έλ‹€ μ“°κΈ° μœ„ν•΄μ„œλŠ” 
    // κΈ°μ‘΄ 쑴재 객체 (;Configuration 객체)μ—μ„œ μƒμ„±ν•˜κ³  @Bean 을 달아, 
    // IoC λ“±λ‘ν•˜μ—¬ μ‚¬μš©ν•¨ ❗ 
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.httpBasic().disable();
        http.authorizeRequests() // λͺ¨λ“  μš”μ²­μ— μ‹€ν–‰
        
                // ⭐ Page
                // κ²½λ‘œμ— λ”°λ₯Έ μ ‘κ·Ό κΆŒν•œ μ„€μ •; κΆŒν•œμ„ 가진 μ‚¬λžŒλ§Œ 접속 κ°€λŠ₯
                .antMatchers("/admin/**")
                .access("hasRole('ADMIN') or hasRole('MANAGER')") 
                .antMatchers("/account")
                .access("hasRole('USER') or hasRole('ADMIN') or hasRole('MANAGER')")

                .antMatchers("/", "/index", "/collections/**")
                .permitAll() // λͺ¨λ“  μ ‘κ·Ό κΆŒν•œμ„ ν—ˆμš©ν•΄λΌ.
                .antMatchers("/account/login", "/account/register")
                .permitAll()
                
                // ⭐ Resource
                .antMatchers("/static/**", "/image/**")
                .permitAll() 

                // ⭐ API
                .antMatchers("/api/account/register")
                .permitAll()

                .anyRequest() // λ‹€λ₯Έ λͺ¨λ“  μš”μ²­λ“€μ€
                .permitAll()
				// .denyAll() // λͺ¨λ“  접근을 차단해라. 

                .and()
                .formLogin() 					// 폼둜그인 λ°©μ‹μœΌλ‘œ 인증할 것. 
                .usernameParameter("email")
                .loginPage("/account/login") 	// 둜그인 νŽ˜μ΄μ§€λ₯Ό λ„μš°λŠ” GET μš”μ²­
                // β€» μŠ€ν”„λ§ Security λŠ” κΈ°λ³Έ 둜그인 창을 μ œκ³΅ν•˜κ³  있음. 
                .loginProcessingUrl("/account/login")   	// 둜그인 POST μš”μ²­
                .failureHandler(new AuthFailureHandler())	// 둜그인 μ‹€νŒ¨ μ‹œ 처리
                .defaultSuccessUrl("/index");
    }
}

WebSecurityConfigurerAdapter λ₯Ό μƒμ†ν•˜λŠ” 클래슀λ₯Ό

@Configuration 둜 μŠ€ν”„λ§ IoC에 등둝 ν•˜κ³ 
@EnableWebSecurity 둜 기쑴의 WebSecurityConfigurerAdapter 클래슀λ₯Ό ν•΄λ‹Ή 클래슀둜 λŒ€μ²΄ν•˜μ—¬ λ³΄μ•ˆμ„ ν™œμ„±ν™” ν•œλ‹€. ( ➑️ SpringSecurityFilterChain 에 λ“±λ‘λ˜μ–΄ μŠ€ν”„λ§μ—μ„œ 생성해 μ‚¬μš©ν•˜κ²Œ 됨 )


πŸ‘Ύ Register νšŒμ›κ°€μž…

ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ JSON ν˜•μ‹μœΌλ‘œ νšŒμ›κ°€μž… 정보(name, eamil, password)λ₯Ό λ³΄λ‚΄κ²Œ 되면,

ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­ μ‹œ Validation 체크λ₯Ό ν•˜κ³ , RegisterReqDto 둜 λ°›μ•„μ„œ β†’ USER 둜 μ²˜λ¦¬ν•œ ν›„ β†’ μ„œλ²„ 응닡 μ‹œμ— CMRespDto 둜 μ‘λ‹΅ν•΄μ£Όκ²Œ λœλ‹€.

🐾 Controller

▫️ AccountPageController.java

@RequestMapping("/account")
@Controller
public class AccountPageController {

    @GetMapping("/login")
    public String login(Model model, @RequestParam @Nullable String error) { // ⭐ url μƒμ—μ„œ (ajax) error 값을 λ°›μ•„μ˜€κ²Œ 됨. 
        if (error != null) {
            model.addAttribute("error", error.equals("auth") ? "이메일 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έκ°€ 잘λͺ»λ˜μ—ˆμŠ΅λ‹ˆλ‹€." : "");
            // auth errorλ©΄ μ—λŸ¬ λ©”μ‹œμ§€, μ•„λ‹ˆλ©΄ 곡백 값을 μ€€λ‹€
        }
        return "account/login";
    }

    @GetMapping("/register")
    public String register() {
        return "account/register";
    }
}

🐾 Domain

▫️ User.java

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
    private int id;
    private String username;
    private String oauth_username;
    private String password;
    private String name;
    private String email;
    private String provider;
    private int role_id;
    private Role role;
    private LocalDateTime create_date;
    private LocalDateTime update_date;
}

▫️ Role.java

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Role {
    private int id;
    private String role;
    private String role_name;
}



πŸ’‘ Spring Boot Validation (@Valid / @Validated / BingdingResult)

🦎 μœ νš¨μ„± 검사

ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­μ΄ 듀어왔을 λ•Œ, μ„œλ²„μ—μ„œ DTO에 Ajax둜 λ°›μ•„μ˜¨ 데이터λ₯Ό 연결해쀄 λ•Œ (-> 바인딩) , 데이터 값이 μœ νš¨ν•œ 지 검사해야 ν•œλ‹€.

Ajax -> < . . . Validation . . . > -> DTO -> Controller

🦎 Spring Boot Validation 을 μ΄μš©ν•œ μœ νš¨μ„± 검사

▫️ dependency μΆ”κ°€

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

▫️ AccountApi.java

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/account")
@RestController
public class AccountApi {
    
    private final AccountService accountService; // DI

    @ValidAspect // μœ νš¨μ„± 검사 Β· 둜그 λ‚¨κΈ°λŠ” κΈ°λŠ₯을 μ–΄λ…Έν…Œμ΄μ…˜(@)으둜 처리
    @LogAspect	 
    @PostMapping("/register")
    public ResponseEntity<?> register(@Validated(ValidationSequence.class) @RequestBody RegisterReqDto registerReqDto, BindingResult bindingResult) throws Exception { 
    // ➑️ 이 DTO λ₯Ό κ°€μ Έμ˜¬ λ•Œ Valid 체크λ₯Ό ν•˜κ² λ‹€. ..

 		accountService.checkDuplicatedEmail(registerReqDto.getEmail());
        accountService.register(registerReqDto);

        return ResponseEntity.ok().body(new CMRespDto<>(1, "Successfully registered", registerReqDto));
    }
}

βœ… @RequestBody
ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ JSON 데이터λ₯Ό 보낼 λ•Œ body에 데이터λ₯Ό λ‹΄μ•„ μ „μ†‘ν•˜κΈ° λ•Œλ¬Έμ— JSON 데이터λ₯Ό λ°›μ•„μ˜€κΈ° μœ„ν•΄μ„œλŠ” @RequestBody κ°€ 항상 ν•„μš”ν•¨.

βœ… μœ νš¨μ„± 검사λ₯Ό 진행할 클래슀 ν˜Ήμ€ λ©”μ†Œλ“œμ˜ νŒŒλΌλ―Έν„°(Request 객체) μ•žμ—
@Valid / @Validated μ–΄λ…Έν…Œμ΄μ…˜μ„ μ£Όλ©΄ Request 객체λ₯Ό κ°€μ Έμ˜¬ λ•Œ μœ νš¨μ„± 이 κ²€μ‚¬λœλ‹€.

🦎 BindingResult

μœ νš¨μ„± 검사 μ‹€νŒ¨ μ‹œ (μœ νš¨ν•˜μ§€ μ•Šμ€ 값이 μžˆμ„ λ•Œ) 였λ₯˜μ— λŒ€ν•œ 정보λ₯Ό λ³΄κ΄€ν•˜λŠ” 객체이닀.

데이터에 μœ νš¨ν•˜μ§€ μ•Šμ€ 속성이 μžˆλ‹€λ©΄, 그에 λŒ€ν•œ 였λ₯˜ 정보가 λ‹΄κΈ΄λ‹€. ν•΄λ‹Ή 정보λ₯Ό μ»¨νŠΈλ‘€λŸ¬μ— 전달해 였λ₯˜ νŽ˜μ΄μ§€λ₯Ό λ³„λ„λ‘œ μ²˜λ¦¬ν•΄ 쀄 수 μžˆλ‹€.

▫️ RegisterReqDto.java (➑️ νŒŒλΌλ―Έν„°λ‘œ λ°›μ•„μ˜€λŠ” DTO ν΄λž˜μŠ€μ—μ„œ 객체의 μ œμ•½ 쑰건을 κ±Έμ–΄μ€€λ‹€)

@Data
public class RegisterReqDto {
    @NotBlank(message = "이름은 λΉ„μ›Œ λ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€", groups = ValidationGroups.NotBlankGroup.class)
    @Size(min = 1, max = 3, message = "이름은 ν•œκΈ€μžμ—μ„œ μ„ΈκΈ€μž 사이여야 ν•©λ‹ˆλ‹€", groups = ValidationGroups.SizeCheckGroup.class)
    @Pattern(regexp = "^[κ°€-νž‡]*$",
            message = "이름은 ν•œκΈ€λ§Œ μž…λ ₯κ°€λŠ₯ν•©λ‹ˆλ‹€",
            groups = ValidationGroups.PatternCheckGroup.class
    )
    private String lastName;
    
    @NotBlank(message = "성은 λΉ„μ›Œ λ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€", groups = ValidationGroups.NotBlankGroup.class)
    @Size(min = 1, max = 2, message = "성은 ν•œκΈ€μžμ—μ„œ λ‘κΈ€μž 사이여야 ν•©λ‹ˆλ‹€", groups = ValidationGroups.SizeCheckGroup.class)
    @Pattern(regexp = "^[κ°€-νž‡]*$",
            message = "성은 ν•œκΈ€λ§Œ μž…λ ₯κ°€λŠ₯ν•©λ‹ˆλ‹€",
            groups = ValidationGroups.PatternCheckGroup.class
    )
    private String firstName;

    @Email
    @NotBlank(message = "이메일은 λΉ„μ›Œ λ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€", groups = ValidationGroups.NotBlankGroup.class)
    private String email;

    @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” λΉ„μ›Œ λ‘˜ 수 μ—†μŠ΅λ‹ˆλ‹€", groups = ValidationGroups.NotBlankGroup.class)
    @Size(min = 8, max = 16, message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 8μžμ—μ„œ 16자 μ‚¬μ΄μ—¬μ•Όν•©λ‹ˆλ‹€.", groups = ValidationGroups.SizeCheckGroup.class)
    @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~!@#$%^&*_])[a-zA-Z\\d-~!@#$%^&*_]*$",
            message = "λΉ„λ°€λ²ˆν˜ΈλŠ” 숫자, 영문, 특수기호λ₯Ό ν•˜λ‚˜ 이상 ν¬ν•¨ν•˜μ—¬ μž‘μ„±ν•΄μ•Όν•©λ‹ˆλ‹€",
            groups = ValidationGroups.PatternCheckGroup.class
    )
    private String password;


    public User toUserEntity() {
        return User.builder()
                .username(email)
                .password(new BCryptPasswordEncoder().encode(password)) // λΉ„λ°€λ²ˆν˜Έ μ•”ν˜Έν™”
                .name(firstName + lastName)
                .email(email)
                .role_id(1) // μ‚¬μš©μž νšŒμ›κ°€μž… : role_id -> 1
                .build();
    }
}

❗ @Valid, @Validated
@Valid : Java κΈ°λ³Έ μ–΄λ…Έν…Œμ΄μ…˜
@Validated : Spring ν”„λ ˆμž„μ›Œν¬μ—μ„œ μ§€μ›ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜. @Valid κΈ°λŠ₯에 μΆ”κ°€μ μœΌλ‘œ μœ νš¨μ„±μ„ 검사할 그룹을 지정할 수 μžˆλ„λ‘ ν•œλ‹€.

@Validated (ValidationSequence.class) ➑️ μ˜΅μ…˜μ„ μ£Όμ–΄ μœ νš¨μ„± 검사 그룹을 지정해 쀄 수 μžˆλ‹€. ν•΄λ‹Ή ν΄λž˜μŠ€μ— μžˆλŠ” ValidationGroups 둜 μœ νš¨μ„± 검사λ₯Ό μ‹€μ‹œν•œλ‹€.

public interface ValidationGroups {
    public interface NotBlankGroup {};
    public interface SizeCheckGroup {};
    public interface PatternCheckGroup {};
}
@GroupSequence({ //➑️ Validation 검사 μˆœμ„œλ₯Ό μ •ν•΄μ£ΌκΈ° μœ„ν•¨. 
        ValidationGroups.NotBlankGroup.class,
        ValidationGroups.SizeCheckGroup.class,
        ValidationGroups.PatternCheckGroup.class,
        Default.class
})
public interface ValidationSequence {
}



μ°Έκ³  πŸ§™
Spring Security - FilterChain
μžλ°” μ •κ·œ ν‘œν˜„μ‹ μ‚¬μš©λ²• 및 예제 - Pattern, Matcher
πŸ™ˆSpringBoot @Valid둜 μœ νš¨μ„± κ²€μ‚¬ν•˜κΈ°πŸ΅
@Valid μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ Parameter κ²€μ¦ν•˜κΈ°
@Valid와 @Validatedλ₯Ό μ΄μš©ν•œ μœ νš¨μ„± κ²€μ¦μ˜ λ™μž‘ 원리 및 μ‚¬μš©λ²• μ˜ˆμ‹œ
μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° κΈ°λ³Έ API및 Filter 이해


πŸ“’ μ†Œκ° 🐰
μ²œμ›μ§œλ¦¬ λ³€ν˜Έμ‚¬ λ³΄λŠ”λ° κ½€ μžΌμžˆλ‹€ ! κΈˆμš”μΌ μ™”μœΌλ©΄ . . .
남ꢁ민 λ‚˜μ˜€λŠ” λ“œλΌλ§ˆ λ‹€ 웃기고 재밌음
μƒκ°λ‚œ 김에 κΉ€κ³Όμž₯μ΄λ‚˜ 또 봐야겠닀

0개의 λŒ“κΈ€