인증 블로그에 시큐리티 활용 1 - 시큐리티 커스터마이징 후 로그인
[build.gradle 파일]
- dependencies에서 security 라이브러리 적용 확인
- 스타터 편집 클릭해서 프로젝트에 적용된 라이브러리들 확인 가능


[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

[웹실행 후 로그인]
- Postman으로 회원가입 테스트 해놓은 아이디와 비밀번호로 로그인
- 경로를 /s/~~ 로 입력 ⇒ /s/로 시작하는 경로는 모두 인증을 필요로 하게 설정했기 때문

- 로그인 성공 시 메인 페이지 이동

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

- 로그인 하지 않고 index 메서드 접근했을 때의 콘솔 출력
- null 예외 발생

인증 블로그에 시큐리티 활용 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