241227 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.
Role
이전 시간과 이어서 진행됩니다.
SecurityConfig
@Configuration
@EnableWebSecurity // JWT 적용 시 굳이 명시하지 않아도 괜찮다.
public class SecurityConfig {
// 스프링 시큐리티는 시큐리티 관련해 다양한 기능을 필터 체인(클라이언트 요청 -> 필터 -> 필터 -> .. -> 서블릿)
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// white list 목록 permitAll 대상
String[] whiteList = {
"/", "/notice", "/user/loginform", "/user/login_fail", "/user/expired", "/shop"
};
httpSecurity
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(whiteList).permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/staff/**").hasAnyRole("ADMIN", "STAFF")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/user/required_loginform")
.loginProcessingUrl("/user/login")
.usernameParameter("userName") // 입력 필드의 name과 동일해야한다.
.passwordParameter("password") // 입력 필드의 name과 동일해야한다.
.successHandler(new AuthSuccessHandler()) // 로그인 성공 시 처리할 로직이 있다면 핸들러를 만들어준다.
.failureForwardUrl("/user/login_fail")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/user/logout") // 로그아웃의 경우 시큐리티가 알아서 처리해준다. 굳이 만들지 않아도 괜찮다.
.logoutSuccessUrl("/")
.permitAll()
)
.exceptionHandling(exception -> exception // 인증 처리 중 예외가 발생했을 때 관련 설정
// exceptionHandling는 ExceptionHandlingConfigurer를 반환
// ExceptionHandlingConfigurer 객체의 accessDeniedPage()를 이용해 403인 경우 이동할 경로 지정
.accessDeniedPage("/user/denied")
)
.sessionManagement(session -> session
.maximumSessions(1) // 최대 허용 세션 갯수 설정
.expiredUrl("/user/expired") // 세션 허용 갯수 초과 시 forward
);
return httpSecurity.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
AuthenticationManager authenticationManager(HttpSecurity httpSecurity,
UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
httpSecurity.getSharedObject(AuthenticationManagerBuilder.class);
// AuthenticationManagerBuilder는 UserDetailsService를 필요로 한다.
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
return authenticationManagerBuilder.build();
}
}
CustomUserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
// 원래는 DB에서 사용자 정보를 읽어야 한다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// loadUserByUsername : 사용자의 정보를 읽어 UserDetails 타입으로 반환
System.out.println("username : " + username);
String role = "";
if(username.equals("guest")) {
role = "ROLE_USER";
} else if(username.equals("batman")) {
role = "ROLE_STAFF";
} else if(username.equals("superman")) {
role = "ROLE_ADMIN";
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
MyUser myUser = MyUser.builder()
.id(1) // 사용자 id로 고정값으로 사용, 실제로는 DB에서 자동으로 생성 내지는 pk 등을 사용한다.
.userName(username)
.password(encoder.encode("1234"))
.role(role)
.build();
// GrantedAuthority 인터페이스 : 사용자가 어떤 권한을 가지고 있는지를 나타내기 위해 사용한다.
// SimpleGrantedAuthority : GrantedAuthority의 파생 클래스
List<GrantedAuthority> authlist = new ArrayList<GrantedAuthority>();
authlist.add(new SimpleGrantedAuthority(myUser.getRole()));
UserDetails userDetails = new User(myUser.getUserName(), myUser.getPassword(), authlist);
return userDetails; // UserDetails 객체를 이용해 스프링 시큐리티가 계정과 비밀번호 유효성 검증을 하고 권한도 검증한다.
}
}
AuthSuccessHandler
public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
// 1) 요청 캐쉬 객체를 직접 생성해서 클라이언트가 요청 시 요청과정에서 발생하는 여러 정보들을 세션에 담아 두고 필요시 꺼내어 쓰기 위한 기능이다.
private RequestCache requestCache = new HttpSessionRequestCache();
// 2) 생성자에서 부모 객체에 전달
public AuthSuccessHandler() {
super.setRequestCache(requestCache);
}
// 로그인 성공 이후 자동으로 호출되는 메소드
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 추가로직 구현, 로깅 처리 .., 세션 처리 .. , 예외 처리 .. 등
// 세션 유지 시간 설정
HttpSession session = request.getSession();
session.setMaxInactiveInterval(60 * 20); // 초 단위로 설정
// 3) 로그인 성공 이후 미리 저장된 요청이 있었는지 읽어와서
SavedRequest cashed = requestCache.getRequest(request, response);
// 4) 만일 미리 저장된 요청이 없다면 (로그인하지 않은 상태로 인증이 필요한 경로를 요청하지 않았다면)
if (cashed == null) {
// 5) 로그인 환영 페이지로 foward 이동
RequestDispatcher rd = request.getRequestDispatcher("/user/login_success");
rd.forward(request, response);
} else {
// 6) 원래 가려던 목적지 경로로 리다이렉트 시킨다 (GET 방식 요청 파라미터도 자동으로 같이 가지고 간다) SecurityConfig
// 클래스에서 http.formLogin().successHandler(new CustomAuthenticationHandler())로 설정한다.
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
💡 SavedRequestAwareAuthenticationSuccessHandler :
인증에 성공하게 되면 기본적으로 시큐리티가 SavedRequestAwareAuthenticationSuccessHandler 클래스를 사용한다. 그래서 savedRequest의 생성여부에 따른 대한 처리를 별도로 할 필요가 없다.
다만 AuthenticationSuccessHandler에서 특정한 로직을 처리해야 하거나 추가적인 기능을 사용해야 한다면 SavedRequestAwareAuthenticationSuccessHandler 클래스를 상속해서 기본 기능은 부모 클래스에게 맡기고 커스터마이징 할 부분을 별도로 처리한다.
🙏 Spring Boot - (7-1) Login 성공 시점에 처리 로직 구현
🙏 SavedRequestAwareAuthenticationSuccessHandler
UserController
@Controller
public class UserController {
@GetMapping("/staff/user/list")
public String userlist() {
return "user/list";
}
@GetMapping("/admin/user/manage")
public String usermanage() {
return "user/manage";
}
@GetMapping("/user/required_loginform")
public String required_loginform() {
return "user/required_loginform";
}
@GetMapping("/user/loginform")
public String loginform() {
return "user/loginform";
}
@PostMapping("/user/login_success")
public String login_success() {
return "user/login_success";
}
@PostMapping("/user/login_fail")
public String login_fail() {
return "user/login_fail";
}
@GetMapping("/user/denied") // 권한 부족 시
public String denied() {
return "user/denied";
}
@GetMapping("/user/expired") // 세션 허용 갯수 초과 시
public String expired() {
return "user/expired";
}
}
required_loginform.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>사용자 정보 예</h3>
<table border="1">
<tr>
<th>id</th><th>password</th><th>role</th>
</tr>
<tr>
<td>guest</td><td>1234</td><td>ROLE_USER</td>
</tr>
<tr>
<td>batman</td><td>1234</td><td>ROLE_STAFF</td>
</tr>
<tr>
<td>superman</td><td>1234</td><td>ROLE_ADMIN</td>
</tr>
</table>
<h3>로그인</h3>
<form th:action="@{/user/login}" method="post">
사용자명 : <input type="text" name="userName" /><br/>
비밀번호 : <input type="password" name="password" /><br/>
<button type="submit">로그인</button>
</form>
</body>
</html>
login_success.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<strong sec:authentication="name"></strong>님 잘왔다!
<br/>
<a th:href="@{/}">메인으로</a>
</body>
</html>
login_fail.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript">
alert("사용자명 또는 비밀번호 불일치");
location.href="[[@{/user/loginform}]]";
</script>
</body>
</html>
denied.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>해당 페이지를 요청할 권한이 없다</h3>
<br/>
<a th:href="@{/}">메인으로</a>
</body>
</html>
expired.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>다른 기기에서 로그인 중이다. 로그아웃 되었다. (세션 허용 갯수 1개 초과)</h3>
<br/>
예를 들어 크롬에서 로그인한 후 동일 계정으로 엣지(다른 브라우저)에서 로그인한 후 크롬에서 새로고침한다.
</body>
</html>
로그인 실패 시
권한 없을 경우
놀이공간 클릭 시 로그인 페이지로 이동
구입하기 클릭 시 로그인 페이지로 이동
또한 나머지 회원목록, 관리 클릭 시 로그인 페이지로 이동된다.
USER 권한
staff 페이지 접근 불가
admin 페이지 접근 불가
STAFF 권한
staff 페이지 접근 가능
admin 페이지는 접근 불가
ADMIN 권한
모든 페이지 접근 가능
최대 허용 세션
크롬에서 로그인
엣지에서 로그인
크롬에서 새로고침하면 크롬에서는 자동으로 로그아웃
'Study > Acorn' 카테고리의 다른 글
241231 도커 (nodejs 웹서버, 우분투 자바 설치, 도커 허브 푸쉬) (0) | 2024.12.31 |
---|---|
241230 도커 (설치, 기본명령어) (0) | 2024.12.30 |
241226 인증/인가 (JWT, 권한) (0) | 2024.12.26 |
241224 인증 / 인가 (권한, Security, JWT) (1) | 2024.12.24 |
241223 인증 / 인가 (0) | 2024.12.23 |