초기설정 -> web, thymeleaf, lombok -> project 생성
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
Bean 수동등록
기술적인 문제나 공통적인 관심사를 처리할 때 사용하는 객체들을 수동으로 등록하는 것
비즈니스 로직 Bean 보다는 그 수가 적기 때문에 수동으로 등록하기 부담스럽지 않음
수동등록된 Bean에서 문제가 발생했을 때 해당 위치를 파악하기 쉽다는 장점이 있음
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //hash함수
}
}
method에 @Bean 등록 class에 @Configuration 등록
passwordEncoderTestcode
package com.sparta.springauth;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class PasswordEncoderTest {
@Autowired
PasswordEncoder passwordEncoder;
public PasswordEncoderTest(PasswordEncoder passwordEncoder){
this.passwordEncoder = passwordEncoder;
}
@Test
@DisplayName("수동 등록한 passwordEncoder를 주입 받아와 문자열 암호화")
void test1(){
String password = "Robbi's password";
String encodePassword = passwordEncoder.encode(password);
System.out.println("encodePassword = " + encodePassword);
String inputPassword = "Robbie"; //encodePassword와 일치하지 않은 값을 대입
boolean matches = passwordEncoder.matches(inputPassword,encodePassword); //첫번쨰는 평문, 두번째는 암호화한 코드
System.out.println("matches = " + matches);
}
}
//
encodePassword = $2a$10$4uYpMhnt8OsjK3kdjYC8W.QzG7X78KWwlEfJhddeEazIIuWDab2/u //암호화완료
matches = false
PasswordConfig클래스 중 PasswordEncoder @Bean등록하여 PasswordEncoderTest 에서 사용
Bean이 여러개 있을 때
첫번째방법
사진 설명을 입력하세요.
Bean이 여러개 있으니 선택해줘야함
사진 설명을 입력하세요.
선택하면 됨
Autowird는 Bean타입으로 DI를 지원하는데 연결이 되지않으면 Bean의 이름으로 찾음
두번째방법
@Primary를 주입하면 Bean이 여러개 있더라도 Primary Bean을 우선적으로 주입함
사진 설명을 입력하세요.
세번째방법
사진 설명을 입력하세요.
@Qulifier 주입
사진 설명을 입력하세요.
pizza Bean을 @Qualifier을 통해서 주입함
@Qualifier이 @Primary보다 우선순위가 높음(좁은범위가 우선적으로 잡힘)
인증과 인가
인증
유저가 실제 유저인지 인증하는 개념
인가
해당 유저가 특정 리소스 접근이 가능한지 허가를 확인하는 개념
HTTP프로토콜을 이용해서 통신을 함, 이 통신은 비연결성 무상태로 이루어져 있음
비연결성 - 서버와 클라이언트가 연결이 되어 있지 않다.(리소스를 절약하기 위해서)
무상태 - 서버가 클라이언트의 상태를 저장하고 있지 않다.
웹 애플리케이션은 두가지 방법을 통해서 인증을 처리함
쿠키-세션 방식의 인증
쿠키 - 저장 공간
세션 - 인증 정보
사진 설명을 입력하세요.
JWT 기반 인증
JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미
사진 설명을 입력하세요.
쿠키-세션인증방식과 JWT방식의 다른점은 세션저장소 차이
쿠키와 세션
HTTP에 상태 정보를 유지하기 위해 사용
쿠키와 세션을 통해 서버에서는 클라이언트 별로 인증 및 인가를 할 수 있음
쿠키
클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
개발자도구 -> Application에서 쿠키와 세션 확인 가능
구성요소
- Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)
- Value (값): 쿠키의 값
- Domain (도메인): 쿠키가 저장된 도메인
- Path (경로): 쿠키가 사용되는 경로
- Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됩니다.)
세션
- 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
- 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장
- 서버에서 생성한 '세션 ID'는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용
사진 설명을 입력하세요.
쿠키 생성 조회
package com.sparta.springauth.auth;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@RestController
@RequestMapping("/api")
public class AuthController {
public static final String AUTHORIZATION_HEADER = "Authorization";
@GetMapping("/create-cookie")
public String createCookie(HttpServletResponse res) {
addCookie("Robbie Auth", res);
return "createCookie";
}
@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
//@CookieValue("Authorization") 값을 value에 할당
System.out.println("value = " + value);
return "getCookie : " + value;
}
public static void addCookie(String cookieValue, HttpServletResponse res) {
try {
cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20");
// Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
cookie.setPath("/"); //path
cookie.setMaxAge(30 * 60); //만료기한
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
}
HttpServletResponse에 들어있는 쿠키 중 Authorization이라는 이름으로 된 쿠키를 @CookieValue을 통해서 가져옴
쿠키를 생성할 때 name - value형식으로 cookieValue를 만들었음 Authorization이라는 name의 RobbieAuth value를 반환함
사진 설명을 입력하세요.
사진 설명을 입력하세요.
사진 설명을 입력하세요.
get cookie -> Authorization=value로 값이 잘 들어옴
set cookie -> Response에서 보면 설정 값 나옴
세션 생성 조회
@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
HttpSession session = req.getSession(true);
// 세션에 저장될 정보 Name - Value 를 추가합니다.
session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");
return "createSession";
}
@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
HttpSession session = req.getSession(false);
String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
System.out.println("value = " + value);
return "getSession : " + value;
}
JWT
JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
즉, 토큰의 한 종류라고 생각하면됨 . 일반적으로 쿠키 저장소를 사용하여 JWT를 저장
서버가 1대인 경우
사진 설명을 입력하세요.
Session1이 모든 Clinet 로그인 정보를 가지고 있음
서버가 2대 이상인 경우
사진 설명을 입력하세요.
Session마다 다른 Clinet로그인 정보를 가지고 있을 수 있음
만약 Client1의 로그인 정보를 가지고 있지 않은 Server에 Api요청을 할 수도 있음
해결방법
1.Sticky Session : Client 마다 요청 Servere 고정
2.세션 저장소 생성하여 모든 세션을 저장
사진 설명을 입력하세요.
Session Storage가 모든 Client의 정보를 소유하고 있기 때문에 모든 Server에서 Client의 API요청 처리 가능
3.JWT사용
로그인 정보를 Server 에 저장하지 않고, Client 에 로그인 정보를 JWT 로 암호화하여 저장 → JWT 통해 인증/인가
사진 설명을 입력하세요.
모든 Server에서 동일한 Secret Key를 소유해야함
사진 설명을 입력하세요.
로그인 암호화할때, JWT를 검증할때 Secret Key사용
JWT 장/단점
장점
동시 접속자가 많을 때 서버 측 부하 낮춤
Client, Sever 가 다른 도메인을 사용할 때
예) 카카오 OAuth2 로그인 시 JWT Token 사용
단점
구현의 복잡도 증가
JWT 에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
기 생성된 JWT 를 일부만 만료시킬 방법이 없음
Secret key 유출 시 JWT 조작 가능(JWT에는 민감한 정보를 넣어두면 안됨)
JWT흐름
1.Client가 id, password로 로그인 성공 시
서버에서 로그인정보 -> SecretKey -> JWT암호화
서버에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달(쿠키(JWT) -> Client)
브라우저 쿠키 저장소에 자동으로 JWT저장
2.Client에서 JWT 통해 인증 방법
서버에서 API요청 시 쿠키에 포함된 JWT를 찾아서 사용
- 쿠키를 찾는 코드
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8");
// Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
//쿠키에 담긴 정보가 여러 개일 수 있기 때문에 그 중 이름이 JWT가 담긴 쿠키의 이름과 동일한지 확인하여 JWT를 가져옴
Server
Client가 전달한 JWT 위조 여부 검증(Select Key사용)
JWT유효시간이 지나지 않았는지 검증
검증 성공시 JWT에서 사용자 정보를 가져와 확인
JWT구조
JWT 는 누구나 평문으로 복호화 가능
하지만 Secret Key 가 없으면 JWT 수정 불가능
→ 결국 JWT 는 Read only 데이터
jwt.io
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
Payload에 실제 유저의 정보가 들어있고, HEADER와 VERIFY SIGNATURE부분은 암호화 관련된 정보 양식이라고 생각
JWT다루기
build.gradle에 라이브러리 추가
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
application.properties에 secretKey 추가
Base64로 Encording한 것, 사용할 때는 Base64로 Decording해야함
jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==
JwtUtil
Util클래스란 특정 파라미터에 대한 작업을 수행하는 메서드들이 존재하는 클래스
다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스
JWT관련 기능들을 가진 JwtUtil이라는 클래스를 만들어 JWT관련 기능을 수행
JWT 관련 기능
1. JWT 생성
2. 생성된 JWT를 Cookie에 저장
3. Cookie에 들어있던 JWT 토큰을 Substring
4. JWT 검증
5. JWT에서 사용자 정보 가져오기
토큰 생성에 필요한 JWT데이터
package com.sparta.springauth.jwt;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Base64;
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자, Token앞에 붙임 구분을 위해 공백추가(규칙같은 거)
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}") //@value($"{}") -> application.properties Base64 Encode 한 SecretKey
private String secretKey;
private Key key; //JWT암호화 복호화할때 사용하기 위함
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 로그 설정(Logging(App동작동안 프로젝트의 상태나 동작 정보를 시간순으로 기록함을 의미)을 위해)
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
//@SL4J로 Logging설정도 가능
@PostConstruct//딱 한번만 받아오면 되는 값을 사용할 때마다 요청을 새로 호출하는 실수를 방지할 때 사용
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey); //decode
key = Keys.hmacShaKeyFor(bytes); //변환 후 key값에 SecretKey가 담김
}
}
권한을 관리하는 InnerClass생성
Entity 패키지 -> Eurm 생성
package com.sparta.springauth.Entity;
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
토큰생성
//JWT 생성(토큰생성)
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료시간(현재시간 + 설정시간)
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
생성된 JWT를 Cookie에 저장
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
Cookie에 들어있던 JWT토큰을 substring
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { //공백,null이 아닌지 && BEARER_PREFIX로 시작하는지
return tokenValue.substring(7); //BEARER_PREFIX = "BEARER " 공백 까지 7자 제외
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
JWT검증
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); //위변조 만료 검증
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
JWT에서 사용자 정보 가져오기
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
테스트
@GetMapping("/create-jwt")
public String createJwt(HttpServletResponse res) {
// Jwt 생성
String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);
// Jwt 쿠키 저장
jwtUtil.addJwtToCookie(token, res);
return "createJwt : " + token;
}
@GetMapping("/get-jwt")
public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if(!jwtUtil.validateToken(token)){
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
// 사용자 username
String username = info.getSubject();
System.out.println("username = " + username);
// 사용자 권한
String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
System.out.println("authority = " + authority);
return "getJwt : " + username + ", " + authority;
}
사진 설명을 입력하세요.
JWT코드 jwt.io에서 json형태로 변환가능
회원가입 기능 구현
초기설정
DB랑 연결하기 위해 MySQL, JPA 라이브러리 추가
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'
spring.datasource.url=jdbc:mysql://localhost:3306/auth
spring.datasource.username=root
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
- 0열 선택0열 다음에 열 추가
- 1열 선택1열 다음에 열 추가
- 2열 선택2열 다음에 열 추가
- 3열 선택3열 다음에 열 추가
- 0행 선택0행 다음에 행 추가
- 1행 선택1행 다음에 행 추가
- 2행 선택2행 다음에 행 추가
- 3행 선택3행 다음에 행 추가
- 4행 선택4행 다음에 행 추가
- 5행 선택5행 다음에 행 추가
컬럼명
|
컬럼타입
|
중복허용
|
설명
|
id
|
Long
|
X
|
테이블 ID(PK)
|
username
|
String
|
X
|
회원 ID
|
password
|
String
|
O
|
패스워드
|
email
|
String
|
X
|
이메일 주소
|
role
|
String
|
O
|
역할
1)사용자 USER 2)관리자 ADMIN |
- 셀 병합
- 행 분할
- 열 분할
- 너비 맞춤
- 삭제
user -> 암호화 -> DB저장
암호화는 양방향과 단방향이 있음
양방향 암호 알고리즘
암호화: 평문 → (암호화 알고리즘) → 암호문
복호화: 암호문 → (암호화 알고리즘) → 평문
단방향 암호 알고리즘
암호화: 평문 → (암호화 알고리즘) → 암호문
복호화: 불가 (암호문 → (암호화 알고리즘) → 평문)
Password확인 절차
사용자가 로그인을 위해 아이디 패스워드(평문) 으로 입력
DB에 저장된 아이디 패스워드(암호문)과 일치 여부 확인
// 사용예시
// 비밀번호 확인
if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) {
throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
}
boolean matches(CharSequence rawPassword, String encodedPassword);
rawPassword : 사용자가 입력한 비밀번호
encodedPassword : 암호화되어 DB 에 저장된 비밀번호
회원가입 API설계
- 0열 선택0열 다음에 열 추가
- 1열 선택1열 다음에 열 추가
- 2열 선택2열 다음에 열 추가
- 0행 선택0행 다음에 행 추가
- 1행 선택1행 다음에 행 추가
- 2행 선택2행 다음에 행 추가
Name
|
Method
|
URL
|
회원가입 페이지
|
GET
|
/api/user/signup
|
회원가입
|
POST
|
/api/user/signup
|
- 셀 병합
- 행 분할
- 열 분할
- 너비 맞춤
- 삭제
로그인 구현
로그인API설계
- 0열 선택0열 다음에 열 추가
- 1열 선택1열 다음에 열 추가
- 2열 선택2열 다음에 열 추가
- 3열 선택3열 다음에 열 추가
- 0행 선택0행 다음에 행 추가
- 1행 선택1행 다음에 행 추가
- 2행 선택2행 다음에 행 추가
Name
|
Method
|
URL
|
설명
|
로그인 페이지
|
Get
|
/api/user/login-page
|
회원가입 페이지
|
로그인
|
POST
|
/api/user/login
|
회원가입
|
- 셀 병합
- 행 분할
- 열 분할
- 너비 맞춤
- 삭제
Filter
사진 설명을 입력하세요.
Filter란 Web 애플리케이션에서 관리되는 영역으로 Client로 부터 오는 요청과 응답에 대해 최초/최종 단계의 위치이며 이를 통해 요청과 응답의 정보를 변경하거나 부가적인 기능을 추가할 수 있음
주로 범용적으로 처리해야하는 작업들, ex) 로깅, 보안 처리에 활용
또한 인증, 인가와 관련된 로직들을 처리할 수도 있음
Filter를 사용하면 인증, 인가와 관련된 로직을 비즈니스 로직과 분리하여 관리할 수 있다는 장점이 있음
사진 설명을 입력하세요.
Filter는 한개만 존재하는 것이 아니라 이렇게 여러개가 Chain형식으로 묶여서 처리될 수 있음
Request URL Logging
package com.sparta.springauth.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j(topic = "LoggingFilter") //name 명시해준것
@Component
@Order(1)//Filter 순서
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 전처리, 요청을 먼저 수행
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI(); //URI정보 가져옴
log.info(url); //요청 로그 확인
chain.doFilter(request, response); // 다음 Filter 로 이동 FilterChain을 받아온 이유가 Filter 이동하기위해
// 후처리
log.info("비즈니스 로직 완료");
}
}
AuthFilter : 인증 및 인가 처리 필터
package com.sparta.springauth.filter;
import com.sparta.springauth.entity.User;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException;
@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
public class AuthFilter implements Filter {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
public AuthFilter(UserRepository userRepository, JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI();
//StringUtils.hasText -> 문자 유효성 검사(null, 공백 등)
if (StringUtils.hasText(url) && (url.startsWith("/api/user") || url.startsWith("/css") || url.startsWith("/js"))) {
log.info("인증처리를 하지 않은 url : " + url);
// 회원가입, 로그인 관련 API 는 인증 필요없이 요청 진행
chain.doFilter(request, response); // 다음 Filter 로 이동
} else {
// 나머지 API 요청은 인증 처리 진행
// 토큰 확인(JWT)
String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest);
if (StringUtils.hasText(tokenValue)) { // 토큰이 존재하면 검증 시작
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if (!jwtUtil.validateToken(token)) {
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
new NullPointerException("Not Found User")
);//해당 유저 있는지 검증(Select)
request.setAttribute("user", user);//user라는 name으로 user를 가져올 수 있음
chain.doFilter(request, response); // 다음 Filter 로 이동 -> ProductController로 감 ->LogginFilter로 감
} else {
throw new IllegalArgumentException("Not Found Token");
}
}
}
}
JWT가져오기
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
ProductController
package com.sparta.springauth.controller;
import com.sparta.springauth.entity.User;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/api")
public class ProductController {
@GetMapping("/products")
public String getProducts(HttpServletRequest req) {
System.out.println("ProductController.getProducts : 인증 완료");
User user = (User) req.getAttribute("user"); //AuthFilter set.Attribute에 name에 저장한 값을 가져옴
System.out.println("user.getUsername() = " + user.getUsername());
return "redirect:/";
}
}
order(1) -> order(2) -> ProductController -> order(1)
'Spring Security' 프레임워크
build.gradle
//Security강의까지의 라이브러리
dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
package com.sparta.springauth.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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 WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources(static,templates) 접근 허용 설정
.requestMatchers("/api/user/**").permitAll()//api/user/로 시작하는 모든 요청 허가
.anyRequest().authenticated() // 그 외 모든 요청은 인증 처리
//AuthFilter부분의 인증방식을 구현
);
// 로그인 사용
http.formLogin(Customizer.withDefaults());
return http.build();
}
}
CSRF란?
CSRF(사이트 간 요청 위조, Cross-site request forgery)
공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것
CSRF 설정이 되어있는 경우 html 에서 CSRF 토큰 값을 넘겨주어야 요청을 수신 가능
쿠키 기반의 취약점을 이용한 공격 이기 때문에 REST 방식의 API 에서는 disable 가능
POST 요청마다 처리해 주는 대신 CSRF protection 을 disable 함
Spring Security
Filter기반임
Session방식으로 동작을 함
www.google.com
Spring Security - Filter Chain
Spring에서 모든 호출은 DispatcherServlet을 통과하게 되고 이후에 각 요청을 담당하는 Controller 로 분배
이 때, 각 요청에 대해서 공통적으로 처리해야할 필요가 있을 때 DispatcherServlet 이전에 단계가 필요하며 이것이 Filter임
사진 설명을 입력하세요.
Spring Security도 인증 및 인가를 처리하기 위해 Filter를 사용하는데 Spring Security는 FilterChainProxy를
통해서 상세로직을 구현하고 있음
사진 설명을 입력하세요.
인증이 되지않으면 login을 반환함
UsernamePasswordAuthenticationFilter(인증 로직)
사진 설명을 입력하세요.
- 인증 과정
- 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilter는 인증된 사용자의 정보가 담기는 인증 객체인 Authentication의 종류 중 하나인 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에게 넘겨 인증을 시도합니다.
- 실패하면 SecurityContextHolder를 비웁니다.
- 성공하면 SecurityContextHolder에 Authentication를 세팅합니다.
SecurityContextHolder
사진 설명을 입력하세요.
// 예시코드
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
context.setAuthentication(authentication); // SecurityContext 에 인증 객체 Authentication 를 저장.
SecurityContextHolder.setContext(context);
Autyentication
principal : 사용자를 식별
- Username/Password 방식으로 인증할 때 일반적으로 UserDetails 인스턴스
credentials : 주로 비밀번호, 대부분 사용자 인증에 사용한 후 비움
authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용
<UserDetails>
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
Spring Security Login
사진 설명을 입력하세요.
로그인 처리 과정
사진 설명을 입력하세요.
Manager인증처리를 함 그때 UserDetails Service를 사용해서 DB 해당 회원을 조회함
조회한 내용을 UserDetails를 만들어서 Manager로 전달. 실패하면 오류 성공하면 Session을 클라이언트에 반환
Security Login부분 Default 사용하지 않고 구현
package com.sparta.springauth.config;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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 WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
// 로그인 사용(디폴트값 -> 만든 로그인페이지)
http.formLogin((formLogin) ->
formLogin
// 로그인 View 제공 (GET /api/user/login-page)
.loginPage("/api/user/login-page")
// 로그인 처리 (POST /api/user/login) //구현했던 로그인 @POST가 아닌 Security가 먼저 수행하는 것
.loginProcessingUrl("/api/user/login")
// 로그인 처리 후 성공 시 URL
.defaultSuccessUrl("/")
// 로그인 처리 후 실패 시 URL
.failureUrl("/api/user/login-page?error")
.permitAll()
);
return http.build();
}
}
package com.sparta.springauth.security;
import com.sparta.springauth.entity.User;
import com.sparta.springauth.repository.UserRepository;
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;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
}
package com.sparta.springauth.security;
import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class UserDetailsImpl implements UserDetails {
private final User user;
public UserDetailsImpl(User user) {
this.user = user;
}
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
package com.sparta.springauth.controller;
import com.sparta.springauth.entity.User;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/api")
public class ProductController {
@GetMapping("/products")
public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
// @HttpServletRequest req
// System.out.println("ProductController.getProducts : 인증 완료");
// User user = (User) req.getAttribute("user"); //set.Attribute가져옴
// System.out.println("user.getUsername() = " + user.getUsername());
User user = userDetails.getUser();
System.out.println("user.getUsername() = " + user.getUsername());
return "redirect:/";
}
}
@AuthenticationPrincipal -> principal 부분에 userDetails 가져옴
Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
package com.sparta.springauth.dto;
import jakarta.validation.constraints.*;
import lombok.Getter;
@Getter
public class ProductRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Positive(message = "양수만 가능합니다.")
private int price;
@Negative(message = "음수만 가능합니다.")
private int discount;
@Size(min=2, max=10)
private String link;
@Max(10)
private int max;
@Min(2)
private int min;
}
Validation 예외처리
@PostMapping("/user/signup")
public String signup(@Valid SignupRequestDto requestDto, BindingResult bindingResult) {
// Validation 예외처리
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
if(fieldErrors.size() > 0) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
}
return "redirect:/api/user/signup";
}
userService.signup(requestDto);
return "redirect:/api/user/login-page";
}
예외 발생시 BindingResult 객체에 오류에 대한 정보가 담김
예외 발생시 for문을 통해서 예외 정보 출력, 예외 미발생 시 userService 비즈니스 로직 수행
정규표현식 확인
@Pattern(regexp = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$")
@NotBlank
private String email;
'K-Digital Training > 내일배움캠프' 카테고리의 다른 글
숙련 2주차/Spring (0) | 2023.11.21 |
---|---|
입문 2주차/Spring (2) | 2023.11.21 |
입문 1주차/Spring (1) | 2023.11.21 |
자바 5주차 (1) | 2023.11.01 |
자바 4주차 (1) | 2023.11.01 |