[스프링부트] 익명 블로그 V5 - 인증 블로그 V1 - 스프링 시큐리티(Spring Security) 활용

익명 블로스 V5(인증 블로그 V1) - 4 (활용)
이나겸's avatar
Nov 30, 2024
[스프링부트] 익명 블로그 V5 - 인증 블로그 V1 - 스프링 시큐리티(Spring Security) 활용

인증 블로그에 시큐리티 활용 1 - 시큐리티 커스터마이징 후 로그인

[build.gradle 파일]

  • dependencies에서 security 라이브러리 적용 확인
  • 스타터 편집 클릭해서 프로젝트에 적용된 라이브러리들 확인 가능
    • notion image
notion image
 

[SecurityConfig 클래스 - 설정 파일]

  • Spring Security 설정을 정의 - 사용자 인증, 인가, 비밀번호 암호화 설정
  • @Configuration
    • 설정 파일임을 나타냄
    • Spring IoC 컨테이너에 의해 인식, 관리됨
  • PasswordEncoder 빈 설정
    • PasswordEncoder 빈 생성
    • BCryptPasswordEncoder를 사용하여 비밀번호를 안전하게 해쉬화(암호화)
  • SecurityFilterChain 빈 설정
    • http.csrf(c -> c.disable())
      • csrf 보호 기능 비활성화(disable) ⇒ Spring은 기본적으로 활성화돼있음
      • 꼭 필요한 경우에만 하기 (시큐리티 이론 페이지 참고)
    • 요청 인가 설정
      • /s/** 경로에 대한 요청은 인증된 사용자만 접근 가능
      • 그 외 모든 요청은 인증 없이 접근 가능
    • 폼 로그인 설정
      • 커스텀 로그인 페이지의 url을 /login-form으로 설정
      • 로그인 폼의 처리 url을 /login으로 설정
      • 로그인 성공 후 이동할 기본 url을 /로 설정
[밑의 SecurityConfig 클래스의 SecurityFilterChain 빈 설정 로직 일부] // 요청 인가 설정(url에 따른 인증/권한 설정 정의) http.authorizeHttpRequests(r -> // "/s/**"로 시작하는 URL 요청은 인증이 필요 // anyRequest().permitAll() : 나머지 모든 요청은 인증 없이 접근 허용 r.requestMatchers("/s/**").authenticated().anyRequest().permitAll()) // 폼 로그인 설정(로그인 페이지와 로그인 처리 url 설정) .formLogin(f -> f.loginPage("/login-form") // 로그인 폼 경로 설정 .loginProcessingUrl("/login") // 로그인 처리 경로 설정 .defaultSuccessUrl("/")); // 로그인 성공 시 갈 경로(리다이렉트)
[SecurityConfig 클래스 전체] // IoC 컨테이너에 띄우기(어노테이션 달기) @Configuration public class SecurityConfig { // PasswordEncoder 빈 설정 @Bean public PasswordEncoder passwordEncoder() { // BCryptPasswordEncoder를 사용하여 비밀번호를 안전한 해쉬 형식으로 변환(암호화) return new BCryptPasswordEncoder(); } // SecurityFilterChain 빈 설정 // 시큐리티 필터 체인은 필터(걸러냄) // 매개변수로 HttpSecurity 의존성 주입 => 보안 설정 구성 @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { // csrf 보호 기능 비활성화(disable) => 꼭 필요한 경우에만 하기(Spring은 기본적으로 활성화돼있음) http.csrf(c -> c.disable()); // url에 따른 인증/권한 설정 정의 http.authorizeHttpRequests(r -> r.requestMatchers("/s/**").authenticated().anyRequest().permitAll()) .formLogin(f -> f.loginPage("/login-form") // 로그인 폼 경로 설정 .loginProcessingUrl("/login") // 로그인 처리 경로 설정 .defaultSuccessUrl("/")); // 로그인 성공 시 갈 경로(리다이렉트) // 보안 설정 기반으로 SecurityFilterChain 객체 반환 return http.build(); } }
 

[User 클래스]

  • Security는 UserDetails 형태의 객체만을 받기 때문에 implements
  • getAuthorities() 메서드
    • 권한이 여러 개 일수도 있기 때문에 List 반환
  • @getter 어노테이션 달아놨는데 getter 굳이 또 작성한 이유
    • 아이디랑 패스워드 필드명이 username, password 이외에 다른 것일 수 있기 때문
    • 아이디랑 패스워드 필드명이 username, password일 경우 당연히 추가로 getter 작성하지 않아도 됨
@AllArgsConstructor // 풀 생성자 @NoArgsConstructor // 디폴트(빈) 생성자 @Table(name = "user_tb") // 테이블명 @Getter @Entity // 엔티티, 테이블 생성, 매핑 가능 public class User implements UserDetails { // 시큐리티는 UserDetails 형태의 객체만을 받기 때문에 implements @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // Autoincrement private Integer id; // Null일것도 감안해서 Integer @Column(unique = true, nullable = false) // 컬럼제약조건 unique(index 알아서 만들어질것), not null private String username; // 유저 아이디는 username으로 약속되어있음, id 아님 @Column(nullable = false) private String password; @Column(nullable = false) private String email; @CreationTimestamp private Timestamp createdAt; // @getter 달아놨는데 getter 굳이 또 작성한 이유 // 아이디랑 패스워드 필드명을 username, password 이외에 다른 걸 해놨을 수 도 있기 때문 public String getUsername() { return username; } public String getPassword() { return password; } // 사용자의 권한 반환 (사용자가 특정 리소스에 접근할 권한) // role -> admin, manager, guest 같은 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(); } // 계정 만료되지 않았는지 확인 @Override public boolean isAccountNonExpired() { return true; } // 계정이 잠겨 있지 않은지 확인 @Override public boolean isAccountNonLocked() { return true; } // 인증 자격 증명(비밀번호 같은)이 만료되지 않았는지 확인 @Override public boolean isCredentialsNonExpired() { return true; } // 계정이 활성화 상태인지 확인 @Override public boolean isEnabled() { return true; } }
 

[UserRequest 클래스 - JoinDTO 클래스]

  • 회원가입을 위한 static 클래스 (DTO)
  • public User toEntity(PasswordEncoder passwordEncoder) 메서드
    • DB에 저장하기 위해서는 User 객체가 필요하기 때문에 toEntity 메서드를 통해 자신의 값을 User 타입으로 변경시킨 엔티티를 반환
    • 비밀번호를 Hash코드로 변경시킨 값(암호화)을 매개변수 PasswordEncoder로 전달
public class UserRequest { // DTO는 불필요한 데이터 빼고 필요한 데이터만 @Data public class JoinDTO { private String username; private String password; private String email; // User를 초기화 할 때, 비밀번호는 기존 값을 hash화하여 초기화 한 뒤 반환 public User toEntity(PasswordEncoder passwordEncoder) { // PasswordEncoder : 패스워드 해쉬화(암호화) String encPassword = passwordEncoder.encode(password); User user = new User(null, username, encPassword, email, null); System.out.println(encPassword); return user; } } }
 

[UserController 클래스]

  • 회원가입 메서드 추가
  • SecurityConfig가 로그인을 대신하고 있기 때문에 로그인 메서드 PostMapping 삭제
@RequiredArgsConstructor @Controller public class UserController { private final UserService userService; // IOC 컨테이너에서 세션 꺼내옴 (세션은 싱글턴이라서 하나만 있으니까 IOC가 관리해줌) private final HttpSession session; // 회원가입 @PostMapping("/join") public String join(UserRequest.JoinDTO joinDTO) { userService.회원가입(joinDTO); return "redirect:/login-form"; } @GetMapping("/login-form") public String loginForm() { return "user/login-form"; } }
 

[UserService 클래스]

  • 회원가입 메서드 추가
    • UserRepository의 save 메서드는 User 클래스를 받기 때문에 toEntity 타입을 변환시켜서 전달
  • loadUserByUsername 메서드
    • UserService 클래스가 implements한 UserDetailsService 인터페이스에서 정의된 메서드 오버라이드
    • 입력 파라미터 username은 사용자의 아이디로 사용
    • UserDetails 인터페이스를 구현한 객체 반환
    • 사용자를 찾지 못했을 때는 throws UsernameNotFoundException로 예외 처리
  • userRepository.findByUsername(username)
    • userRepository는 사용자 정보를 데이터베이스에서 조회하는 역할
    • userRepository 객체를 사용하여 데이터베이스에서 주어진 username을 가진 사용자 정보를 검색하고 User 객체 반환
    • 반환값은 UserDetails 인터페이스를 구현한것으로 Spring Security에서 인증 작업 처리하는데 사용
@RequiredArgsConstructor @Service public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; // DB에 저장할 때 비밀번호를 hash로 변경하여 넣을 수 있도록 인코더를 매개 변수로 전달 @Transactional public void 회원가입(UserRequest.JoinDTO joinDTO) { userRepository.save(joinDTO.toEntity(passwordEncoder)); } // post 요청 // /login 일때 호출됨 // key 값 -> username, password // Content-Type -> x-www-form-urlencoded @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); return user; }
 

[BoardController 클래스]

  • @GetMapping("/")
    • / 경로 요청하면 콘솔에 username(아이디) 출력하고 index 페이지(메인 페이지) 이동
  • @GetMapping("/s/board/save-form")
    • SecurityConfig 클래스의 SecurityFilterChain 빈 설정의 url에 따른 인증/권한 설정 정의에서 /s/**경로에 인증된 User만 접근 가능하도록 설정해놨기 때문에 글쓰기 화면 GetMapping의 경로 수정
  • @AuthenticationPrincipal 어노테이션
    • Spring Security와 통합되어 현재 인증된(로그인된) 사용자의 정보를 가져옴
    • User 객체 사용헤사 인증된 사용자의 정보 주입
    • 인증된 사용자의 정보만 들고오기 때문에 로그인을 하지 않고 메서드에 접근하면 null 예외 발생할 수 있음 ⇒ null의 getUsername() 메서드 출력을 할 수 없기 때문
@RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; @GetMapping("/") public String index() { System.out.println(user.getUsername()); return "index"; } @GetMapping("/s/board/save-form") // /s/**경로에 인증된 User만 접근 가능 public String saveForm(@AuthenticationPrincipal User user) { // @AuthenticationPrincipal : 현재 인증된(로그인된) 사용자의 정보를 // 쉽게 가져올 수 있도록 해줌 System.out.println("로그인 : " + user.getUsername()); return "board/save-form"; } }
 

회원가입 후 로그인 테스트

[Postman으로 회원가입 테스트]

  • 회원가입 폼을 만들어두지 않아서 Postman으로 테스트 진행
  • /join 경로로 Post 요청
  • 아이디 love, 패스워드 1234, 이메일 love@nate.com
    • notion image

[웹실행 후 로그인]

  • Postman으로 회원가입 테스트 해놓은 아이디와 비밀번호로 로그인
  • 경로를 /s/~~ 로 입력 ⇒ /s/로 시작하는 경로는 모두 인증을 필요로 하게 설정했기 때문
notion image
  • 로그인 성공 시 메인 페이지 이동
    • notion image

[BoardController의 index 메서드로 테스트]

  • 위의 BoardController 코드 참고
  • 로그인이 되었다면 예외 발생 없이 콘솔에 로그인 된 유저의 username을 출력
  • 인증되지 않은 유저가 해당 경로에 접근하게 되면 null 예외를 발생시키도록 작성한 상태
  • 로그인 하고 index 메서드 접근했을 때의 콘솔 출력
    • notion image
  • 로그인 하지 않고 index 메서드 접근했을 때의 콘솔 출력
    • null 예외 발생
      • notion image
 

인증 블로그에 시큐리티 활용 2 - 시큐리티커스터마이징으로 권한 설정

[User 클래스]

  • 권한 설정 필터링
    • role 필드 생성
    • 컬럼값은 ROLE_ADMIN, ROLE_MANAGER과 같이 ROLE_을 붙여야 함
    • 회원가입 시 기본값은 ROLE_USER로 설정
  • getAuthorities() 메서드
    • 사용자가 특정 리소스에 접근할 권한 추가
    • 유저가 권한을 여러 개 가질 수 있어서 List로 반환
@AllArgsConstructor // 풀 생성자 @NoArgsConstructor // 디폴트(빈) 생성자 @Table(name = "user_tb") // 테이블명 @Getter @Entity // 엔티티, 테이블 생성, 매핑 가능 public class User implements UserDetails { // 시큐리티는 UserDetails 형태의 객체만을 받기 때문에 implements @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // Autoincrement private Integer id; // Null일것도 감안해서 Integer @Column(unique = true, nullable = false) // 컬럼제약조건 unique(index 알아서 만들어질것), not null private String username; // 유저 아이디는 username으로 약속되어있음, id 아님 @Column(nullable = false) private String password; @Column(nullable = false) private String email; @CreationTimestamp private Timestamp createdAt; // 권한 설정 필터링 // ROLE_ADMIN,ROLE_MANAGER @Column(nullable = false, columnDefinition = "varchar(255) default 'ROLE_USER'") private String role; // 회원가입되면 기본 ROLE_USER // @getter 달아놨는데 getter 굳이 또 작성한 이유 // 아이디랑 패스워드 필드명을 username, password 이외에 다른 걸 해놨을 수 도 있기 때문 public String getUsername() { return username; } public String getPassword() { return password; } // 사용자의 권한 반환 (사용자가 특정 리소스에 접근할 권한) // role -> admin, manager, guest 같은 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(); // 권한 설정 필터링 코드 String[] roles = role.split(","); List<GrantedAuthority> gs = new ArrayList<GrantedAuthority>(); for (String roleName : roles) { GrantedAuthority g = new SimpleGrantedAuthority("ROlE_USER"); gs.add(g); } return gs; } // 계정 만료되지 않았는지 확인 @Override public boolean isAccountNonExpired() { return true; } // 계정이 잠겨 있지 않은지 확인 @Override public boolean isAccountNonLocked() { return true; } // 인증 자격 증명(비밀번호 같은)이 만료되지 않았는지 확인 @Override public boolean isCredentialsNonExpired() { return true; } // 계정이 활성화 상태인지 확인 @Override public boolean isEnabled() { return true; } }
 

[SecurityConfig 클래스 - 설정 클래스]

  • SecurityFilterChain 빈 설정으로 권한에 따라 접근 가능한 URL 설정
    • /s/로 시작하는 URL은 USER와 ADMIN 권한을 가진 유저만 접근 가능
    • /admin/으로 시작하는 URL은 ADMIN 권한을 가진 유저만 접근 가능
// IoC 컨테이너에 띄우기(어노테이션 달기) @Configuration public class SecurityConfig { // PasswordEncoder 빈 설정 @Bean public PasswordEncoder passwordEncoder() { // BCryptPasswordEncoder를 사용하여 비밀번호를 안전한 해쉬 형식으로 변환(암호화) return new BCryptPasswordEncoder(); } // SecurityFilterChain 빈 설정 // 시큐리티 필터 체인은 필터(걸러냄) // 매개변수로 HttpSecurity 의존성 주입 => 보안 설정 구성 @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { // csrf 보호 기능 비활성화(disable) => 꼭 필요한 경우에만 하기(Spring은 기본적으로 활성화돼있음) http.csrf(c -> c.disable()); // url에 따른 인증/권한 설정 정의 http.authorizeHttpRequests(r -> r.requestMatchers("/s/**").hasAnyRole("USER", "ADMIN") .requestMatchers("/admin/**").hasAnyRole("ADMIN") .anyRequest().permitAll()) .formLogin(f -> f.loginPage("/login-form") .loginProcessingUrl("/login") .defaultSuccessUrl("/")); // 보안 설정 기반으로 SecurityFilterChain 객체 반환 return http.build(); } }
 

인증 블로그에 시큐리티 활용 3 - 시큐리티 설정 후 View에 User 정보 전달

[SecurityConfig 클래스 - 설정 클래스]

  • SecurityFilterChain configure
    • 시큐리티 세션이 아닌 HttpSession에 User 정보를 sessionUser 저장하는 필터
    • 필터 설정에서 successHandler 설정하기
      • 로그인 성공 시 추가 작업 설정 가능
      • authentication.getPrincipal() ⇒ authentication에서 UserDetils 객체 받을 수 있음
// IoC 컨테이너에 띄우기(어노테이션 달기) @Configuration public class SecurityConfig { // PasswordEncoder 빈 설정 @Bean public PasswordEncoder passwordEncoder() { // BCryptPasswordEncoder를 사용하여 비밀번호를 안전한 해쉬 형식으로 변환(암호화) return new BCryptPasswordEncoder(); } // SecurityFilterChain 빈 설정 // 시큐리티 필터 체인은 필터(걸러냄) // 매개변수로 HttpSecurity 의존성 주입 => 보안 설정 구성 @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { // csrf 보호 기능 비활성화(disable) => 꼭 필요한 경우에만 하기(Spring은 기본적으로 활성화돼있음) http.csrf(c -> c.disable()); // url에 따른 인증/권한 설정 정의 http.authorizeHttpRequests( r -> r.requestMatchers("/s/**").authenticated().anyRequest().permitAll()) .formLogin(f -> f.loginPage("/login-form") .loginProcessingUrl("/login") // successHandler 설정 .successHandler((request, response, authentication) -> { User user = (User) authentication.getPrincipal(); . HttpSession session = request.getSession(); session.setAttribute("sessionUser", user); response.sendRedirect("/"); })); // 보안 설정 기반으로 SecurityFilterChain 객체 반환 return http.build(); } }
 

[Application.properties 파일]

  • mustacje session
    • 세션과 request에 접근 허용
# 4. mustache session (세션과 request에 접근 허용) spring.mustache.servlet.expose-request-attributes=true spring.mustache.servlet.expose-session-attributes=true
 

[save-form.mustache 파일]

  • {{sessionUser.username}}
    • 세션에 저장된 User의 username 정보를 View에서 바로 접근 가능
{{> layout/header}} {{sessionUser.username}} <!-- 세션에 저장된 유저 정보를 View에서 바로 접근 가능 --> <section> <form action="/board/save" method="post" enctype="application/x-www-form-urlencoded"> <input type="text" name="title" placeholder="제목"><br> <input type="text" name="content" placeholder="내용"><br> <button type="submit">글쓰기</button> </form> </section> </body> </html>
Share article

Nakyeom's Study