π Spring Boot Security
πΎ Register νμκ°μ
π Spring Boot Validation (@Valid / @Validated / BindingResult )
μ€νλ§ μ΄ν리μΌμ΄μ μ μΈμ¦ λ° μ‘μΈμ€ μ μ΄λ₯Ό μν νλ μμν¬μ΄λ€.
β«οΈ 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 μ λ±λ‘λμ΄ μ€νλ§μμ μμ±ν΄ μ¬μ©νκ² λ¨ )
ν΄λΌμ΄μΈνΈμμ JSON νμμΌλ‘ νμκ°μ μ 보(name, eamil, password)λ₯Ό 보λ΄κ² λλ©΄,
ν΄λΌμ΄μΈνΈ μμ² μ Validation 체ν¬λ₯Ό νκ³ , RegisterReqDto λ‘ λ°μμ β USER λ‘ μ²λ¦¬ν ν β μλ² μλ΅ μμ CMRespDto λ‘ μλ΅ν΄μ£Όκ² λλ€.
β«οΈ 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";
}
}
β«οΈ 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;
}
ν΄λΌμ΄μΈνΈ μμ²μ΄ λ€μ΄μμ λ, μλ²μμ DTOμ Ajaxλ‘ λ°μμ¨ λ°μ΄ν°λ₯Ό μ°κ²°ν΄μ€ λ (-> λ°μΈλ©) , λ°μ΄ν° κ°μ΄ μ ν¨ν μ§ κ²μ¬ν΄μΌ νλ€.
Ajax -> < . . . Validation . . . > -> DTO -> Controller
β«οΈ 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 κ°μ²΄λ₯Ό κ°μ Έμ¬ λ μ ν¨μ± μ΄ κ²μ¬λλ€.
μ ν¨μ± κ²μ¬ μ€ν¨ μ (μ ν¨νμ§ μμ κ°μ΄ μμ λ) μ€λ₯μ λν μ 보λ₯Ό 보κ΄νλ κ°μ²΄μ΄λ€.
λ°μ΄ν°μ μ ν¨νμ§ μμ μμ±μ΄ μλ€λ©΄, κ·Έμ λν μ€λ₯ μ λ³΄κ° λ΄κΈ΄λ€. ν΄λΉ μ 보λ₯Ό 컨νΈλ‘€λ¬μ μ λ¬ν΄ μ€λ₯ νμ΄μ§λ₯Ό λ³λλ‘ μ²λ¦¬ν΄ μ€ μ μλ€.
β«οΈ 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 μ΄ν΄
π’ μκ° π°
μ²μμ§λ¦¬ λ³νΈμ¬ 보λλ° κ½€ μΌμλ€ ! κΈμμΌ μμΌλ©΄ . . .
λ¨κΆλ―Ό λμ€λ λλΌλ§ λ€ μκΈ°κ³ μ¬λ°μ
μκ°λ κΉμ κΉκ³Όμ₯μ΄λ λ λ΄μΌκ² λ€