일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- continue 사용법
- 멀티프로세싱
- 캡슐화
- 객체지향
- 혼공얄코
- 프로그래머스
- contiune
- 쿠키
- 오버로딩
- 멀티태스킹
- 리눅스
- spring security 설정
- 프로그래머스 붕대 감기
- hackerrank
- 티스토리챌린지
- 자바의 정석
- 입출력
- SQL Mapper
- 중첩 break
- 다형성
- CPU
- 오버라이딩
- 오블완
- 자바의정석
- over()
- 붕대 감기
- 붕대 감기 자바
- break 사용법
- java
- spring security
- Today
- Total
쉽게 쉽게
[Spring] Spring Security 구현 (2) - 커스텀 본문
▤ 목차
1. Spring Security 커스텀 화면
Spring Security에서 기본으로 제공하는 로그인 화면과 설정이 있지만 이는 기본적인 수준이라 커스텀이 반드시 필요하다.
커스텀 구현 과정에서 화면 생성, 커스텀용 클래스 및 빈 생성 등을 진행할 것이다.
Spring Security는 AuthenticationManager(ProviderManager)가 가지고 있는 provider 목록을 순회하면서 provider가 실행 가능한 경우에 provider의 authenticate 메서드를 호출하여 인증 절차를 수행한다.
따라서 4~8에 해당하는 과정을 커스텀하여 만들어줄 것이다.
전체적인 과정은 아래와 같다.
login 화면 구현 -> Spring Security 설정 수정 -> custcomUser 생성 (vo) -> AuthenticationProvider 생성 (권한 처리)
생성할 클래스 목록을 살펴보면 아래와 같다.
- customAuthenticationProvider에서 사용하는 custcomUser (로그인 vo)를 생성
- customAuthenticationProvider 로그인 인증을 처리하는 클래스를 생성
- customAuthenticationProvider에서 사용되는 customUserDetailsService 생성
- 로그인 성공/실패를 관리하는 loginSuccessHandler / loginFailureHandler 핸들러 생성
1. login 화면 구현
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<style>
.login-wrapper{
width: 400px;
height: 350px;
padding: 40px;
box-sizing: border-box;
}
.login-wrapper > h2{
font-size: 24px;
color: #6A24FE;
margin-bottom: 20px;
}
#login-form > input{
width: 100%;
height: 48px;
padding: 0 10px;
box-sizing: border-box;
margin-bottom: 16px;
border-radius: 6px;
background-color: #F8F8F8;
}
#login-form > input::placeholder{
color: #D2D2D2;
}
#login-form > input[type="submit"]{
color: #fff;
font-size: 16px;
background-color: #6A24FE;
margin-top: 20px;
}
#login-form > input[type="checkbox"]{
display: none;
}
</style>
<body>
<div class="login-wrapper">
<h2>Login</h2>
<form action="/common/loginProcess.do" method="post" id="login-form">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> <!-- CSRF 토큰 -->
<input type="text" name="user_id" placeholder="Id">
<input type="password" name="user_password" placeholder="Password">
<input type="submit" value="Login">
</form>
</div>
</body>
</html>
Spring Security에서 csrf 설정을 비활성화하지 않으면 login 화면에 csrf 토큰을 넣어줘야 한다.
만약 csrf 토큰을 넣지않으면 403 에러가 발생할 것이다.
2. 로그인 성공화면 구현
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>로그인 성공</h2>
</body>
</html>
2. Spring Security 커스텀 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security-5.8.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- HTTP 보안 설정 -->
<http auto-config="false" use-expressions="true">
<!-- csrf 비활성화 -->
<!-- <csrf disabled="true" /> -->
<intercept-url pattern="/resources/**" access="permitAll" />
<intercept-url pattern="/common/**" access="permitAll" />
<intercept-url pattern="/**" access="permitAll" />
<!-- 로그인 설정 -->
<!--
login-page : form login 폼 페이지 지정
default-target-url : 로그인 성공시 이동할 페이지 지정
authentication-failure-handler-ref : 로그인 실패시 핸들러
authentication-success-handler-ref : 로그인 성공시 핸들러
username-parameter : 로그인 id 값
password-parameter : 로그인 비밀번호 값
-->
<form-login
login-page="/common/login.do"
default-target-url="/common/main.do"
login-processing-url="/common/loginProcess.do"
authentication-failure-handler-ref="loginFailureHandler"
authentication-success-handler-ref="loginSuccessHandler"
username-parameter="user_id"
password-parameter="user_password" />
<!-- 로그아웃 설정 -->
<logout logout-url="/common/logout" logout-success-url="/common/login.do" invalidate-session="true" delete-cookies="JSESSIONID"/>
</http>
<!-- PasswordEncoder 설정 (Spring Security 5.8.1 권장사항) -->
<beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!-- 로그인 성공/실패 핸들러 -->
<beans:bean id="loginSuccessHandler"
class="iancms.global.auth.loginSuccessHandler" />
<beans:bean id="loginFailureHandler"
class="iancms.global.auth.loginFailureHandler" />
<!-- CustomAuthenticationProvider 등록 -->
<authentication-manager>
<authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>
<!-- 로그인 권한 과정을 처리할 클래스 -->
<beans:bean id="customAuthenticationProvider" class="test.global.auth.customAuthenticationProvider" />
</beans:beans>
2024.10.31 - [개발공부/Spring] - [Spring] Spring Security 구현 (1) - 설정
이전에 Spring Security 정리했던 설정보다 새롭게 추가한 설정에 대해 설명하고자 한다.
1. Spring Security 세부 설정
CSRF 설정
- CSRF 설정을 활성화하면 login 화면에서 csrf 토큰값을 넘겨줘야 한다.
- CSRF 토큰을 비활성화하려면 <csrf disabled="true" />를 추가해 주면 된다.
form-login (로그인 세부 설정)
- Spring Security의 로그인 인증을 하는 역할
- login-processing-url : 로그인 요청을 처리할 URL을 지정
- username-parameter : 로그인 폼에서 사용자 ID를 입력받을 input 태그의 name 속성 값을 지정
- password-parameter : 로그인 폼에서 비밀번호를 받을 input 태그의 name 속성 값을 지정
- default-target-url : 로그인이 성공하면 이동할 URL을 지정
- authentication-success-handler-ref : 로그인이 성공하면 호출될 핸들러를 지정
- authentication-failure-handler-ref : 로그인이 실패하면 호출될 핸들러를 지정
- logout : 로그아웃 요청을 처리할 URL을 지정
로그인이 성공/실패시 핸들러를 설정
- 로그인 과정 성공/실패시 추가적인 처리를 진행할 수 있는 핸들러를 호출할 수 있다.
- 없어도 default-target-url로 성공처리를 할 수 있지만 추가적인 기능을 원한다면 구현이 필요하다.(로그인 로그 등)
- Spring Security의 관련 클래스를 상속받아 구현되며 loginSuccessHandler / loginFailureHandler라고 명명해서 구현할 것이다.
authentication-manager 설정
- authentication-manager는 인증 처리하는 filter로부터 인증처리를 지시받는 첫 번째 클래스
- Spring Security는 ID와 Password를 Authentication 인증 객체에 저장하고 이 객체를 AuthenticationManager에게 전달
- authentication-manager는 AuthenticationProvider 목록 중에서 인증 처리 요건에 맞는 AuthenticationProvider를 찾아 인증 처리를 위임
- Spring Security의 AuthenticationProvider를 상속받는 customAuthenticationProvider라는 클래스를 구현해서 로그인 인증을 처리할 것이다.
3. custcomUser 구현
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class customUser implements UserDetails {
private static final long serialVersionUID = 1L;
/** 아이디 */
private String user_id;
/** 비밀번호 */
private String user_password;
/** 이름 */
private String user_name;
/** 생년월일 */
private String user_birth;
/** 권한 */
/** ROLE_ADMIN, ROLE_USER */
private String user_role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(user_role));
return authorities;
}
@Override
public String getPassword() {
// TODO Auto-generated method stub
return user_password;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return user_id;
}
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return false;
}
}
lombok을 활용하여 UserDetails를 구현하는 custcomUser를 생성했다.
1. UserDetails이란?
UserDetails는 Spring Security가 사용자의 인증을 처리하는 데 필요한 사용자 정보를 제공하는, 사용자의 정보
(사용자 이름, 비밀번호, 권한, 계정 만료, 비밀번호 만료, 계정 잠금 등)를 담고 있는 인터페이스이다.
해당 인터페이스에서 구현을 요구하는 메서드는 다음과 같다.
- Collection<? extends GrantedAuthority> getAuthorities(): 사용자에게 부여된 권한(롤)을 GrantedAuthority 객체의 컬렉션으로 반환
- String getPassword(): 사용자의 암호화된 비밀번호를 반환
- String getUsername(): 사용자의 이름을 반환
- boolean isAccountNonExpired(): 사용자 계정이 만료되지 않았는지 여부를 반환
- boolean isAccountNonLocked(): 사용자 계정이 잠기지 않았는지 여부를 반환
- boolean isCredentialsNonExpired(): 사용자의 비밀번호가 만료되지 않았는지 여부를 반환
- boolean isEnabled(): 사용자 계정이 활성화되었는지 여부를 반환
custcomUser 구현 목적
Spring Security의 AuthenticationProvider를 상속받는 customAuthenticationProvider라는 클래스를 구현하는 게 최종 목적인데 AuthenticationProvider에서 UserDetailsService를 통해 사용자 정보를 조회를 진행하며, 그 결과를 UserDetails로 받기 때문이다.
또한 UserDetails에서 다루는 필수값은 아이디, 비밀번호, 권한이며 내가 원하는 값들을 추가하기 위해서도 custcomUser를 구현할 필요가 있다.
4. customAuthenticationProvider 생성
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class customAuthenticationProvider implements AuthenticationProvider {
@Autowired
private customUserDetailsService userDetailsService; // UserDetailsService를 주입받아 사용
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호를 인코딩/검증하기 위한 PasswordEncoder
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 사용자가 입력한 사용자명과 비밀번호를 가져옴
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// UserDetailsService를 통해 사용자 정보를 조회
UserDetails user = userDetailsService.loadUserByUsername(username);
// 비밀번호가 일치하는지 확인
if (!passwordEncoder.matches(password, user.getPassword())){
throw new BadCredentialsException("Invalid username or password");
}
//authentication 객체로 리턴
UsernamePasswordAuthenticationToken authenticatedUser = null;
authenticatedUser = new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
// 인증 성공 시, 인증된 Authentication 객체 반환
return authenticatedUser;
}
@Override
public boolean supports(Class<?> authentication) {
// UsernamePasswordAuthenticationToken 타입의 인증을 지원
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
customAuthenticationProvider는 로그인 요청한 유저와 DB의 유저 정보를 비교하여 인증을 담당하는 부분이다.
UsernamePasswordAuthenticationToken으로 반환할 때, 권한은 UserDetails의 getAuthorities를 활용해서 반환한다.
1. customUserDetailsService 구현
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import iancms.domain.user.common.service.commonService;
import iancms.domain.user.common.vo.customUser;
@Service
public class customUserDetailsService implements UserDetailsService {
@Autowired
private commonService commonService; // 데이터베이스에서 사용자 정보를 가져올 service
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
// 데이터베이스에서 사용자 정보를 조회
customUser user = commonService.loginCheck(username);
return user;
} catch (Exception e) {
throw new UsernameNotFoundException("User not found");
}
}
}
userDetailsService는 Spring Security에서 유저의 정보를 가져오는 인터페이스이다.
user에 user_id, password, 권한 값은 필수며 나머지 값은 원하는 대로 추가하면 된다.
필자는 uesr에 모든 값을 넘겨줬다.
아래는 DB에서 로그인 정보를 불러올 때, 쿼리이다.
<select id="loginCheck" parameterType="String" resultType="customUser">
SELECT
*
FROM
user_mn
where
user_id = #{user_id}
</select>
5. loginSuccessHandler / loginFailureHandler 생성
1. loginSuccessHandler 구현
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import iancms.domain.user.common.vo.customUser;
@Component
public class loginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
customUser user = (customUser) authentication.getPrincipal();
String sAuth = user.getUser_role();
String targetUrl = "/";
// 권한에 따라 접속 메뉴 구분
if (sAuth.equals("ROLE_USER")) {
targetUrl = "/common/main.do";
} else if (sAuth.equals("ROLE_ADMIN")) {
targetUrl = "/admin/main.do";
}
//페이지 이동
response.sendRedirect(targetUrl);
}
}
로그인 성공 시 권한에 따라 페이지 이동을 다르게 할 수 있다.
추가적으로 로그인 시 로그가 쌓이는 로직을 여기에 구현할수도 있다.
2. loginFailureHandler 구현
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
@Component
public class loginFailureHandler extends SimpleUrlAuthenticationFailureHandler{
private static final Logger logger = LoggerFactory.getLogger(loginFailureHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String errorMessage;
String inputUserId = (String) request.getParameter("userId");
if (exception instanceof BadCredentialsException) {
errorMessage = "아이디 또는 비밀번호가 맞지 않습니다." + "<br>" + "다시 확인해 주세요.";
} else if (exception instanceof InternalAuthenticationServiceException) {
errorMessage = "내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다." + "<br>" + "관리자에게 문의하세요.";
} else if (exception instanceof UsernameNotFoundException) {
errorMessage = "계정이 존재하지 않습니다." + "<br>" + "회원가입 진행 후 로그인 해주세요.";
} else if (exception instanceof AuthenticationCredentialsNotFoundException) {
errorMessage = "인증 요청이 거부되었습니다." + "<br>" + "관리자에게 문의하세요.";
} else {
errorMessage = "알 수 없는 이유로 로그인에 실패하였습니다." + "<br>" + "관리자에게 문의하세요.";
}
logger.info("[FAIL] 로그인 실패 : ID={}, MSG={}", inputUserId, exception.getMessage());
request.getSession().setAttribute("errorMessage", errorMessage);
setDefaultFailureUrl("/common/login.do");
super.onAuthenticationFailure(request, response, exception);
}
}
로그인 실패 시 에러 메서지를 세션에 담아 로그인 화면에 띄울 수 있다.
// 로그인 화면
@RequestMapping(value = "/login.do", method = RequestMethod.GET)
public String login(HttpSession session , commonDto dto, Model model) throws Exception {
// 세션에서 에러 메시지 가져오기
String errorMessage = (String) session.getAttribute("errorMessage");
System.out.print(errorMessage);
model.addAttribute("msg", errorMessage);
return "common/login.main";
}
컨트롤러에서 세션에 담긴 메세지 값을 받아 화면에서 모달로 띄워준다.
<!-- 실패 메세지를 출력(modal) -->
<div id="modalOverlay" class="modal-overlay" ></div>
<!-- 모달 창 -->
<div id="myModal" class="modal" style="display:none;">
<div class="modal-header">
Notification
<span id="closeModal" class="modal-close">×</span>
</div>
<div class="modal-body">
<p id="modalMessage">${msg}</p>
</div>
<div class="modal-button">
<button id="closeModalButton">Close</button>
</div>
</div>
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
/* 모달 창 스타일 */
.modal {
position: fixed;
top: 10%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1001;
width: 300px;
}
/* 모달 헤더 스타일 */
.modal-header {
font-size: 18px;
margin-bottom: 10px;
}
/* 모달 닫기 버튼 스타일 */
.modal-close {
float: right;
font-size: 20px;
cursor: pointer;
}
/* 모달 본문 스타일 */
.modal-body {
margin-bottom: 10px;
}
/* 모달 버튼 */
.modal-button {
display: flex;
justify-content: flex-end;
}
.modal-button button {
padding: 5px 10px;
cursor: pointer;
}
이로써 설정은 마무리했으며, 원하는 권한별 메뉴 경로를 잘 설정하여 마무리하면 될 것 같다.
https://backend-jaamong.tistory.com/83
https://bin-repository.tistory.com/129
https://gregor77.github.io/2021/05/18/spring-security-03/
잘못된 내용이 있다면 지적부탁드립니다. 방문해주셔서 감사합니다. |
'개발공부 > Spring' 카테고리의 다른 글
[Spring] Spring Security 구현 (1) - 설정 (1) | 2024.10.31 |
---|---|
[Spring] tiles 적용 (5) | 2024.09.05 |
[Spring] static 변수에 autowired 설정 방법 (0) | 2024.07.29 |
[Spring] 스케줄러 구현 (1) | 2024.06.30 |
[Spring] 게시글 조회수 중복방지 (1) | 2024.05.30 |