스프링부트

Spring Security를 활용한 로그인 구현

3은 2024. 12. 3. 11:26
728x90

 

 

UsernamePasswordAuthenticationFilter의 흐름이다. 이걸 보고 코드를 보면 더 이해가 잘된다.

출처: https://velog.io/@bimilless/Spring-Security-%EC%9D%B8%EC%A6%9D-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8

 

1. 로그인 요청 : 클라이언트 -> 서버

 

signin.jsp

    <form method="post" action="/login">
        아이디 : <input id="userId" name="userId" type="text"><br/>
        비밀번호 : <input id="userPw" name="userPw" type="password"><br/>
        <input type="submit" value="Login" > <%--onclick="login();"--%>
    </form>
  • 사용자가 userId와 userPw를 입력
  • 로그인 버튼을 클릭하면 <form> 태그의 action 속성에 지정된 URL( /login )로 POST 요청 전송

 

2. Spring Security 필터 체인에서 요청 처리

SecurityConfiguration.java 일부 코드 (Spring Security 설정)

.formLogin((login -> login
        .loginPage("/signin")    //사용자 정의 로그인 페이지로 전환, 기본 로그인 페이지 무시 (Get)
        .loginProcessingUrl("/login")   //사용자 이름과 비밀번호를 검증할 URL 지정  (POST)
                                        //form 태그의 action 과 동일하게 작성해야 됨
        .defaultSuccessUrl("/main", true)   //로그인 성공 이후 이동 페이지, [true]이면 무조건 저장된 위치로 이동
                                                                        //[false]이면 인증 전에 보안이 필요한 페이지를 방문하다가 인증에 성공한 경우이면 이전 위치로 리다이렉트 됨
        .failureUrl("/signin?error")     //인증에 실패할 경우 사용자에게 보내질 URL 지정
        .usernameParameter("userId")        //인증을 수행할 때 사용자 이름(아이디)를 찾기 위해 확인하는 HTTP 매개변수 설정
        .passwordParameter("userPw")        //인증을 수행할 때 비밀번호를 찾기 위해 확인하는 HTTP 매개변수 설정
                                            //input 태그의 name 과 동일하게 작성해야 됨
        .successHandler(customAuthenticationSuccessHandler)     //인증 성공 시 사용할 Handler를 지정
        .permitAll()
        )
)

 

  • UsernamePasswordAuthenticationFilter 가 요청 가로채기
    • /login 요청을 Spring Security가 필터 체인을 통해 가로챔
    • userId와 userPw 데이터를 추출하여 UsernamePasswordAuthenticationToken 객체 생성
  • AuthenticationManager 호출
    • 생성된 UsernamePasswordAuthenticationToken 객체를 기반으로 인증 프로세스 시작
    • 등록된 CustomAuthenticationProvider로 인증 처리 위임

 

3. CustomAuthenticationProvider에서 인증 처리

 

CustomAuthenticationProvider.java

 

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private WebClientService webClientService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String userId = (String) authentication.getPrincipal();     // 폼에서 입력한 id를 가져온다
        String userPw = (String) authentication.getCredentials();   // 폼에서 입력한 pw를 가져온다

        /*내부 api 호출 코드*/
        String url = "http://localhost:8080/signin";

        String data = String.format("{\"userId\":\"%s\", \"userPw\":\"%s\"}", userId, userPw);

        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

        String result = webClientService.asynchronousRequest(url, HttpMethod.POST, data, headers).block().getBody();

	/*API 응답 처리*/
        JSONObject jsonResult = new JSONObject(result);
        
        if (jsonResult.getInt("status") == 200) {
            //UsernamePasswordAuthenticationToken 생성
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                    jsonResult.getString("userId"),
                    null,
                    List.of(new SimpleGrantedAuthority(jsonResult.getString("authority")))
            );
            token.setDetails(jsonResult.getString("userName")); // userName을 Details에 추가
            return token;
        } else {
            throw new BadCredentialsException("Invalid credentials");
        }
    }

 

1.입력 데이터 추출 :

  • UsernamePasswordAuthenticationToken에서 userId와 userPw 추출

2. 내부 API 호출 :

  • WebClientService를 사용해 /singin API에 인증 요청을 보냄
WebClient란?
- Spring WebClient는 웹으로 API를 호출하기 위해 사용되는 Http Client 모듈 중 하나

@Service
public class WebClientService {

    private WebClient createWebClient () {
        try {
            SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
            HttpClient httpClient = HttpClient.create().secure((builder) -> builder.sslContext(sslContext));

            //MaxInMemorySize 설정 - Spring Webflux는 어플리케이션 메모리 이슈를 방지하기 위해 코덱의 버퍼 사이즈를 디폴트 256KB로 제한하고 있지만 설정을 추가하여 모든 디폴트 코덱의 최대 버퍼 사이즈를 조절 할 수 있다
            return WebClient.builder().codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1 * 1024 * 1024)) // 1MB
                    .clientConnector(new ReactorClientHttpConnector(httpClient)).build();
        } catch (Exception e) {
            e.printStackTrace();

            return null;
        }
    }

    public Mono<ResponseEntity<String>> asynchronousRequest (String url, HttpMethod method, String data, MultiValueMap<String, String> headers) {
        return createWebClient().method(method).uri(url).contentType(MediaType.APPLICATION_JSON).body(Mono.just(data != null ? data : ""), String.class).headers((header) -> {
            if (headers == null) {
                return;
            }

            headers.forEach((key, values) -> {
                values.forEach(value -> {
                    header.set(key, value);
                });
            });
        }).accept(MediaType.APPLICATION_JSON).retrieve().toEntity(String.class);            //retrieve() - body를 바로 가져온다
                                                                                            //exchange() - ClientResponse를 상태값, 헤더와 함께 가져온다
    }

}​

 

 

3. API 응답 처리 : 

  • /singin API는 사용자 정보를 확인한 뒤 JSON 응답을 반환
  • status가 200이면 UsernamePasswordAuthenticationToken 생성, 아니면 예외 처리

 

4. 내부 API  /singin에서 사용자 인증

LoginController.java

//사용자로부터 로그인 요청을 받고, UserService를 호출하여 인증 로직을 처리
@RestController
@RequestMapping(value = "/signin")
public class LoginController {
    @Autowired
    private UserService userService;

    @PostMapping
    public ResponseEntity<String> login(@RequestBody String body) {
        JSONObject response = new JSONObject(body);  //response는 {"userId":"user", "userPw":"asdf"} 이런식
        
        //사용자 조회 및 인증
        UserVO user = userService.findUserById(response.getString("userId"));
        if(user == null || !user.getUserPw().equals(response.getString("userPw"))) {  //user가 없거나 비밀번호가 일치하지 않으면 400 에러
            response.put("message", "Invalid credentials");
            response.put("status", 400);
            return ResponseEntity.status(400).body(response.toString());
        }

        response.put("authority", user.getRole());
        response.put("userName", user.getUserName());
        response.put("status", 200);
        return ResponseEntity.status(200).body(response.toString());
    }

}

 

1. 사용자 조회

  • UserService에서 userId를 기반으로 사용자 정보 조회

2. 비밀번호 검증

  • 입력된 비밀번호화 저장된 비밀번호 비교하여 일치 여부 확인

3. 응답 생성

  • 인증 성공 시 사용자 정보를 포함한 JSON 응답 반환
  • 실패 시 에러 메시지와 상태 코드 반환

 

UserService.java

 

// DB 연동 없이 하드코딩함

@Service
public class UserService {
    public UserVO findUserById(String userId) {
        if("user".equals(userId)) {
            return new UserVO("NONAME","user", "asdf", "ROLE_USER");
        } else if("manager".equals(userId)) {
            return new UserVO("ADMIN","manager", "1234", "ROLE_MANAGER");
        } else {
            return null;
        }
    }
}

 

UserVO.java
public class UserVO {

    private final String userName;
    private final String userId;
    private final String userPw;
    private final String role;

    public UserVO(String userName, String userId, String userPw, String role) {

        this.userName = userName;
        this.userId = userId;
        this.userPw = userPw;
        this.role = role;
    }

    //Getter
    public String getUserName() {
        return userName;
    }

    public String getUserId() {
        return userId;
    }

    public String getUserPw() {
        return userPw;
    }

    public String getRole() {
        return role;
    }

}

 

5. Spring Security에서 인증 결과 처리

SecurityConfiguration.java 일부 코드 (Spring Security 설정)

.formLogin((login -> login
        .loginPage("/signin")    //사용자 정의 로그인 페이지로 전환, 기본 로그인 페이지 무시 (Get)
        .loginProcessingUrl("/login")   //사용자 이름과 비밀번호를 검증할 URL 지정  (POST)
                                        //form 태그의 action 과 동일하게 작성해야 됨
        .defaultSuccessUrl("/main", true)   //로그인 성공 이후 이동 페이지, [true]이면 무조건 저장된 위치로 이동
                                                                        //[false]이면 인증 전에 보안이 필요한 페이지를 방문하다가 인증에 성공한 경우이면 이전 위치로 리다이렉트 됨
        .failureUrl("/signin?error")     //인증에 실패할 경우 사용자에게 보내질 URL 지정
        .usernameParameter("userId")        //인증을 수행할 때 사용자 이름(아이디)를 찾기 위해 확인하는 HTTP 매개변수 설정
        .passwordParameter("userPw")        //인증을 수행할 때 비밀번호를 찾기 위해 확인하는 HTTP 매개변수 설정
                                            //input 태그의 name 과 동일하게 작성해야 됨
        .successHandler(customAuthenticationSuccessHandler)     //인증 성공 시 사용할 Handler를 지정
        .permitAll()
        )
)

 

1. 인증 성공 시

  • Spring Security는 인증된 사용자 정보 세션에 저장
  • 설정된 URL(/main)으로 리다이렉트

2. 인증 실패 시

  • 로그인 페이지(/siginin?error)으로 리다이렉트

 

6. JSP에서 사용자 데이터 출력

 

ViewController.java

@GetMapping("/main")
    public String main(Model model) {
        // Authentication 객체 가져오기
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        model.addAttribute("userName",authentication.getDetails().toString());
        model.addAttribute("userId", authentication.getPrincipal().toString());
        model.addAttribute("authorities", authentication.getAuthorities().toString());

        return "main";
    }

 

  • SecurityContext에서 인증 정보를 가져온다
  • userName, userId, authorities을 Model에 추가하여 jsp에 전달

main.jsp

<h1>Main Page</h1>

<!-- 권한이 ROLE_MANAGER일 때만 링크 표시 -->
<c:if test="${authorities == 'ROLE_MANAGER'}">
    <button><a href="/management">Management Page</a></button>
</c:if>

<p>Login Success!</p>
<h1>사용자 프로필</h1>
<p>ID : ${userId}</p>
<p>User Name : ${userName}</p>
<p>권한 : ${authorities}</p>

<form action="/logout" method="post">
    <button type="submit">로그아웃</button>
</form>

 

  • JSP 페이지에서 전달받은 데이터 출력

 

 

 

참고

 

Spring Security 인증 컴포넌트

사용자의 로그인 request를 제일 먼저 만나는 컴포넌트는 바로 Spring Security Filter Chain의 UsernamePasswordAuthenticationFilter 이다.UsernamePasswordAuthenticationFilter 는 일반적으

velog.io