241223 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.
인증, 인가
이전 시간에 세션과 JWT만을 이용한 인증, 인가 절차를 배워보았다.
이번 시간에는 본격적으로 스프링 시큐리티와 JWT를 배워보자.
시큐리티는 기본적으로 세션을 이용하지만 세션 말고 JWT와 연계하여 사용하게 된다면
확장성과 SPA를 구현하기에 더욱 더 좋다.
🙏 spring security는 session, jwt
Spring Boot JWT 실습
이전 시간에 했던 의존성 동일하게 사용 / 출력 부분도 복붙해주자
build.gradle
0.11.5 버전 사용, build.gradle에 의존성 부여해주고 리프레쉬!
0.11 버전
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
0.11 버전
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
0.12 버전
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
login.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>JWT으로 로그인 연습</h2>
<form th:action="@{/login}" method="post">
ID : <input type="text" name="userid"><br/>
PW : <input type="text" name="password"><br/>
<input type="submit" value="확인">
</form>
<div th:if="${param.error}">
<p style="color: red">잘못된 정보로 로그인 실패</p>
</div>
</body>
</html>
JwtService
package pack.service;
import java.security.Key;
import java.util.Date;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
@Service
public class JwtService {
private Key key;
@PostConstruct
public void init() {
key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 시크릿키를 암호화 알고리즘을 사용하여 암호화 한다. HS256을 가장 일반적으로 사용
}
public String createToken(String id) {
return Jwts.builder()
.setSubject(id) // 주체 설정
.setIssuedAt(new Date()) // 생성 시간
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 만료시간 (1시간)
.signWith(key) // 암호화 키
.compact();
}
// 주어진 토큰에서 사용자 id를 추출하기 위함
public String getUseridFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
AuthController
package pack.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import pack.service.JwtService;
@Controller
public class AuthController {
@Autowired
private JwtService jwtService;
@GetMapping("/login")
public String login() {
return "login";
}
@PostMapping("login")
public String login(@RequestParam("userid") String id,
@RequestParam("password") String password,
HttpServletResponse response) {
String validId = "yyummmmmmmm";
String validPw = "1234";
if(id.equals(validId) && password.equals(validPw)) {
String token = jwtService.createToken(id);
Cookie cookie = new Cookie("jwt", token);
cookie.setHttpOnly(true); // 클라이언트 스크립트에서 쿠키에 접근 불가
cookie.setPath("/"); // 모든 경로 허용
response.addCookie(cookie); // 클라이언트 쿠키에 담아 전달
return "redirect:/success";
} else {
return "redirect:/login?error";
}
}
@GetMapping("/success")
public String success(HttpServletRequest request, Model model) {
String userId = getUserIdFromToken(request);
if(userId == null) {
return "redirect:/login";
}
model.addAttribute("myuser", userId);
return "success";
}
private String getUserIdFromToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if(cookies != null) {
for(Cookie cookie:cookies) {
if(cookie.getName().equals("jwt")) {
String token = cookie.getValue();
return jwtService.getUseridFromToken(token); // ID 반환
}
}
}
return null; // 없으면 null 반환
}
}
AuthController 추가
@Controller
public class AuthController {
@Autowired
private JwtService jwtService;
... 생략
@GetMapping("/logout")
public String logout(HttpServletResponse response) {
Cookie cookie = new Cookie("jwt", null);
cookie.setMaxAge(0); // 쿠키 수명 0
cookie.setPath("/"); // 모든 경로 허용
response.addCookie(cookie); // 클라이언트 쿠키에 담아 전달
return "redirect:/login";
}
@GetMapping("/gugu")
public String gugu(HttpServletRequest request) {
String userId = getUserIdFromToken(request);
if (userId == null) {
return "redirect:/login";
} else {
return "gugu";
}
}
@PostMapping("gugu")
public String gugu(@RequestParam(name = "dan")int dan,
HttpServletRequest request, Model model) {
String userId = getUserIdFromToken(request);
if (userId == null) {
return "redirect:/login";
} else {
model.addAttribute("dan", dan);
return "guguresult";
}
}
}
시큐리티 실습 (session)
데이터의 중요성이 커지면서 고객 데이터의 효율적인 관리 능력 또한 중요해지고 있다.
그렇기에 인증과 인가에 대한 중요성이 더욱 더 부각되고 있다. 인증된 사람만이 데이터를 볼 수 있도록 한다.
Spring Security를 사용하는 이유
자바 개발자들이 보안 기능을 추가할 때 Spring Security를 사용하는 이유는 보안에 필요한 기능들을 제공하기 때문이다.
인증(Authentication) :
인증은 사용자의 신원을 입증하는 과정이다.
간단히 말해 어떤 사이트에 아이디와 비번을 입력하여 로그인 하는 과정이다.
인가(Authorization) :
'권한부여'나 '허가'와 같은 의미로 사용된다. 즉, 어떤 대상이 특정 목적을 실현하도록 허용하는 것을 말한다. 예를 들면, 파일 공유 시스템에서 권한별로 접근할 수 있는 폴더가 상이하다. 관리자는 접속이 가능하지만 일반 사용자는 접속할 수 없는 경우에서 사용자의 권한을 확인하게 되는데, 이 과정을 인가라고 한다.
스프링 시큐리티는 필터 기반으로 동작한다.
Spring Security는 '인증'과 '권한'에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
Filter는 DispatcherServlet을 만나기 전에 적용되므로 가장 먼저 URL 요청을 받지만, Interceptor는 Dispatcher와 Controller 사이에 위치한다는 점에서 적용 시기의 차이가 있다.
필터가 요청을 DispatcherServlet를 만나기 전에 가로챈다.
그러므로 Spring Security는 보안과 관련하여 다양한 옵션을 제공해주기 때문에 개발자는 일일이 보안 관련 로직을 작성하지 않아도 된다는 장점이 있다. 기본 구조는 다음과 같다.
🙏 spring security 참조 pdf 및 동영상 (2024년 기준)
build.gradle
security 관련 의존성을 주석 처리 후 리프레쉬 하자!, 우선 시큐리티 없이 진행
dependencies {
//implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
application.properties
spring.application.name=sprsecurity01basic
# 시큐리티 디버깅 확인
logging.level.org.springframework.security=DEBUG
WebController
package pack.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebController {
@GetMapping("/")
public String home() {
return "home";
}
}
home.html
(templates에 생성)
<!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="@{logout}" method="post">
<button type="submit">로그아웃</button>
</form>
</body>
</html>
시큐리티 없이는 바로 들어와진다.
build.gradle
주석처리한 시큐리티 관련 의존성 살리기
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
그러면 어떤 요청이 들어와도 무조건 로그인 페이지로 바로 이동한다.
시큐리티는 반드시 로그인(인증)을 하도록 요청을 인터셉트한다.
ID : user
Using generated security password: b91d732c-cc12-47ea-9393-376556c25882
패스워드의 경우 리로딩되면 바뀜, 바뀌는 패스워드로 로그인해야한다.
authorizeHttpRequests
SecurityConfig
package pack.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity // spring security 기능을 설정 (보안설정, 인증, 폼로그인...)
public class SecurityConfig {
// SecurityFilterChain : 요청을 가로채서 설정된 인증, 인가 부분을 체크
// HttpSecurity : 특정 http 요청에 대해 어떠한 보안 정책을 적용할 지 구성하는데 사용
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth // http 요청에 대한 보안 설정 부분
.requestMatchers("/", "/login", "/kbs", "/mbc/**").permitAll() // 해당 요청에 대해서는 인증 없이 springboot controller로 넘어감
.anyRequest().authenticated() // 그 외 요청들은 인증된 경우에만 접근 허용
);
return http.build();
}
}
WebController
@Controller
public class WebController {
... 생략
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/kbs")
public String kbs() {
return "media";
}
@GetMapping({"/mbc", "/mbc/sbs", "/mbc/sbs/5"})
public String mbc() {
return "media";
}
}
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>
media.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>대한민국 방송</h2>
</body>
</html>
허용된 요청들은 인증없이 다 들어가진다.
formlogin
SecurityConfig
@Configuration
@EnableWebSecurity // spring security 기능을 설정 (보안설정, 인증, 폼로그인...)
public class SecurityConfig {
// SecurityFilterChain : 요청을 가로채서 설정된 인증, 인가 부분을 체크
// HttpSecurity : 특정 http 요청에 대해 어떠한 보안 정책을 적용할 지 구성하는데 사용
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth // http 요청에 대한 보안 설정 부분
.requestMatchers("/login", "/kbs", "/mbc/**").permitAll() // 해당 요청에 대해서는 인증 없이 springboot controller로 넘어감
.anyRequest().authenticated() // 그 외 요청들은 인증된 경우에만 접근 허용
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/", true)
.permitAll()
);
return http.build();
}
}
로그인 후 home으로 이동, 세션을 지우고 다시 메인으로 요청하면 로그인 페이지로 이동 됨
logout
SecurityConfig
@Configuration
@EnableWebSecurity // spring security 기능을 설정 (보안설정, 인증, 폼로그인...)
public class SecurityConfig {
// SecurityFilterChain : 요청을 가로채서 설정된 인증, 인가 부분을 체크
// HttpSecurity : 특정 http 요청에 대해 어떠한 보안 정책을 적용할 지 구성하는데 사용
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
... 생략
.formLogin(form -> form
.loginPage("/login") // 사용자 정의 로그인 페이지 지정
.defaultSuccessUrl("/", true) // 인증에 성공한 후 redirect URL 지정
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout") // 기본으로 설정되어 있기 때문에 굳이 안써도 괜찮다.
.deleteCookies("JSESSIONID")
.permitAll()
);
return http.build();
}
}
UserDetailsService
사용자 입력 로그인 정보 사용
SecurityConfig
@Configuration
@EnableWebSecurity // spring security 기능을 설정 (보안설정, 인증, 폼로그인...)
public class SecurityConfig {
... 생략
// security가 제공하는 로그인 기본 정보 대신 사용자 입력 로그인 정보를 사용하기
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.builder()
.username("myuser")
.password("111")
.roles("USER")
.build();
// 인증 테스트용, RAM에 저장했기 때문에 정보가 사라진다.
return new InMemoryUserDetailsManager(userDetails);
}
}
PasswordEncoder
비밀번호 암호화
SecurityConfig
@Configuration
@EnableWebSecurity // spring security 기능을 설정 (보안설정, 인증, 폼로그인...)
public class SecurityConfig {
... 생략
// security가 제공하는 로그인 기본 정보 대신 사용자 입력 로그인 정보를 사용하기
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.builder()
.username("myuser") // 원래는 DB에 저장된 사용자의 이름과 비밀번호를 넣어준다.
.password(passwordEncoder().encode("111"))
//.roles("USER")
.build();
// 인증 테스트용, RAM에 저장했기 때문에 정보가 사라진다.
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 알고리즘 객체의 하나이다.
}
}
이제 더 이상 시큐리티가 제공하는 비밀번호는 없다.
Role (권한 부여)
이전에 사용했던 login.html 그대로 사용
TestController
@Controller
public class TestController {
@GetMapping("/login")
public String login() {
return "login";
}
}
SecurityConfig
@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()
);
return http.build();
}
}
'Study > Acorn' 카테고리의 다른 글
241224 인증 / 인가 (0) | 2024.12.24 |
---|---|
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 |