spring security formLogin 구현

2024. 3. 26. 17:45·개발/spring boot
728x90

개발 환경

java 8, spring boot 2.4.4

 

1. build.gradle

implementation 'org.springframework.boot:spring-boot-starter-security'
// 타임리프에서 스프링시큐리티의 문법이나 형식을 지원하는 확장팩 라이브러리
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

 

2. configuration 설정

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final FormAuthenticationProvider formAuthenticationProvider;
    private final FormAuthenticationFailureHandler formAuthenticationFailureHandler;
    private final FormAuthenticationSuccessHandler formAuthenticationSuccessHandler;

    public AuthenticationManager authenticationManager() {
        List<AuthenticationProvider> authProviderList = new ArrayList<>();
        authProviderList.add(formAuthenticationProvider);
        ProviderManager providerManager = new ProviderManager(authProviderList);
        return providerManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/login", "/login-error", "/signin", "/login*").permitAll() // 로그인 페이지와 에러 페이지는 접근 허용
                .antMatchers("/logout").hasRole("ADMINISTRATOR")
                .anyRequest().authenticated() // 그 외 모든 요청은 인증 필요
                .and()
                .formLogin()
                .loginPage("/login")            // 사용자 정의 로그인 페이지 URL
                .loginProcessingUrl("/signin")  // 로그인 url
                .usernameParameter("loginId")   //form 에서 보내는 id
                .passwordParameter("password")  //form 에서 보내는 password
                .defaultSuccessUrl("/")         //성공 후 접근하는 페이지 
                .failureHandler(formAuthenticationFailureHandler)
                .successHandler(formAuthenticationSuccessHandler);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/images/**", "/sb-admin/**", "/bootstrap/**", "/icons/**");
    }

}

 

//login.html 

<form method="post" id="loginForm" th:action="@{/signin}">
 <label for="loginId">아이디</label><input class="form-control" name="loginId" id="loginId" type="text" autocomplete="off" />
 <label for="password">비밀번호</label><input class="form-control" name="password" id="password" type="password" autocomplete="off" />
</form>
  • loginProcessingUrl("/signin")과 같이 설정하면, 사용자가 로그인 정보를 입력하고 로그인 버튼을 클릭할 때, 폼 데이터가 /signin URL로 전송됩니다.
  • th:action="@{/signin}" : th:action="@{/signin}"으로 설정하면, 폼 데이터는 /signin 경로로 전송됩니다. 이는 loginProcessingUrl에 지정된 경로와 일치해야 합니다.
  • usernameParameter("loginId")와 같이 설정하면, 사용자 이름 필드의 이름을 loginId로 사용하도록 Spring Security에 지시합니다. 이는 HTML 폼의 사용자 이름 입력 필드의 id나 name 속성이 loginId로 설정

3. FormAuthenticationProvider

@Component
@RequiredArgsConstructor
public class FormAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String loginId = authentication.getName();
        String passwd = String.valueOf(authentication.getCredentials());

        UserDetails userDetails = null;
        try {
            userDetails = userDetailsService.loadUserByUsername(loginId);

            if (ObjectUtils.isEmpty(userDetails) || !passwd.equals(userDetails.getPassword())) {
                throw new BadCredentialsException("Invalid password");
            }

        } catch (UsernameNotFoundException e) {
            log.info(e.toString());
            throw new UsernameNotFoundException(e.getMessage());
        } catch (BadCredentialsException e) {
            log.info(e.toString());
            throw new BadCredentialsException(e.getMessage());
        } catch (Exception e) {
            log.info(e.toString());
            throw new RuntimeException(e.getMessage());
        }

        return new UsernamePasswordAuthenticationToken(loginId, null, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}
  • 로그인하게되면 FormAuthenticationProvider 이쪽으로 접근하게됩니다.
  • 그 이후 userDetailsService.loadUserByUsername(loginId); 접근하게 되면  아래 코드가 실행
  • user password 가 암호화 안되어 있기 때문에 equals() 로 비교합니다.
  • UsernamePasswordAuthenticationToken : 인증이 성공하면, 사용자의 로그인 ID, 비밀번호(여기서는 보안상의 이유로 null로 설정), 그리고 권한 목록을 포함하는 새로운 UsernamePasswordAuthenticationToken 객체를 반환합니다. 이 토큰은 인증된 사용자를 나타냅니다. 이 메서드는 전반적으로 제공된 Authentication 객체의 정보를 기반으로 사용자의 인증 과정을 진행하며, 인증에 성공하면 인증된 사용자를 나타내는 새로운 Authentication 객체를 반환합니다. 인증 과정 중에 문제가 발생하면 적절한 예외를 던져 인증 실패를 나타냅니다.

4. UserDetailsServiceImpl

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    private final HttpUtil httpUtil;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Map<String, Object> param = new HashMap<String, Object>(){{
            put("userId", username);
        }};

        ResponseEntity<?> responseEntityUser;
        try {
            responseEntityUser = httpUtil.restApiDataPostRequest("tablet/login", "v2.0", param);
        } catch (Exception e) {
            // restApiDataPostRequest 메서드에서 발생하는 예외를 잡아서 Spring Security 인증 실패로 처리
            throw new UsernameNotFoundException("사용자 id가 없습니다. " + username, e);
        }

        Users user = new Gson().fromJson(String.valueOf(responseEntityUser.getBody()), Users.class);

        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("No user found with username: " + username);
        }

        return UserDetailsImpl.build(user);
    }
}
  • 사용자 이름(username)을 포함하는 맵(param)을 생성합니다. 이 맵은 외부 시스템에 요청을 보낼 때 사용될 파라미터를 담고 있습니다.
  • httpUtil.restApiDataPostRequest 메서드를 호출하여 userId 에 해당하는 user를 찾아 옵니다.
  • 요청 중 예외가 발생하면 catch 블록이 실행됩니다.
  • Gson().fromJson 메서드를 사용하여 ResponseEntity의 바디(응답 내용)을 Users 클래스의 인스턴스로 변환합니다.
  • UserDetailsImpl 클래스는 UserDetails 인터페이스의 구현체로, Spring Security에서 사용자의 인증 정보를 담는 데 사용됩니다.

5. UserDetailsImpl

@Getter
public class UserDetailsImpl implements UserDetails {

    private Long id;
    private String username;
    private String userId;
    @JsonIgnore
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(Long id, String username, String userId, String password
            ,Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.username = username;
        this.userId = userId;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserDetailsImpl build(Users users) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(users.getUserGroupType()));

        return new UserDetailsImpl(
                users.getId(),
                users.getUserName(),
                users.getUserId(),
                users.getUserPassword(),
                authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.userId;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

6. 인증이 성공 했을때 FormAuthenticationSuccessHandler 클래스

@Component
public class FormAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        setDefaultTargetUrl("/");

        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if(savedRequest!=null) {
            String targetUrl = savedRequest.getRedirectUrl();
            redirectStrategy.sendRedirect(request, response, targetUrl);
        } else {
            redirectStrategy.sendRedirect(request, response, getDefaultTargetUrl());
        }

    }
}

 

7. 인증 실패 했을때 FormAuthenticationFailureHandler 클래스

@Component
public class FormAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception) throws IOException, ServletException {

        setDefaultFailureUrl("/login?error=true");

        super.onAuthenticationFailure(request, response, exception);

        String errorMessage = exception.getMessage();

        if (exception instanceof BadCredentialsException) {
            errorMessage = "암호가 다릅니다.";
        }

        request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage);
    }
}

 

8.인증에 실패 했을때 controller 로 /login?error=true 전달

@GetMapping("/login")
public String loginView(@RequestParam(value = "error", required = false) String error, Model model, HttpServletRequest request) {

    if (StringUtils.hasText(error)) {
        // 세션에서 인증 실패 메시지 가져오기
        HttpSession session = request.getSession(false);
        String errorMessage = null;
        if (session != null) {
            errorMessage = (String) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        }

        // 모델에 에러 메시지 추가
        model.addAttribute("errorMessage", errorMessage);
    }

    return "login";
}
  • 요청 파라미터를 String 타입의 error 변수에 바인딩합니다. required = false는 이 파라미터가 필수가 아니라는 것을 의미합니다. 즉  "error" 파라미터가 요청에 포함되어 있지 않아도 이 메소드는 에러 없이 호출될 수 있습니다.

9.오류 메시지 표출 부분

<div th:if="${errorMessage}" class="form-group">
    <span th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}"
          class="alert alert-danger" />
</div>
728x90

'개발 > spring boot' 카테고리의 다른 글

공공테이터 포털 공휴일 api 연동 방법  (0) 2024.05.22
Spring Security를 사용한 서버에서 다른 포트를 사용하여 다중 로그인할 때 둘 다 로그아웃되는 현상  (0) 2024.03.27
Spring Boot plugin requires Gradle 5 (5.6.x only) or Gradle 6 (6.3 or later). The current version is Gradle 4.5.1  (0) 2024.03.21
Refused to apply style from '<URL>' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.  (0) 2024.03.21
spring boot 이미지 경로 설정 방법 WebMvcConfigurer 사용  (0) 2024.03.11
'개발/spring boot' 카테고리의 다른 글
  • 공공테이터 포털 공휴일 api 연동 방법
  • Spring Security를 사용한 서버에서 다른 포트를 사용하여 다중 로그인할 때 둘 다 로그아웃되는 현상
  • Spring Boot plugin requires Gradle 5 (5.6.x only) or Gradle 6 (6.3 or later). The current version is Gradle 4.5.1
  • Refused to apply style from '<URL>' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
nix-be
nix-be
  • nix-be
    NiX
    nix-be
  • 전체
    오늘
    어제
    • 홈
      • 책
        • 오브젝트
      • 성장
        • jpa Querydsl 정리
        • 코딩테스트
      • 인프라
        • linux
        • vmware
        • CI&CD
        • 네트워크
        • docker
      • 개발
        • spring boot
        • JPA
        • java
        • thymeleaf
        • 이슈
        • jquery
        • javascript
        • 안드로이드
      • DB
        • postgreSql
      • 잡다한것
        • 프로그램
        • 일상 관련
      • 회사
        • 티
  • 블로그 메뉴

    • 홈
    • 개발
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
nix-be
spring security formLogin 구현
상단으로

티스토리툴바