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 |