Published 2023. 1. 6. 20:06
반응형
1. Spring Security 란?
- Spring에서 제공하는 애플리케이션의 보안 (권한, 인증, 인가 등)을 담당하는 프레임워크
- Spring Security는 인증과 권한에 대한 부분을 Filter의 흐름에 따라 처리
- [Spring Boot] Session을 사용한 로그인 구현에서 세션을 통해 로그인 하는 방법을 정리했었음
- 로그인 성공 시 직접 세션에 "userId"라는 attributes에 userId를 담아 로그인을 진행했었음
- Security에는 Security Session이 존재하고, 로그인 성공 시 여기에 Authentication을 넣어줘야 함
- Authentication에는 UserDetails라는 유저 정보를 또 넣어 줘야함
- Security Session(Authentication(UserDetails)) 이런 식의 포맷을 직접 맞춰줘야 함
- 대부분의 사이트를 개발할 때 로그인 기능은 거의 필수적인데 이를 체계적으로 만들 수 있도록 도와준다고 보면 될듯함
2. Spring Security를 사용한 로그인 구현 (Form Login)
- Spring Security를 사용하여 Form Login을 진행해보는 예제
2.1. 라이브러리 추가
- Spring Security를 사용하기 위해 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
2.2. BCRyptPasswordEncoder 생성
- Spring Security에서는 비밀번호를 암호화 해주는 BCRyptPasswordEncoder가 존재
- 이를 Spring에 등록해놓고 비밀번호 암호화, 비밀번호 체크할 때 사용하면 됨
@Configuration
public class BCryptConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3. SecurityConfig 생성
- Spring Security에 대한 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/security-login/info").authenticated()
.antMatchers("/security-login/admin/**").hasAuthority(UserRole.ADMIN.name())
.anyRequest().permitAll()
.and()
.formLogin()
.usernameParameter("loginId")
.passwordParameter("password")
.loginPage("/security-login/login")
.defaultSuccessUrl("/security-login")
.failureUrl("/security-login/login")
.and()
.logout()
.logoutUrl("/security-login/logout")
.invalidateHttpSession(true).deleteCookies("JSESSIONID");
}
}
- authorizeRequest() : 인증, 인가가 필요한 URL 지정
- antMatchers(URL)
- authenticated() : 해당 URL에 진입하기 위해서 Authentication(인증, 로그인)이 필요함
- hasAuthority() : 해당 URL에 진입하기 위해서 Authorization(인가, ex)권한이 ADMIN인 유저만 진입 가능)이 필요함
- URL에 ** 사용 : ** 위치에 어떤 값이 들어와도 적용시킴
- antMatchers(HttpMethod.POST, URL) : 이런 식으로 특정 HttpMethod만 검사 할 수도 있음
- anyRequest() : 그 외의 모든 URL
- permitAll() : Authentication, Authorization 필요 없이 통과
- formLogin() : Form Login 방식 적용
- usernameParameter() : 로그인할 때 사용되는 ID를 적어줌
- 이 예제에서는 loginId로 로그인하기 때문에 따로 적어줘야 함
- 만약 userName으로 로그인을 한다면 적어주지 않아도 됨
- passwordParameter() : 로그인할 때 사용되는 password를 적어줌
- 이 예제에서는 password로 로그인하기 때문에 따로 적어주지 않아도 됨
- loginPage() : 로그인 페이지 URL
- defaultSuccessURL() : 로그인 성공 시 이동할 URL
- failureURL() : 로그인 실패 시 이동할 URL
- usernameParameter() : 로그인할 때 사용되는 ID를 적어줌
- logout() : 로그아웃에 대한 정보
2.4. PrincipalDetails 생성
- 우리가 직접 로그인 처리를 안해도 되는 대신 지정해줘야 할 정보들
- POST /login 에 대한 요청을 security가 가로채서 로그인 진행해주기 때문에 우리가 직접 @PostMapping("/login") 을 만들지 않아도 됨
- 로그인에 성공하면 Security Session을 생성해 줌 (Key값 : Security ContextHolder)
- Security Session(Authentication(UserDetails)) 이런 식의 구조로 되어있는데 PrincipalDetails에서 UserDetails를 설정해준다고 보면 됨
public class PrincipalDetails implements UserDetails {
private User user;
public PrincipalDetails(User user) {
this.user = user;
}
// 권한 관련 작업을 하기 위한 role return
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collections = new ArrayList<>();
collections.add(() -> {
return user.getRole().name();
});
return collections;
}
// get Password 메서드
@Override
public String getPassword() {
return user.getPassword();
}
// get Username 메서드 (생성한 User은 loginId 사용)
@Override
public String getUsername() {
return user.getLoginId();
}
// 계정이 만료 되었는지 (true: 만료X)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겼는지 (true: 잠기지 않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호가 만료되었는지 (true: 만료X)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정이 활성화(사용가능)인지 (true: 활성화)
@Override
public boolean isEnabled() {
return true;
}
}
2.5. PrincipalDetailsService 생성
- 이전까지는 UserService에서 findUserByLoginId 이런 식으로 User을 불러왔었음
- Security는 User 객체가 아닌 PrincipalDetails가 필요하기 때문에 따로 설정이 필요 => 이 서비스에서는 Security에서 사용할 PrincipalDetails를 return
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByLoginId(username)
.orElseThrow(() -> {
return new UsernameNotFoundException("해당 유저를 찾을 수 없습니다.");
});
return new PrincipalDetails(user);
}
}
2.6. Controller 구현
- Entity, DTO, Repository, Service, 화면은 미리 만들어 놓음
- [Spring Boot] 로그인 구현 방법 정리 참고
- 공통 화면 사용을 위해 모든 요청에 아래 코드 추가
- model.addAttribute("loginType", "security-login");
- model.addAttribute("pageName", "Security 로그인");
@Controller
@RequiredArgsConstructor
@RequestMapping("/security-login")
public class SecurityLoginController {
private final UserService userService;
@GetMapping(value = {"", "/"})
public String home(Model model, Authentication auth) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "Security 로그인");
if(auth != null) {
User loginUser = userService.getLoginUserByLoginId(auth.getName());
if (loginUser != null) {
model.addAttribute("nickname", loginUser.getNickname());
}
}
return "home";
}
@GetMapping("/join")
public String joinPage(Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "Security 로그인");
model.addAttribute("joinRequest", new JoinRequest());
return "join";
}
@PostMapping("/join")
public String join(@Valid @ModelAttribute JoinRequest joinRequest, BindingResult bindingResult, Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "Security 로그인");
// loginId 중복 체크
if(userService.checkLoginIdDuplicate(joinRequest.getLoginId())) {
bindingResult.addError(new FieldError("joinRequest", "loginId", "로그인 아이디가 중복됩니다."));
}
// 닉네임 중복 체크
if(userService.checkNicknameDuplicate(joinRequest.getNickname())) {
bindingResult.addError(new FieldError("joinRequest", "nickname", "닉네임이 중복됩니다."));
}
// password와 passwordCheck가 같은지 체크
if(!joinRequest.getPassword().equals(joinRequest.getPasswordCheck())) {
bindingResult.addError(new FieldError("joinRequest", "passwordCheck", "바밀번호가 일치하지 않습니다."));
}
if(bindingResult.hasErrors()) {
return "join";
}
userService.join2(joinRequest);
return "redirect:/security-login";
}
@GetMapping("/login")
public String loginPage(Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "Security 로그인");
model.addAttribute("loginRequest", new LoginRequest());
return "login";
}
@GetMapping("/info")
public String userInfo(Model model, Authentication auth) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "Security 로그인");
User loginUser = userService.getLoginUserByLoginId(auth.getName());
if(loginUser == null) {
return "redirect:/security-login/login";
}
model.addAttribute("user", loginUser);
return "info";
}
@GetMapping("/admin")
public String adminPage( Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "Security 로그인");
return "admin";
}
}
- 전에 다뤘던 쿠키 로그인, 세션 로그인 과는 달리 @PostMapping("/login"), @GetMapping("/logout"), @GetMapping("/info")에서의 인증(로그인 체크), @GetMapping("/admin")에서의 인가(권한 체크)하는 부분이 없음
- 이는 Spring Security가 로그인, 로그아웃, 인증, 인가를 모두 진행해주기 때문
2.7. 결과
- 메인 페이지 (로그인 하지 않은 상황)
- 메인 페이지 (로그인 한 상황)
- 유저 정보 페이지
- 관리자 페이지 (인가 실패, 쿠키나 세션과는 달리 Spring Security에서 에러를 발생 시켜줌 => Http Error 403(Forbidden)이 발생해야 함)
- 관리자 페이지 (ADMIN 계정으로 로그인 한 경우)
2.8. 추가 (인가를 할 수 있는 다른 방법)
- 위의 예제에서는 SecurityConfig에 URL을 지정해줌으로써 인증, 인가를 진행
- 다른 방법으로 인증, 인가를 진행할 수도 있음
- SecurityConfig에 아래의 어노테이션 추가
@EnableGlobalMethodSecurity(prePostEnabled = true)
- 인가를 진행하고자 하는 URL에 매핑되어있는 메소드에 아래와 같은 어노테이션을 적절히 추가
@PreAuthorize("hasAuthority('ADMIN')")
@PreAuthorize("hasAnyAuthority('ADMIN', 'USER')")
반응형
'Spring Boot > 문법 정리' 카테고리의 다른 글
[Spring Boot] Spring Security를 사용한 Jwt Token 로그인 구현 (1) | 2023.01.07 |
---|---|
[Spring Boot] Spring Security 인증, 인가 실패 처리 - authenticationEntryPoint, accessDeniedHandler (0) | 2023.01.07 |
[Spring Boot] Session을 사용한 로그인 구현 (7) | 2023.01.04 |
[Spring Boot] Cookie를 사용한 로그인 구현 (1) | 2023.01.02 |
[Spring Boot] 로그인 구현 방법 정리 (6) | 2023.01.01 |