2025. 2. 17. 21:46ㆍSpringboot/security
현재 SecurityConfig 부분.
@Configuration //Spring에서 설정 파일임을 나타냄.
@EnableWebSecurity
//Spring Security의 보안 설정을 활성화하는 애너테이션(annotation)
// Spring Boot 2에서는 필수였지만, Spring Boot 3에서는 생략가능. but security설정임을 명시
public class SecurityConfig {
@Bean //security는 password를 DB에 저장할 때 인코딩해서 저장. 비교할 때는 디코딩 후 비교.
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/h2-console/**").permitAll() // H2 콘솔 접근 허용
)
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")) // H2 콘솔 CSRF 비활성화
.headers(headers -> headers.frameOptions(frame -> frame.disable())); // H2 콘솔을 iframe에서 허용
http.authorizeHttpRequests((auth) -> auth
.requestMatchers("/join/**", "/login").permitAll() // 이 url들은 로그인 안해도됨
.requestMatchers("/admin").hasAuthority("ADMIN") //ADMIN 권한 사용자만
.requestMatchers("/my/**").hasAnyAuthority("ADMIN", "USER") // ADMIN, USER 권한 중 하나 가지면 ㅇㅋ
.anyRequest().authenticated() // 그 외 요청들은 로그인해야만..
);
http.formLogin((auth) -> auth.loginPage("/login") // login페이지 URL 지정. @RequestMapping("/login")을 만들어야한다. => login.html
.loginProcessingUrl("/loginProc") // login.html에서 form태그의 action URL이 /loginProc여야한다.
// @RequestMapping("/loginProc")는 없다. login과정은 security가 하기때문.
.defaultSuccessUrl("/") // 로그인 성공 후 redirect 되는 URL
.permitAll()
);
http.logout((auth) -> auth.logoutUrl("/logout") // /logout으로 요청하면 logout이 된다. @RM은 없다. 로그아웃도 security가 한다.
.logoutSuccessUrl("/")); // 로그아웃 성공 후 /로 redirect
http.csrf((auth) -> auth.disable()); //보안관련설정. 자세한 설명은 csrf 따로.
return http.build();
}
}
로그인 페이지지정
config 로그인페이지 지정 코드
http.formLogin((auth) -> auth.loginPage("/login") // login페이지 URL 지정. @RequestMapping("/login")을 만들어야한다. => login.html
.loginProcessingUrl("/loginProc") // login.html에서 form태그의 action URL이 /loginProc여야한다.
// @RequestMapping("/loginProc")는 없다. login과정은 security가 하기때문.
.defaultSuccessUrl("/") //로그인 성공 후 redirect 되는 url
.permitAll()
);
LoginController
@Controller
public class LoginController {
@GetMapping("/login")
public String login(){
return "login";
}
}
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
로그인페이지입니다.
<form action="/loginProc" method="post" name="loginForm"> <!-- securityConfig의 loginProcessingUrl("/loginProc") -->
<input id="username" type="text" name="username" placeholder="id(username)"/> <!-- name이 반드시 username이어야한다. -->
<input id="password" type="password" name="password" placeholder="password"/> <!-- name이 반드시 password이어야한다. -->
<button type="submit" >로그인</button>
</form>
</body>
</html>
로그인 과정
위의 로그인페이지에서 id(username) /password를 입력하고 로그인버튼을 누른다.
/loginProc로 요청이 오면 security가 자동으로 로그인과정을 진행한다.
( securityConfig에 loginProceessingUrl("/loginProc") )
security session방식은 이 과정에서 security가 로그인하는 과정을 이해하는게 핵심이다.
로그인 시 다음과 같은 과정을 거친다.
참고 :공식 문서 그림
우리의 목적은 오직 로그인페이지에서 입력한 username, password의 값과
DB에 있는 userEntity정보로 로그인 여부를 결정하게 하는
+ DB에 있는 UserEntity정보로부터 로그인정보를 저장할 객체를 session에 저장하는것이다.
하지만 SecurityConfig를 보면 DB와 관련된 설정은 보이지 않는다.
security는 이 2가지 목적을 개발자가 작성하도록 한다.
UserDetailService과 UserDetails는 interface로서 이를 구현한 클래스를 만들어야한다.
나머지부분은 신경쓰지 않아도 자동으로 동작한다.
CustomUserDetails
public class CustomUserDetails implements UserDetails {
private UserEntity userEntity; //엔티티필드. 사실 UserDTO필드를 만드는게 맞음.
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
for(String role : userEntity.getRoles()){
collection.add( ()-> role ); //new GrantedAuthority
}
return collection;
}
//isAccountNonExpired , isAccountNonLocked , isCredentialsNonExpired , isEnabled
//spring boot 2 에서는 필수로 구현했지만, 3부터는 userDetials가 default 메소드
}
CustomUserDetailService
@Service //UserDetailsService의 구현체 빈등록
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userData = userRepository.findByUsername(username);
if (userData != null) {
return new CustomUserDetails(userData);
}
return null;
}
}
다시 로그인과정을 보자.
이 후 로그인 성공을 한다면 로그인 정보를 저장하게 된다.
로그인을 성공했다면 로그인정보를 저장하는 것도 security가 알아서 저장한다.
우린 단순히 이 로그인정보를 이제 Controller(+service 등)에서 사용하면 된다.
※저장 과정 살펴보기. SecurityContextHolder, SecirtyContext, Authentication 이란.

SecurityContextHolder는 SecurityContext들을 저장하는 싱글톤(Singleton) 클래스입니다.
내부적으로 ThreadLocal을 사용하여 각 요청(Request)마다 인증 정보를 저장 및 관리합니다.
SecurityContext는 매 요청마다 생성되고 요청이 끝난 후 사라집니다.
Autehnticaion(인증정보= 사용자정보+자격증명+권한+ 기타 등등 )는 SecurityContext에 저장됩니다.
SecurityContext는 요청 후 사라지지만, 사라지기전에
Session에 SecurityContext가 저장됩니다. (복사)
로그인정보를 Controller에서 사용하는 것은
session에 담긴 SecurityContext 안의 Authentcation( 또는 그 안의 UserDetails외 여러가지)를 말합니다.
Principal : 사용자정보 우리가 만든 CustomUserDetails(UserDetails).
Security 내부적으로는 Object타입.
왜 UserDetails타입이 아니라 Object타입인지는 Oauth2에서 설명.
(UserDetails) authentication.getPrincipal()
Credentials : 자격증명인증판단에 사용되는 정보
사용자를 인증하는 데 사용되는 값 (비밀번호, 토큰, OTP 등)
Autrhoties : 권한들
로그인정보 사용하기
SecurityContextPersistenceFilter에서 로그인정보를 sesssion에 저장했다.
spring security에서 로그인정보란 Autehnticaion객체를 말하고 Autehntcation객체안에
개발자가 만든 UserDetails 객체가 들어있다고 보면된다.
(실제로 session에 저장되는건 Authentication을 갖고있는 SecurityContext지만
session에 로그인정보=Autehntcation이 저장됬다고 생각해도 OK)
그래서 이 로그인정보를 Contrller 등에서 사용하는 방법은
Autehntciation을 직접 사용해도되고, 그 안의 담긴 여러가지 정보들을 사용해도 돼서 여러가지이다.
하지만 사실 로그인정보를 사용할 때 개발자가 직접 로그인정보라고 생각한 UserDetails를 사용하는게 일반적이다.
MainController
@Controller
public class MainController {
//로그인 후
@RequestMapping("/my/info")
@ResponseBody
public String myInfo(@AuthenticationPrincipal CustomUserDetails userDetails, //개발자가 직접 만든 로그인 정보. 이거 사용하세요!
Principal principal, // username만 가져올 수 있음.
Authentication authentication, //spring securiy객체가 실제 사용하는 로그인정보. userDetails를 포함 + 좀 더 다양한정보
HttpSession session,
HttpServletRequest request
){
StringBuilder sb=new StringBuilder();
sb.append( "권한 : "+ userDetails.getAuthorities().iterator().next().getAuthority() +"<br>" ); //첫번째권한.
sb.append( "password : "+ userDetails.getPassword() +"<br> ");
sb.append( "username : "+ userDetails.getUsername() + "<br>" );
//DB에 있는 정보들을 더 원한다면 CusomuserDetails에서 getUserEntity() 메소드를 만들거나 하자.
//SecurityContextHolder.getContext().getAuthentication(); 위의 Authentcaton과 동일
//request.getUserPrincipal() 위의 Principal과 동일
//session.getAttribute("SPRING_SECURITY_CONTEXT"). SecurityContextHolder.getContext()랑 동일
return sb.toString();
}
@RequestMapping("/admin")
@ResponseBody
public String admin(){
return "권한이없어서 여기에 도달 할 수 없다.";
}
}
※ 참고
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
라이브러리 추가 후 thymeleaf 에서 security 관련된 태그를 사용할 수 있다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<!-- thymeleaf의 th 태그와 security의 sec 태그 사용가능 -->
<head>
<meta charset="UTF-8">
<title>Spring Security Test</title>
</head>
<body>
<h1>Spring Security와 Thymeleaf 테스트</h1>
<!-- 로그인한 사용자 이름 표시 -->
<!--Controller에서 Autehntcation authentication의 getName 값. -->
<p>현재 사용자: <span sec:authentication="name"></span></p>
<!-- @AutehntcationPrincipal CustomUserDetails userDetails 의 값을 사용하고 싶다면 Controller에서 model에 직접 담아야 함. -->
<!-- 역할(ROLE) 출력 -->
<p>권한: <span sec:authentication="authorities"></span></p>
<!-- 로그인 상태 확인 -->
<div sec:authorize="isAuthenticated()">
<p>로그인한 사용자만 볼 수 있습니다.</p>
<form th:action="@{/logout}" method="post">
<button type="submit">로그아웃</button>
</form>
</div>
<!-- 비로그인 상태 확인 -->
<div sec:authorize="isAnonymous()">
<p>로그인하지 않은 사용자입니다.</p>
<a th:href="@{/login}">로그인</a>
</div>
<!-- 특정 권한 확인 -->
<div sec:authorize="hasAuthority('ADMIN')">
<p>관리자 전용 페이지</p>
</div>
</body>
</html>
실행결과
회원가입 후