- [Spring] 스프링 심화 - Spring Security 1
- 적용하기
- Spring Security의 요소 확인
- [Spring] 스프링 심화 - Spring Security 2
- Default Form Login 방식 사용
- UserDetails, UserDetailsService custom
- 비밀번호 암호화 이해 및 적용
- [Spring] 스프링 심화 - Spring Security 3
- CustomSecurityFilter 적용
- @AuthenticationPrincipal
- @Secured
- ExceptionHandling - 401, 403
적용과정
# WebSecurityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf().disable();
http.authorizeRequests().antMatchers("/api/user/**").permitAll()
.anyRequest().authenticated();
// Custom 로그인 페이지 사용
http.formLogin().loginPage("/api/user/login-page").permitAll();
// Custom Filter 등록하기
// + UsernamePWAuthFilter 전에 CustomSecurityFilter 등록
http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
+) addFilterBefore()
CustomSecurityFilter를 먼저 실행하고 인증객체를 만들어 context에 추가를 하면
인증이 완료가 되기때문에 UsernamePWAuthFilter가 수행되더라도 인증이되어서 다음필터로 넘아가지고
controller까지 요청 가능
# CustomSecurityFilter
@RequiredArgsConstructor
public class CustomSecurityFilter extends OncePerRequestFilter {
private final UserDetailsServiceImpl userDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
if(username != null && password != null && (request.getRequestURI().equals("/api/user/login") || request.getRequestURI().equals("/api/test-secured"))){
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 비밀번호 확인
if(!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
}
// 인증 객체 생성 및 등록
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 위에서 검증이 끝나서 필요없음
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 다음 필터 UsernamePasswordAuthenticationFilter로
filterChain.doFilter(request,response);
}
}
CustomSecurityFilter
JwtAuthFilter
JwtAuthFilter
: 로그인 성공 후 반환된 토큰이, 인증이 필요한 API 요청과 같이 들어왔을 때 토큰을 검증하여 사용자를 인증처리 해줌
=> 토큰방식을 적용하게 되면 사용자의 로그인, 회원가입 요청은 Filter 인증되지 않게 permitAll()
처리하여 실제 검증 및 처리는 service
에서 수행하도록 함
=> 그 외 인증이 필요한 요청에서는 로그인을 통해 발급받은 토큰을 같이 보내 Filter에서 토큰을 검증하고 인증처리 함
# UserController
@PostMapping("/login")
public String login(@AuthenticationPrincipal UserDetails userDetails) {
System.out.println("*********************************************************");
System.out.println("UserController.login");
System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
System.out.println("*********************************************************");
return "redirect:/api/user/login-page";
}
// 여기선 principal 부분에 UserDetails를 넣어줬기 때문에 UserDetails를 파라미터로 받아올 수 있음
적용과정
# WebSecurityConfig - 애노테이션 추가, 접근 제한 페이지 이동 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화!!
public class WebSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf().disable();
http.authorizeRequests().antMatchers("/api/user/**").permitAll()
.anyRequest().authenticated();
// Custom 로그인 페이지 사용
http.formLogin().loginPage("/api/user/login-page").permitAll();
// Custom Filter 등록하기
http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), UsernamePasswordAuthenticationFilter.class);
// 접근 제한 페이지 이동 설정
http.exceptionHandling().accessDeniedPage("/api/user/forbidden");
return http.build();
}
}
# UserRoleEnum
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
# UserServiceImpl
public class UserDetailsImpl implements UserDetails {
// ...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(adminAuthority);
return authorities;
}
}
# TestController - @Secured 추가
@Controller
@RequestMapping("/api")
public class TestController {
@Secured(value = UserRoleEnum.Authority.ADMIN)
@PostMapping("/test-secured")
public String securedTest(@AuthenticationPrincipal UserDetails userDetails) {
System.out.println("*********************************************************");
System.out.println("UserController.securedTest");
System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
System.out.println("*********************************************************");
return "redirect:/api/user/login-page";
}
}
적용과정
# WebSecurityConfig
// 401 Error 처리, Authorization 즉, 인증과정에서 실패할 시 처리
http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
// 403 Error 처리, 인증과는 별개로 추가적인 권한이 충족되지 않는 경우
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
# SecurityExceptionDto
@Getter
@NoArgsConstructor
public class SecurityExceptionDto {
private int statusCode;
private String msg;
public SecurityExceptionDto(int statusCode, String msg) {
this.statusCode = statusCode;
this.msg = msg;
}
}
# CustomAccessDeniedHandler
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException{
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}
# CustomAuthenticationEntryPoint
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}