스프링부트
Spring Security를 활용한 로그인 구현
3은
2024. 12. 3. 11:26
728x90
UsernamePasswordAuthenticationFilter의 흐름이다. 이걸 보고 코드를 보면 더 이해가 잘된다.
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