241224 인증 / 인가

241224 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.


권한 부여

(어제 내용과 이어서 진행됩니다.)

login.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>로그인 페이지</h2>
	<form th:action="@{/login}" method="post">
		사용자명 : <input type="text" name="username"><br /> 비밀번호 : <input
			type="text" name="password"><br />
		<button type="submit">로그인</button>
	</form>

	<div th:if="${param.logout}">
		<p>성공적으로 로그아웃 되었다!</p>
	</div>

	<div th:if="${param.expired}">
		<p>세션 시간 만료로 로그아웃 되었다!</p>
	</div>
</body>
</html>

Testcontroller

@Controller
public class TestController {
	
	@GetMapping("/login")
	public String login() {
		return "login";
	}
}

SecurityConfig

authorize / formlogin / logout

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	// Spring Security의 Role(역할) : 사용자의 접근 권한을 그룹화하고 관리하는 개념
	// 시스템에서 사용자가 어떤 기능이나 경로에 접근할 수 있는 지를 결정
	// Role은 일반적으로 접두사 "ROLE_"을 포함. ex) ROLE_ADMIN, ROLE_USER ...
	// 사용자는 여러개의 롤을 가질 수 있다. Role의 이름은 마음대로 주면 된다. 정해진 것은 없다.
	// hasRole() : 내부적으로 ROLE_ADMIN을 확인, hasAuthority() : 접두사 없이 권한을 직접 확인
	// 장점 : 관리가 편리해진다. 확장성, 보안 강화, ...
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests(auth -> auth
					.requestMatchers("/admin").hasRole("ADMIN")
					.requestMatchers("/user").hasRole("USER")
					.requestMatchers("/tom").hasRole("TOM")
					.requestMatchers("/common").permitAll()
					.anyRequest().authenticated()
					)
			.formLogin(form -> form
					.loginPage("/login")
					.defaultSuccessUrl("/default", true)
					.permitAll()
					)
			.logout(logout -> logout
					.logoutUrl("/logout")
					.permitAll()
					);
		
		return http.build();			
	}
}

UserDetailsService / PasswordEncoder

// 임포트 확인
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	... 생략
	
	@Bean
	public UserDetailsService userDetailsService() {
		UserDetails adminUser = User.builder()
				.username("admin")
				.password(passwordEncoder().encode("admin123"))
				.roles("ADMIN")
				.build();
		
		UserDetails nomalUser = User.builder()
				.username("user")
				.password(passwordEncoder().encode("user123"))
				.roles("USER")
				.build();
		
		UserDetails tomUser = User.builder()
				.username("tom")
				.password(passwordEncoder().encode("tom123"))
				.roles("TOM")
				.build();
		
		return new InMemoryUserDetailsManager(adminUser, nomalUser, tomUser);
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

 

TestController

@Controller
public class TestController {
	
	@GetMapping("/login")
	public String login() {
		return "login";
	}
	
	@GetMapping("/default")
	public void defaultAfterLogin(Authentication authentication,
			HttpServletResponse response) throws IOException {
		// Authentication : 현재 로그인한 사용자에 대한 정보를 가지고 있다.
		for(GrantedAuthority authority : authentication.getAuthorities()) {
			String role = authority.getAuthority(); // 특정 권한(ROLE)을 문자열로 반환
			System.out.println(role);
			
			if(role.equals("ROLE_ADMIN")) {
				response.sendRedirect("/admin");
				return;
			} else if(role.equals("ROLE_USER")) {
				response.sendRedirect("/user");
				return;
			} else if(role.equals("ROLE_TOM")) {
				response.sendRedirect("/tom");
				return;
			}
		}
		
		response.sendRedirect("/common");
	}
	
	@GetMapping("/admin")
	public String adminPage(Model model) {
		model.addAttribute("msg", "관리자 권한");
		return "common";
	}
	
	@GetMapping("/user")
	public String userPage(Model model) {
		model.addAttribute("msg", "유저 권한");
		return "common";
	}
	
	@GetMapping("/tom")
	public String tomPage(Model model) {
		model.addAttribute("msg", "톰 권한");
		return "common";
	}
	
	@GetMapping("/common")
	public String commonPage(Model model) {
		model.addAttribute("msg", "모두에게 허용됨");
		return "common";
	}
}

common.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
[[${msg}]]
</body>
</html>


Security Session 실습

application.properties

spring.application.name=sprsecurity03session

#mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=1111

# jpa
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect

login.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>시큐리티으로 로그인 연습</h2>
<form th:action="@{/auth/login}" method="post">
	직원번호 : <input type="text" name="sabun"><br/>
	직원명 : <input type="text" name="irum"><br/>
	<input type="submit" value="확인">
</form>

<div th:if="${param.error}">
<p style="color: red">잘못된 정보로 로그인 실패</p>
</div>

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</body>
</html>

success.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>로그인 성공</h2>
<p>환영! [[${username}]]야</p>
즐겨봐라 <a th:href="@{/auth/gugu}">구구단</a>
<pre>
.
.
</pre>
<a th:href="@{/auth/logout}">로그아웃</a>
</body>
</html>

gugu.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
구구단</p>
<form th:action="@{/auth/gugu}" method="post">
단 입력 : <input type="text" name="num" value="2" required/><br/>
<input type="submit" value="확인">
</form>
</body>
</html>

guguresult.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>구구단 [[${num}]]단 결과</h2>
<ul>
	<li th:each="i:${#numbers.sequence(1, 9)}">
	[[${num}]] x [[${i}]] = [[${num*i}]] 
	</li>
</ul>
<br/>
<a th:href="@{/auth/gugu}">다시 입력</a>
<br/>
<a th:href="@{/auth/success}">로그인 성공 페이지</a>
</body>
</html>

Jikwon

@Entity
@Table(name = "jikwon")
@Getter
@Setter
public class Jikwon {
	@Id
	private Long jikwonno;
	private String jikwonname;
	
}

JikwonRepository

@Repository
public interface JikwonRepository extends JpaRepository<Jikwon, Long> {

}

CustomUserDetailService

@Service
// 사용자 인증 시 사용자 정보를 로드
public class CustomUserDetailService implements UserDetailsService {
	// UserDetailsService : 사용자 정보를 DB 또는 기타 저장소에서 로드 후 UserDetails 객체를 반환
	
	@Autowired
	private JikwonRepository jikwonRepository;
	
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Override
	public UserDetails loadUserByUsername(String sabun) throws UsernameNotFoundException {
		Long jikwonNo = Long.parseLong(sabun);
		
		Jikwon jikwon = jikwonRepository.findById(jikwonNo)
				.orElseThrow(() -> new UsernameNotFoundException("해당 직원이 없어"));
		
		return User.builder()
				.username(jikwon.getJikwonname())
				.password(passwordEncoder.encode(jikwon.getJikwonname()))
				.build();
	}
}

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authConf)
		throws Exception {
		//  AuthenticationManager는 인증 로직의 중심 역할을 하며, 요청을 처리한다.
		//  AuthenticationConfiguration은 Spring Security의 AuthenticationManager를 
		// 설정 및 관리하는 도우미 역할을 한다.
		return authConf.getAuthenticationManager();
	}
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http)
	 throws Exception {
		http
			.authorizeHttpRequests(auth -> auth
				.requestMatchers("/auth/login", "/auth/logout", "/static/**")
				.permitAll()
				.anyRequest() // 그 외 요청 URI에 대해선 인증 필수.
				.authenticated()
				)
			.formLogin(formLogin -> formLogin
				.loginPage("/auth/login")
				.loginProcessingUrl("/auth/login") // 로그인 폼 제출 시 처리할 URL
				.usernameParameter("sabun")
				.passwordParameter("irum")
				.defaultSuccessUrl("/auth/success", true)
				.permitAll()
				)
		.logout(logout -> logout
				.logoutUrl("/auth/logout")
				.logoutSuccessUrl("/auth/login")
				.invalidateHttpSession(true)  // 세션 무효화
				.clearAuthentication(true) // 인증 정보 무효화
				.deleteCookies("JSESSIONID")
				.permitAll()
		);
		return http.build();
	}
	
	@Bean
	public PasswordEncoder getPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

AuthController

@Controller
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
	
	private final AuthenticationManager authenticationManager;
	
	@GetMapping("/login")
	public String login() {
		return "login";
	}
	
	@PostMapping("/login")
	public String performLogin(@RequestParam(name = "sabun")String sabun,
			@RequestParam(name = "irum")String irum, Model model) {
		try {
			// 두 개의 매개변수를 기반으로 인증 토큰을 생성
			UsernamePasswordAuthenticationToken token = 
					new UsernamePasswordAuthenticationToken(sabun, irum);
			
			// 인증 매니저로 인증 시도
			// CustomUserDetailService의 loadUserByUsername()을 호출하여 사용자 정보를 얻음
			Authentication authentication = authenticationManager.authenticate(token);
			
			// 인증 성공 시, SecurityContextHolder에 인증 객체가 저장
			SecurityContextHolder.getContext().setAuthentication(authentication); 
			
			return "redirect:/auth/success";
			
		} catch (AuthenticationException e) {
			model.addAttribute("error", "로그인 실패");
			return "login";
		}
	}
	
	@GetMapping("/success")
	public String success(Model model) {
		// SecurityContextHolder에 보관하고 있는 사용자 인증 객체를 얻음
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		
		String username = authentication.getName();
		model.addAttribute("username", username);
		
		return "success";
	}
	
	@GetMapping("/logout")
	public String logout() {
		return "redirect:/auth/login";
	}
}

처음 세션은 기본적으로 만들어진 세션

로그인 후 세션은 시큐리티에 의해 만들어진 세션

로그아웃, 세션은 흔적이 남음

@Controller
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
	
    ... 생략
    
	@GetMapping("/gugu")
	public String gugu() {
		// SecurityContextHolder에 보관하고 있는 사용자 인증 객체를 얻음
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		
		if(authentication == null || !authentication.isAuthenticated()) { // isAuthenticated() : 사용자 인증 여부를 확인,
			return "redirect:/auth/login";
		}
		return "gugu";
	}
	
	@PostMapping("/gugu")
	public String guguResult(@RequestParam("num")int num, Model model) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		
		if(authentication == null || !authentication.isAuthenticated()) { // isAuthenticated() : 사용자 인증 여부를 확인,
			return "redirect:/auth/login";
		}
		model.addAttribute("num", num);
		return "guguresult";
	}
}


Security JWT 실습

이전 실습이었던 Security Session의 소스 그대로 사용한다.

application.properties

JWT 디버깅 추가

spring.application.name=sprsecurity04jwt

#mariadb server connect
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=1111

# jpa
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect

logging.level.org.springframework.security=DEBUG

build.gradle

JWT 의존성 추가

dependencies {
	... 생략
    
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

JwtUtil

// JWT를 생성하고 검증하는 클래스
@Component
public class JwtUtil {
	
	// 고정된 비밀 키 사용 (예제용)  최소 256비트 길이의 비밀 키
	private final String SECRET_KEY = "mySuperSecretKey12345678901234567890123456789012";
	private final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
	
	private Key getSigningKey() {
		return Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
	}
	
	public String generateToken(String sabun) {
		return Jwts.builder()
				.setSubject(sabun)
				.setIssuedAt(new Date())
				.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
				.signWith(getSigningKey())
				.compact();
	}
	
	public Claims extractClaims(String token) {
		return Jwts.parserBuilder()
				.setSigningKey(getSigningKey())
				.build()
				.parseClaimsJws(token)
				.getBody();
	}
	
	public String extractUsername(String token) {
		return extractClaims(token).getSubject();
	}
	
	// JWT 유효성 검증
	public boolean vaildateToken(String token) {
		try {
			Claims claims = extractClaims(token);
			return !claims.getExpiration().before(new Date()); // JWT가 만료되지 않은 경우 true 반환
		} catch (Exception e) {
			// 예외 상황 : 토큰 서명이 잘못된 경우, JWT 구조가 올바르지 않은 경우, 기타 ...
			return false;
		}
	}
}

다음 시간에 이어서!

'Study > Acorn' 카테고리의 다른 글

241223 인증 / 인가  (0) 2024.12.23
241220 인증 / 인가  (0) 2024.12.20
241219 AWS RDS  (0) 2024.12.19
241218 AWS EC2  (0) 2024.12.18
241217 가상화 (하이퍼바이저 프로그램 배포, AWS 실습)  (1) 2024.12.17