커스텀 로그인 적용하기
프로젝트를 만들고 실행을 하면 다음과 같이 로그인 창이 뜬다.
의존성은 위 7개를 추가해주었다.
스프링 시큐리티를 적용하면 자동으로 로그인창이 생긴다.
user와 프로젝트 시작시 나오는 비밀번호를 통해 로그인을 할 수 있다.
커스텀 로그인창을 적용시켜보자
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean //비밀번호 암호화를 할때 사용
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/","/login","/logout","/loginSuccess","/join","/joinSuccess").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN","USER")
.anyRequest().authenticated() // 나머지 경로에 대해서 로그인한 사용자는 접근 허용
// .anyRequest().denyAll() // 모두 접근 불가
);
http
.formLogin((auth) -> auth
.loginPage("/login")
.loginProcessingUrl("/loginSuccess")
.permitAll()
);
http
.csrf((auth) -> auth
.disable()
);
return http.build();
}
}
SecurityConfig 클래스를 만든다,
SecurityFilterChain을 반환하는 filterChain 메서드를 만들고 http를 빌드해서 리턴한다.
동작 순서는 상단 부터 적용된다. (순서 유의하자)
만약 아래와 같은 코드가 있을경우
.requestMatchers("/").permitAll()
.requestMatchers("/").denyAll()
첫줄에서 이미 permitAll()을 했으므로 denyAll()은 무시된다.
"/","/login","/logout","/loginSuccess","/join","/joinSuccess"에 대한 접근을 permitAll()을 한뒤 (join에 대한 내용은 뒤에서 회원가입 구현을 위해 미리 추가해 놓자)
http
.formLogin((auth) -> auth
.loginPage("/login")
.loginProcessingUrl("/loginSuccess")
.permitAll()
);
커스텀 로그인 페이지를 설정해준다.
loginPage의 접근 url과 성공시 url를 설정해준다.
@Controller
public class LoginController {
@GetMapping("/login")
public String loginP(){
return "login";
}
}
LoginController를 통해 해당 커스텀 로그인 페이지를 반환해준다.
커스텀 로그인 페이지가 적용되었다.
<form action="/loginSuccess" method="post" name="loginForm">
<input id="username" type="text" name="username" placeholder="id"/>
<input id="password" type="password" name="password" placeholder="password"/>
<input type="submit" value="login"/>
</form>
로그인 form 전송 url을 loginSucess로 설정해준다.
회원가입
<p>main page</p>
<hr>
<button onclick="location.href='/login'">로그인</button>
<button onclick="location.href='/join'">회원가입</button>
메인 페이지에 로그인과 회원가입 버튼을 만들어줬다.
@Controller
@RequiredArgsConstructor
public class JoinController {
private final JoinService joinService;
@GetMapping("/join")
public String joinP(){
return "/join";
}
@PostMapping("/joinSuccess")
public String joinSuccess(JoinDTO joinDTO){
joinService.save(joinDTO);
return "redirect:/login";
}
}
@Service
@RequiredArgsConstructor
public class JoinService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public void save(JoinDTO joinDTO){
if(userRepository.existsByUsername(joinDTO.getUsername())){
return;
}
UserEntity entity = UserEntity.builder()
.username(joinDTO.getUsername())
.password(bCryptPasswordEncoder.encode(joinDTO.getPassword()))
.role("ROLE_USER")
.build();
userRepository.save(entity);
}
}
@Repository
public interface UserRepository extends JpaRepository<UserEntity,Long> {
boolean existsByUsername(String username);
UserEntity findByUsername(String username);
}
Spring Security는 role 값을 넣어줄때 접두사 ROLE_을 붙여줘야한다.
ADMIN 페이지
SecurityConfig 클래스에서 .requestMatchers("/admin").hasRole("ADMIN")를 통해 admin 페이지는 role이 ADMIN인 경우만 접근이 가능하도록 설정했다.
role이 ROLE_ADMIN인 경우에만 접근이 가능하다.
스프링 시큐리티는 역할을 부여할떄 접두사 ROLE_을 적어줘야한다.
CSRF 활성화
/**
* 개발환경에서는 csrf 토큰 비활성화 해도 되지만
* 배포를 할때는 csrf 공격을 방지하기 위해 활성화 해야한다.
* 활성화를 하면 post 전송을 하는 form 태크 내부에
* <input type="hidden" name="_csrf" th:value="${_csrf.token}"> 를 추가해줘야 한다.
* default 값은 enable이다.
*/
// http
// .csrf((auth) -> auth
// .disable()
// );
csrf disable을 지워서 csrf를 활성화해주자.
활성화를 하면 post 요청의 경우 csrf 토큰을 같이 보내야한다.
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
모든 post 요청에 csrf 토큰을 추가해주자
로그아웃 구현
스프링 시큐리티의 로그아웃은 post 요청만 받아드린다. 우리는 csrf 토큰을 활성화 했으므로 로그아웃 요청을 보낼때 csrf 토큰을 추가해서 post 요청으로 보내야 한다.
http
.logout((auth) -> auth
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.permitAll()
);
SecurityConfig 클래스의 filterChain메서드에 위 로그아웃 로직을 추가해주고
<form action="/logout" method="post" name="logout">
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
<input type="submit" value="로그아웃"/>
</form>
csrf 토큰을 넣어서 post 요청으로 로그아웃 요청을 보내면 된다.
<button onclick="location.href='/logout'">로그아웃</button>
get 요청은 csrf 토큰이 없어도 되므로 토큰없이 로그아웃 요청을 보낼수 있지만
스프링 시큐리티는 로그아웃을 post 요청만 처리하기 때문에 get 요청으로 로그아웃을 처리하고 싶으면 컨트롤러를 만들어서 구현해야한다.
@GetMapping("/logout")
public String logout() {
String id = SecurityContextHolder.getContext().getAuthentication().getName();
if(!id.equals("anonymousUser")){
log.info("[logout] username: {}",id);
SecurityContextHolder.getContext().setAuthentication(null);
}
return "redirect:/";
}
UserDetailsService와 UserDetails
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final UserEntity userEntity;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userData = userRepository.findByUsername(username);
// 아이디가 존재
if(userData != null){
return new CustomUserDetails(userData);
}
return null;
}
}
UserDetailsService와 UserDetails에 대해서는 다음 포스팅에 더 자세히 다루었다.
https://bangbaeking.tistory.com/108
[Spring] 스프링 시큐리티 정리
스프링 시큐리티란? Spring Security는 Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다. Spring Security는 '인증'과 '인가'에 대한 부분을 Filter 에서 처
bangbaeking.tistory.com
@Controller
public class MainController {
@GetMapping("/")
public String mainP(Model model){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String id = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
model.addAttribute("id",id);
model.addAttribute("role",role);
return "main";
}
}
id와 role을 구해서 model에 담아서 main페이지 뷰로 전달해서 출력해보자
'Java , Spring > Spring' 카테고리의 다른 글
Gradle, Maven, Gradle Wrapper (0) | 2024.04.11 |
---|---|
[Spring] 스프링 시큐리티 정리 (2) | 2024.02.14 |
[Spring] 인증, 인가 - (쿠키, 세션, 토큰) (1) | 2024.02.08 |
[Spring] 선언적 트랜잭션 @Transactional (1) | 2024.01.22 |
[Spring] Spring Security 를 이용해서 비밀번호 암호화 하기 (0) | 2023.09.26 |
댓글