반응형
Spring Boot Security 3.x 환경에서 JWT를 이용한 로그인 과정에 대해 환경설정 및 구성 방법에 대해 알아봅니다.
💡 [참고] Spring Security 관련 글 및 Github Repository 경로입니다. 참고하시면 도움이 됩니다.
분류 | 상세 분류 | 주제 | 링크 |
Spring Boot 2.x | 이론 | Spring Boot Security 이해하기 -1 : 2.7.x 버전 구조 및 파일 이해 | https://adjh54.tistory.com/91 |
Spring Boot 2.x | 환경 설정 | Spring Boot Security 이해하기 -2 : 2.7.x 버전 구현하기 | https://adjh54.tistory.com/92 |
Spring Boot 2.x | 이론 | Spring Boot Security 이해하기 -3: JWT(JSON Web Token) 이해하기 | https://adjh54.tistory.com/93 |
Spring Boot 2.x | 환경 설정 | Spring Boot Security 이해하기 -4: JWT 환경 설정 및 구성 하기 | https://adjh54.tistory.com/94 |
Spring Boot 2.x | 소스코드 | Spring Boot Security 2.x + JWT 기반 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot2-security |
기타 | 이론 | Spring Boot 2.x.x 버전 프로젝트 생성: 지원 종료 및 다운그레이드 | https://adjh54.tistory.com/361 |
Spring Boot 3.x | 이론 | Spring Boot Security 3.x + JWT 이해하기 -1 : 구조 및 Client / Server 처리과정 | https://adjh54.tistory.com/576 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -2 : 환경 설정 및 구성 | https://adjh54.tistory.com/577 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -3 : Refresh Token 활용 | https://adjh54.tistory.com/583 |
Spring Boot 3.x | 환경 설정 | Spring Boot Security 3.x + JWT 이해하기 -4 : 로그아웃 + 블랙리스트 활용 | https://adjh54.tistory.com/592 |
Spring Boot 3.x | 소스코드 | Spring Boot Security 3.x + JWT 기반 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot3-security |
React | 소스코드 | Spring Boot 3.x와 통신하여 로그인을 수행하는 클라이언트 구성 Github Repository | https://github.com/adjh54ir/blog-codes/tree/main/react-login |
1) 구성 패키지 구조 및 설명
💡 구성 패키지 구조 및 설명
- Spring Boot Security 3.4.3 버전 + JWT 0.12.6 버전을 활용하여 구성한 프로젝트의 패키지 구조 및 설명입니다.
패키지 | 파일 명 | 설명 |
common.utils | TokenUtils | - JWT의 구성요소를 생성하고 최종적으로 JWT를 생성하여 유효성을 체크하는 유틸입니다. |
config | DataSourceConfig | - 데이터베이스와 연결 및 SQL Mapper(MyBatis)와의 연결을 담당하는 설정 클래스입니다. |
config | WebSecurityConfig | - Spring Security 환경 설정을 구성하기 위한 설정 클래스입니다. |
config.filter | CustomAuthenticationFilter | - 아이디와 비밀번호 기반의 데이터를 Form 데이터로 전송을 받아 '인증'을 담당하는 필터 역할의 클래스입니다. |
config.filter | JwtAuthorizationFilter | - 지정한 URL 별 JWT 유효성 검증을 수행하며 직접적인 사용자 '인증'을 확인하는 필터 역할의 클래스입니다. |
config.handler | CustomAuthenticationProvider | - 전달받은 사용자의 아이디와 비밀번호를 기반으로 비즈니스 로직을 처리하여 사용자의 ‘인증’에 대해서 검증을 수행하는 클래스입니다. |
config.handler | CustomAuthFailureHandler | - 사용자의 '인증'에 대해 실패하였을떄, 수행하여 사용자에게 응답 값을 제공해주는 Handler입니다. |
config.handler | CustomAuthSuccessHandler | - 사용자의 '인증'에 대해 성공하였을때, 수행하여 사용자에게 사용자 정보 및 JWT에 대한 응답 값을 제공해주는 Handler입니다. |
controller | TokenController | - 토큰 생성 테스트를 위해 임시로 구성한 Controller입니다. |
controller | UserController | - 로그인 테스트를 위해 임시로 구성한 Controller입니다. |
mapper | UserMapper | - 사용자 관련 SQL Mapper 인터페이스 입니다. |
model.dto | UserDetailsDto | - Spring Security에서 사용되는 UserDetailsDto 클래스 입니다. |
model.dto | UserDto | - tb_user 테이블과 매핑되는 DTO 클래스입니다. |
service | UserService | - 사용자 서비스 인터페이스입니다. |
service.impl | UserDetailsServiceImpl | - 사용자 인증 정보를 로드하고 UserDetails 객체를 생성하는 역할을 담당 |
service.impl | UserServiceImpl | - 사용자 서비스의 구현체 클래스입니다. |
2) 구성 개발환경
개발환경 | 분류 | 버전 | 설명 |
java jdk | java version | 17 | Java 개발 키트 |
spring-boot | spring boot starter | 3.3.4 | Spring Boot 프레임워크 |
spring-boot-web | spring boot starter | 3.3.4 | Spring Boot 웹 애플리케이션 개발 지원 |
spring-boot-security | spring boot starter | 3.3.4 | Spring Security 통합 |
org.mybatis.spring.boot:mybatis-spring-boot-starter | spring boot starter | 3.0.3 | MyBatis와 Spring Boot 통합 |
io.jsonwebtoken:jjwt | opensource | 0.12.6 | JSON Web Token 생성 및 검증 |
com.fasterxml.jackson.core:jackson-databind | opensource | 2.16.1 | JSON 데이터 바인딩 라이브러리 |
jakarta.xml.bind:jakarta.xml.bind-api | opensource | 4.0.2 | XML 바인딩 API |
org.postgresql:postgresql | opensource | - | PostgreSQL JDBC 드라이버 |
lombok | spring boot | - | Java 코드 생성 라이브러리 |
💡 [참고] Spring Security 6.3.3 기준 (* Spring Boot 3.x 기준) Java 17 이상을 사용해야 합니다.
3) 개발환경 구성 : 테이블 구성 및 데이터베이스 연결
💡 개발환경 구성 : 테이블 구성 및 데이터베이스 연결
- 로그인 기능을 구현하기 위해서 데이터베이스와의 연결 설정을 구성합니다.
1. 테이블 구조 : tb_user
컬럼 | 설명 |
user_sq | 사용자 시퀀스 |
user_id | 사용자 아이디 |
user_pw | 사용자 패스워드 |
user_nm | 사용자 이름 |
user_st | 사용자 상태 (S: 일반, D: 탈퇴) |
create table tb_user
(
user_sq serial
constraint tb_user_pk
primary key,
user_id text,
user_pw text,
user_nm text,
user_st text
);
alter table tb_user
owner to localmaster;
2. 라이브러리 설정
💡 라이브러리 설정
- 해당 데이터베이스 연결을 위해서 사용하는 Mybatis Framework와 postgreSQL 데이터베이스의 드라이버를 설치해 줍니다.
dependencies {
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
runtimeOnly 'org.postgresql:postgresql'
}
3. 패키지 구조
💡 패키지 구조
- 해당 개발 환경에서는 SQL Mapper인 MyBatis Framework를 통해서 환경을 구성합니다.
파일 | 설명 |
application.properties | - yml 파일을 통해서 환경설정을 구성하기 위해 서버 실행 시 application-loc.yml 파일을 바라보게 구성합니다 |
application-loc.yml | - 로컬 데이터베이스 연결을 위해서 환경 설정을 구성합니다 |
DataSourceConfig.java | - 로컬 데이터베이스와 MyBatis 연결을 위해 환경 클래스를 구성합니다 |
config/common-mybatis-config.xml | - MyBatis에서 사용하는 설정 정보를 지정합니다. |
sql/ddl.sql | - 임시로 사용자 테이블을 만들 수 있는 스크립트입니다. |
4. application.properties
💡 application.properties
- spring.profiles.active 속성을 loc로 지정함으로써 application-loc.yml 파일을 바라보도록 지정하였습니다.
spring.application.name=spring-boot3-security
spring.profiles.active=loc
💡[참고] 개발환경에 따라 각각 환경을 구성하는 방법에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
5. application-loc.yml
💡 application-loc.yml
- yaml 파일 형태로 spring boot의 datasource를 지정합니다.
- 기본적으로 HikariCP를 이용하며, PostgreSQL 데이터베이스와의 연결을 구성하였습니다.
# Spring Boot Configuration
spring:
# 1. Spring Boot JDBC + HikariCP 설정
datasource:
hikari:
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://localhost:5432/testdb
username: localmaster
password: qwer1234
pool-name: Hikari Connection Pool # Alias
maximum-pool-size: 5
6. DataSourceConfig.java
💡 DataSourceConfig.java
- DataSource에 대한 설정을 위한 설정 파일입니다.
- 해당 클래스의 주요한 역할은 MyBatis와 데이터베이스를 연결하는 역할을 수행합니다.
어노테이션/메서드 | 설명 |
@Configuration | 이 클래스가 Spring의 구성 클래스임을 나타냅니다 |
@PropertySource | application.properties 파일을 속성 소스로 사용함을 지정합니다 |
hikariConfig() | HikariCP 설정을 위한 빈을 생성합니다. spring.datasource.hikari 접두사를 가진 속성들을 사용합니다 |
dataSource() | HikariDataSource를 사용하여 DataSource 빈을 생성합니다 |
sqlSessionFactory() | MyBatis SqlSessionFactory를 설정합니다. 매퍼 위치, 타입 별칭 패키지, MyBatis 설정 파일 등을 지정합니다 |
sqlSessionTemplate() | SqlSessionTemplate 빈을 생성합니다. 이는 MyBatis 작업을 위한 템플릿입니다 |
package com.adjh.springboot3security.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 데이터베이스와 연결 및 SQL Mapper(MyBatis)와의 연결을 담당하는 설정 클래스입니다.
*
* @author : jonghoon
* @fileName : DataSourceConfig
* @since : 10/1/24
*/
@Configuration
@PropertySource("classpath:/application.properties")
public class DataSourceConfig {
final ApplicationContext applicationContext;
public DataSourceConfig(ApplicationContext ac) {
this.applicationContext = ac;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
public DataSource dataSource() {
return new HikariDataSource(hikariConfig());
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean session = new SqlSessionFactoryBean();
session.setDataSource(dataSource);
session.setMapperLocations(applicationContext.getResources("classpath:mapper/*.xml"));
session.setTypeAliasesPackage("com.adjh.springboot3security.model.dto");
session.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:config/common-mybatis-config.xml"));
return session.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
7. common-mybatis-config.xml
💡 common-mybatis-config.xml
- MyBatis의 설정 파일로, SQL 매퍼의 동작을 세부적으로 제어하는 데 사용됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--
해당 파일은 Mybatis 관련 환경 설정을 하는 파일입니다.
-->
<configuration>
<settings>
<!-- column name to camel case-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 쿼리 결과 필드가 null인 경우, 누락이 되서 나오지 않게 설정-->
<setting name="callSettersOnNulls" value="true"/>
<!-- 쿼리에 보내는 파라미터가 null인 경우, 오류가 발생하는 것 방지 -->
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
</configuration>
8. ddl.sql
💡 ddl.sql
- tb_user 테이블을 생성하기 위한 DDL SQL 스크립트입니다.
create table tb_user
(
user_sq serial
constraint tb_user_pk
primary key,
user_id text,
user_pw text,
user_nm text,
user_st text
);
alter table tb_user
owner to localmaster;
9. 중간 결과 확인
💡 중간 결과 확인
- 데이터베이스 연결이 완료되어 서버가 정상적으로 수행되었습니다.
반응형
4) 개발환경 : 소스코드 구성
1. 라이브러리 설정 : build.gradle
💡 라이브러리 설정 : build.gradle
- 이전에 데이터베이스 설정을 위한 라이브러리 설정에 아래의 라이브러리를 설치해 줍니다.
dependencies {
implementation "org.springframework.boot:spring-boot-starter-security:3.3.4"
implementation "org.springframework.boot:spring-boot-starter-web:3.3.4"
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
2. UserDto
💡 UserDto
- 클라이언트로부터 전달받은 사용자 아이디(userId), 패스워드(userPw)를 정의하며, 추가로 데이터베이스에서 조회되는 데이터를 조회해 올 때 사용이 됩니다.
package com.adjh.springboot3security.model.dto;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* tb_user 테이블과 매핑되는 DTO 클래스입니다.
*
* @author : jonghoon
* @fileName : UserDto
* @since : 10/1/24
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserDto {
// 사용자 시퀀스
private int userSq;
// 사용자 아이디
private String userId;
// 사용자 패스워드
private String userPw;
// 사용자 이름
private String userNm;
// 사용자 상태
private String userSt = "S";
@Builder(toBuilder = true)
private UserDto(int userSq, String userId, String userPw, String userNm, String userSt) {
this.userSq = userSq;
this.userId = userId;
this.userPw = userPw;
this.userNm = userNm;
this.userSt = userSt;
}
}
3. UserDetailDto
💡 UserDetailDto
- Spring Security에서 사용되는 UserDetails 인터페이스를 정의한 DTO 클래스입니다.
package com.adjh.springboot3security.model.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* Spring Security에서 사용되는 UserDetails 인터페이스를 정의한 DTO 클래스입니다.
*
* @author : jonghoon
* @fileName : UserDetailsDto
* @since : 10/1/24
*/
@Slf4j
@Getter
@AllArgsConstructor
public class UserDetailsDto implements UserDetails {
// @Delegate 어노테이션을 사용하여 UserDto 객체의 메서드를 이 클래스에서 직접 사용할 수 있게 합니다.
@Delegate
private UserDto userDto;
private Collection<? extends GrantedAuthority> authorities;
/**
* 사용자의 권한 목록을 반환합니다.
*
* @return Collection<? extends GrantedAuthority>
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
/**
* 사용자의 비밀번호를 반환합니다.
*
* @return String
*/
@Override
public String getPassword() {
return userDto.getUserPw();
}
/**
* 사용자의 이름을 반환합니다.
*
* @return String
*/
@Override
public String getUsername() {
return userDto.getUserNm();
}
/**
* 계정이 만료되지 않았는지 여부를 반환합니다.
* 현재 항상 false를 반환하므로, 모든 계정이 만료된 것으로 처리됩니다.
*
* @return boolean
*/
@Override
public boolean isAccountNonExpired() {
return false;
}
/**
* 계정이 잠기지 않았는지 여부를 반환합니다.
*
* @return boolean
*/
@Override
public boolean isAccountNonLocked() {
return false;
}
/**
* 자격 증명(비밀번호)이 만료되지 않았는지 여부를 반환합니다.
*
* @return boolean
*/
@Override
public boolean isCredentialsNonExpired() {
return false;
}
/**
* 계정이 활성화되어 있는지 여부를 반환합니다.
*
* @return boolean
*/
@Override
public boolean isEnabled() {
return false;
}
}
4. TokenUtils
💡 TokenUtils
- 해당 Utils에서는 지정한 규칙에 따라서 토큰을 생성하고 유효성을 체크하며 토큰을 기반으로 사용자 정보를 조회하는 유틸입니다.
package com.adjh.springboot3security.common.utils;
import com.adjh.springboot3security.model.dto.UserDto;
import com.adjh.springboot3security.model.dto.ValidTokenDto;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT의 구성요소를 생성하고 최종적으로 JWT를 생성하여 유효성을 체크하는 유틸입니다.
*
* @author lee
* @fileName TokenUtils
* @since : 10/1/24
*/
@Log4j2
@Component
public class TokenUtils {
private static SecretKey JWT_SECRET_KEY;
/**
* JWT_SECRET_KEY 변수값에 환경 변수에서 불러온 SECRET_KEY를 주입합니다.
*
* @param jwtSecretKey
*/
public TokenUtils(@Value("${jwt.secret}") String jwtSecretKey) {
TokenUtils.JWT_SECRET_KEY = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));
}
/**
* '토큰의 만료기간'을 지정하는 메서드
*
* @return {Date} Calendar
*/
private static Date createExpiredDate() {
Calendar c = Calendar.getInstance();
// c.add(Calendar.SECOND, 3); // 10초
c.add(Calendar.HOUR, 1); // 1시간
// c.add(Calendar.HOUR, 8); // 8시간
// c.add(Calendar.DATE, 1); // 1일
return c.getTime();
}
/**
* JWT의 '헤더' 값을 생성해주는 메서드
*
* @return HashMap<String, Object>
*/
private static Map<String, Object> createHeader() {
return Jwts.header()
.add("typ", "JWT")
.add("alg", "HS256")
.add("regDate", System.currentTimeMillis()).build();
}
/**
* '사용자 정보' 기반으로 'Claims' 생성하는 메서드
*
* @param userDto 사용자 정보
* @return Map<String, Object>
*/
private static Map<String, Object> createClaims(UserDto userDto) {
// 공개 클레임에 사용자의 이름과 이메일을 설정하여 정보를 조회할 수 있다.
Map<String, Object> claims = new HashMap<>();
log.info("userId :" + userDto.getUserId());
log.info("userNm :" + userDto.getUserNm());
claims.put("userId", userDto.getUserId());
claims.put("userNm", userDto.getUserNm());
return claims;
}
/**
* 토큰을 기반으로 유효한 토큰인지 여부를 반환해주는 메서드
* - Claim 내에서 사용자 정보를 추출합니다.
*
* @param token String : 토큰
* @return boolean : 유효한지 여부 반환
*/
public static boolean isValidToken(String token) {
try {
Claims claims = getTokenToClaims(token);
log.info("expireTime :" + claims.getExpiration());
log.info("userId :" + claims.get("userId"));
log.info("userNm :" + claims.get("userNm"));
return true;
} catch (ExpiredJwtException exception) {
log.debug("token expired " + token);
log.error("Token Expired" + exception);
return false;
} catch (JwtException exception) {
log.debug("token expired " + token);
log.error("Token Tampered" + exception);
return false;
} catch (NullPointerException exception) {
log.debug("token expired " + token);
log.error("Token is null" + exception);
return false;
}
}
/**
* 사용자 정보를 기반으로 토큰을 생성하여 반환 해주는 메서드
*
* @param userDto UserDto : 사용자 정보
* @return String : 토큰
*/
public static String generateJwt(UserDto userDto) {
log.debug("생성된 JWT Secret Key: " + JWT_SECRET_KEY);
// 사용자 시퀀스를 기준으로 JWT 토큰을 발급하여 반환해줍니다.
JwtBuilder builder = Jwts.builder()
.setHeader(createHeader()) // Header 구성
.claims(createClaims(userDto)) // Payload - Claims 구성
.subject(String.valueOf(userDto.getUserSq())) // Payload - Subject 구성
.signWith(JWT_SECRET_KEY) // Signature 구성
.expiration(createExpiredDate()); // Expired Date 구성
return builder.compact();
}
/**
* Refresh Token으로 기간을 14일로 지정합니다.
*
* @return
*/
private static Date createRefreshTokenExpiredDate() {
Calendar c = Calendar.getInstance();
c.add(Calendar.DATE, 14);
return c.getTime();
}
public static String generateRefreshToken(UserDto userDto) {
log.debug("JWT Secret Key: " + JWT_SECRET_KEY);
return Jwts.builder()
.setHeader(createHeader())
.claims(createClaims(userDto))
.subject(String.valueOf(userDto.getUserSq()))
.signWith(JWT_SECRET_KEY)
.expiration(createRefreshTokenExpiredDate())
.compact();
}
/**
* 'Header' 내에서 'Token' 정보를 반환하는 메서드
*
* @param header 헤더
* @return String
*/
public static String getHeaderToToken(String header) {
return header.split(" ")[1];
}
/**
* 'JWT' 내에서 'Claims' 정보를 반환하는 메서드
*
* @param token : 토큰
* @return Claims : Claims
*/
private static Claims getTokenToClaims(String token) {
System.out.println("확인111 : " + token);
System.out.println("확인222 : " + JWT_SECRET_KEY);
return Jwts.parser()
.verifyWith(JWT_SECRET_KEY)
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 'Claims' 내에서 '사용자 아이디'를 반환하는 메서드
*
* @param token : 토큰
* @return String : 사용자 아이디
*/
public static String getClaimsToUserId(String token) {
Claims claims = getTokenToClaims(token);
return claims.get("userId").toString();
}
}
5. application-loc.yml
💡 application-loc.yml
- 위에서 불러오는 고유한 JWT 키값을 환경설정 파일에서 지정을 합니다.
- 해당 KEY는 반드시 32자 이상으로 문자로 구성이 되어야 합니다. 이는 HS256 알고리즘을 사용하는데에 권고 사항입니다.
# Spring Boot Custom Value
jwt:
# 2. spring Boot Security + Key 설정(* 반드시 32이상 글자로 구성되어야 함)
secret: 7Hs9x2mK4Lp6Rw3tYzAqBcDfGjNvXeUi
6. WebSecurityConfig
💡 WebSecurityConfig
- Spring Security 환경 설정을 구성하기 위한 클래스입니다.
- 웹 서비스가 로드될 때 Spring Container 의해 관리가 되는 클래스이며 사용자에 대한 ‘인증’과 ‘인가’에 대한 구성을 Bean 메서드로 주입을 합니다.
번호 | 메서드 명 | 설명 |
1 | webSecurityCustomizer() | 정적 자원(Resource)에 대해 인증된 사용자의 접근 '인가' 설정을 담당 |
2 | securityFilterChain() | HTTP에 대한 '인증'과 '인가'를 담당하며, 인증 방식과 절차에 대한 설정을 수행 |
3 | authenticationManager() | 인증 메서드를 제공하는 매니저로, 'Provider'의 인터페이스 역할 |
4 | customAuthenticationProvider() | '인증' 제공자로 사용자의 이름과 비밀번호를 데이터베이스에 제공하여 반환 |
5 | bCryptPasswordEncoder() | 비밀번호 암호화를 위한 BCrypt 인코딩 수행 |
6 | customAuthenticationFilter() | 커스텀 '인증' 필터로 접근 URL, 데이터 전달방식 등 인증 과정 및 처리 설정 |
7 | customLoginSuccessHandler() | 사용자 정보가 맞을 경우 수행되는 Handler |
8 | customLoginFailureHandler() | 사용자 정보가 맞지 않을 경우 수행되는 Handler |
9 | jwtAuthorizationFilter() | JWT 토큰을 통한 사용자 인증 |
10 | corsConfigurationSource() | Cors 관련되어서 커스텀 설정을 합니다. |
package com.adjh.springboot3security.config;
import com.adjh.springboot3security.config.filter.CustomAuthenticationFilter;
import com.adjh.springboot3security.config.filter.JwtAuthorizationFilter;
import com.adjh.springboot3security.config.handler.CustomAuthFailureHandler;
import com.adjh.springboot3security.config.handler.CustomAuthSuccessHandler;
import com.adjh.springboot3security.config.handler.CustomAuthenticationProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
/**
* Spring Security 환경 설정을 구성하기 위한 설정 클래스입니다.
* 웹 서비스가 로드 될때 Spring Container 의해 관리가 되는 클래스이며 사용자에 대한 ‘인증’과 ‘인가’에 대한 구성을 Bean 메서드로 주입을 합니다.
*
* @author : jonghoon
* @fileName : WebSecurityConfig
* @since : 10/1/24
*/
@Slf4j
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
/**
* 1. 정적 자원(Resource)에 대해서 인증된 사용자가 정적 자원의 접근에 대해 ‘인가’에 대한 설정을 담당하는 메서드입니다.
*
* @return WebSecurityCustomizer
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// 정적 자원에 대해서 Security를 적용하지 않음으로 설정
return web -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
/**
* 2. HTTP에 대해서 ‘인증’과 ‘인가’를 담당하는 메서드이며 필터를 통해 인증 방식과 인증 절차에 대해서 등록하며 설정을 담당하는 메서드입니다.
*
* @param http HttpSecurity
* @return SecurityFilterChain
* @throws Exception Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 커스텀 설정 적용
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) // 우선 모든 요청에 대한 허용
.addFilterBefore(jwtAuthorizationFilter(), BasicAuthenticationFilter.class) // JWT 인증 (커스텀 필터)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 미사용 (JWT 사용)
.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 사용자 인증(커스텀 필터)
.formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화
.build();
}
/**
* 3. authenticate 의 인증 메서드를 제공하는 매니져로'Provider'의 인터페이스를 의미합니다.
* - 과정: CustomAuthenticationFilter → AuthenticationManager(interface) → CustomAuthenticationProvider(implements)
*
* @return AuthenticationManager
*/
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(customAuthenticationProvider());
}
/**
* 4. '인증' 제공자로 사용자의 이름과 비밀번호를 데이터베이스에 제공하여 반환받습니다.
* - 과정: CustomAuthenticationFilter → AuthenticationManager(interface) → CustomAuthenticationProvider(implements)
*
* @return CustomAuthenticationProvider
*/
@Bean
public CustomAuthenticationProvider customAuthenticationProvider() {
return new CustomAuthenticationProvider(bCryptPasswordEncoder());
}
/**
* 5. 비밀번호를 암호화하기 위한 BCrypt 인코딩을 통하여 비밀번호에 대한 암호화를 수행합니다.
*
* @return BCryptPasswordEncoder
*/
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 6. 커스텀을 수행한 '인증' 필터로 접근 URL, 데이터 전달방식(form) 등 인증 과정 및 인증 후 처리에 대한 설정을 구성하는 메서드입니다.
*
* @return CustomAuthenticationFilter
*/
@Bean
public CustomAuthenticationFilter customAuthenticationFilter() {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
customAuthenticationFilter.setFilterProcessesUrl("/api/v1/user/login"); // 접근 URL
customAuthenticationFilter.setAuthenticationSuccessHandler(customLoginSuccessHandler()); // '인증' 성공 시 해당 핸들러로 처리를 전가한다.
customAuthenticationFilter.setAuthenticationFailureHandler(customLoginFailureHandler()); // '인증' 실패 시 해당 핸들러로 처리를 전가한다.
customAuthenticationFilter.afterPropertiesSet();
return customAuthenticationFilter;
}
/**
* 7. Spring Security 기반의 사용자의 정보가 '맞을 경우' 수행이 되며 결과값을 리턴해주는 Handler
*
* @return CustomLoginSuccessHandler
*/
@Bean
public CustomAuthSuccessHandler customLoginSuccessHandler() {
return new CustomAuthSuccessHandler();
}
/**
* 8. Spring Security 기반의 사용자의 정보가 '맞지 않을 경우' 수행이 되며 결과값을 리턴해주는 Handler
*
* @return CustomAuthFailureHandler
*/
@Bean
public CustomAuthFailureHandler customLoginFailureHandler() {
return new CustomAuthFailureHandler();
}
/**
* 9. JWT 토큰을 통하여서 사용자를 인증합니다.
*
* @return JwtAuthorizationFilter
*/
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter();
}
/**
* 10. CORS에 대한 설정을 커스텀으로 구성합니다.
*
* @return CorsConfigurationSource
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*")); // 허용할 오리진
configuration.setAllowedMethods(List.of("*")); // 허용할 HTTP 메서드
configuration.setAllowedHeaders(List.of("*")); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보 허용
configuration.setMaxAge(3600L); // 프리플라이트 요청 결과를 3600초 동안 캐시
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로에 대해 이 설정 적용
return source;
}
}
7. JwtAuthorizationFilter
💡 JwtAuthorizationFilter
- Spring Security의 필터 체인에서 JWT(JSON Web Token) 인증을 처리하는 중요한 컴포넌트입니다.
- JWT 토큰이 존재하지 않는 경우 다음 필터로 수행되도록 처리가 됩니다.
- JWT 토큰이 존재하는 경우 유효한지에 대해 검증을 수행합니다.
메서드 | 설명 |
doFilterInternal | - JWT 인증 처리의 주요 로직을 수행합니다. - 토큰이 필요하지 않은 API 엔드포인트를 확인하고 처리합니다. - 클라이언트 요청의 Authorization 헤더에서 토큰을 추출하고 검증합니다. - 토큰이 유효한 경우 사용자 ID를 확인하고 다음 필터로 진행합니다. - 예외 발생 시 에러 응답을 생성합니다. |
jwtTokenError | - JWT 관련 예외 발생 시 JSON 형태의 에러 응답을 생성합니다. - 토큰 만료, 잘못된 토큰, 기타 오류에 대한 메시지를 구성합니다. - 에러 상태, 코드, 메시지, 이유를 포함한 JSON 응답을 반환합니다. |
package com.adjh.springboot3security.config.filter;
import com.adjh.springboot3security.common.utils.TokenUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 지정한 URL 별 JWT 유효성 검증을 수행하며 직접적인 사용자 '인증'을 확인하는 필터 역할의 클래스입니다.
*
* @author : jonghoon
* @fileName : CustomAuthenticationFilter
* @since : 10/1/24
*/
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private static final String HTTP_METHOD_OPTIONS = "OPTIONS";
@Override
protected void doFilterInternal(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain)
throws IOException, ServletException {
// 1. 토큰이 필요하지 않는 경우에 대해 API Endpoint 관리
List<String> notUseJwtUrlList = Arrays.asList(
"/api/v1/user/login",
"/api/v1/token/token",
"/user/login",
"/token/token"
);
// 2. 토큰이 필요하지 않는 API 호출 발생 시 : 아래 로직 처리 없이 다음 필터로 이동
if (notUseJwtUrlList.contains(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
// 3. 토큰이 필요없는 HTTP Method OPTIONS 호출 발생 시: 아래 로직 처리 없이 다음 필터로 이동
if (request.getMethod().equalsIgnoreCase(HTTP_METHOD_OPTIONS)) {
chain.doFilter(request, response);
return;
}
// [STEP1] Client API 호출에서 "Authorization"를 속성을 확인합니다.
String header = request.getHeader("Authorization");
logger.debug("[+] header Check: " + header);
try {
// [STEP2-1] Header 내에 토큰이 존재하는 경우 : null, isEmpty() 체크 수행
if (StringUtils.isNotBlank(header)) {
// [STEP2] Header 내에 토큰을 추출합니다.
String token = TokenUtils.getHeaderToToken(header);
System.out.println("추출된 토큰 :: " + token);
// [STEP3] 추출한 토큰이 유효한지 여부를 체크합니다.
if (TokenUtils.isValidToken(token)) {
// [STEP4] 토큰을 기반으로 사용자 아이디를 반환 받는 메서드
String userId = TokenUtils.getClaimsToUserId(token);
logger.debug("[+] userId Check: " + userId);
// [STEP5] 사용자 아이디가 존재하는지 여부 체크
if (StringUtils.isNotBlank(userId)) {
chain.doFilter(request, response); // 리소스로 접근
} else {
throw new Exception("토큰 내에 사용자 아이디가 존재하지 않습니다"); // 사용자 아이디가 존재하지 않는 경우
}
} else {
throw new Exception("토큰이 유효하지 않습니다."); // 토큰이 유효하지 않은 경우
}
} else {
throw new Exception("토큰이 존재하지 않습니다."); // 토큰이 존재하지 않는 경우
}
}
// Token 내에 Exception 발생 하였을 경우 : 클라이언트에 응답값을 반환하고 종료합니다.
catch (Exception e) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
String jsonResponse = jwtTokenError(e);
printWriter.print(jsonResponse);
printWriter.flush();
printWriter.close();
}
}
/**
* JWT 내에 Exception 발생 시 JSON 형태의 예외 응답값을 구성하는 메서드
*
* @param e Exception
* @return String
*/
private String jwtTokenError(Exception e) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonMap = new HashMap<>();
String resultMsg = "";
// [CASE1] JWT 기간 만료
if (e instanceof ExpiredJwtException) {
resultMsg = "토큰 기간이 만료되었습니다.";
}
// [CASE2] JWT내에서 오류 발생 시
else if (e instanceof JwtException) {
resultMsg = "잘못된 토큰이 발급되었습니다.";
}
// [CASE3] 이외 JWT내에서 오류 발생
else {
resultMsg = "OTHER TOKEN ERROR";
}
// Custom Error Code 구성
jsonMap.put("status", 403);
jsonMap.put("code", "9999");
jsonMap.put("message", resultMsg);
jsonMap.put("reason", e.getMessage());
try {
return objectMapper.writeValueAsString(jsonMap);
} catch (JsonProcessingException err) {
log.error("내부적으로 JSON Parsing Error 발생 " + err);
return "{}"; // 빈 JSON 객체를 반환
}
}
}
8. CustomAuthenticationFilter
💡 CustomAuthenticationFilter
- Security의 UsernamePasswordAuthenticationFilter를 확장하여 사용자 정의 인증 로직을 구현합니다.
- 주로 폼 기반의 로그인 요청을 처리하고 인증 토큰을 생성하는 역할을 합니다.
메서드 명 | 설명 |
attemptAuthentication(HttpServletRequest request, HttpServletResponse response) | - 지정된 URL로 form 전송을 했을 때 파라미터 정보를 가져옵니다. - AuthenticationManager를 사용하여 인증을 시도합니다. |
getAuthRequest(HttpServletRequest request) | - Request로 받은 ID와 패스워드를 기반으로 인증 토큰을 발급합니다. - ObjectMapper를 사용하여 요청 본문에서 UserDto 객체를 추출합니다. - ID와 암호화된 패스워드로 UsernamePasswordAuthenticationToken을 생성합니다. |
package com.adjh.springboot3security.config.filter;
import com.adjh.springboot3security.model.dto.UserDto;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* 아이디와 비밀번호 기반의 데이터를 Form 데이터로 전송을 받아 '인증'을 담당하는 필터입니다.
*/
@Slf4j
@Component
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
/**
* 지정된 URL로 form 전송을 하였을 경우 파라미터 정보를 가져온다.
*
* @param request from which to extract parameters and perform the authentication
* @param response the response, which may be needed if the implementation has to do a
* redirect as part of a multi-stage authentication process (such as OpenID).
* @return Authentication {}
* @throws AuthenticationException {}
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordAuthenticationToken authRequest;
try {
authRequest = getAuthRequest(request);
setDetails(request, authRequest);
} catch (Exception e) {
throw new RuntimeException(e);
}
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* Request로 받은 ID와 패스워드 기반으로 토큰을 발급한다.
*
* @param request HttpServletRequest
* @return UsernamePasswordAuthenticationToken
* @throws Exception e
*/
private UsernamePasswordAuthenticationToken getAuthRequest(HttpServletRequest request) throws Exception {
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);
UserDto user = objectMapper.readValue(request.getInputStream(), UserDto.class);
log.debug("1.CustomAuthenticationFilter :: userId:" + user.getUserId() + " userPw:" + user.getUserPw());
// ID와 암호화된 패스워드를 기반으로 토큰 발급
return new UsernamePasswordAuthenticationToken(user.getUserId(), user.getUserPw());
} catch (UsernameNotFoundException ae) {
throw new UsernameNotFoundException(ae.getMessage());
}
}
}
9. CustomAuthenticationProvider
💡 CustomAuthenticationProvider
- 사용자의 인증을 수행하는 단계입니다.
- 클라이언트로부터 전달받은 값을 기반으로 데이터베이스를 조회하여서 사용자에 대한 인증을 수행합니다.
메서드 명 | 설명 |
authenticate(Authentication authentication) | - 사용자 인증을 수행하는 메인 메서드입니다 - 전달받은 Authentication 객체에서 사용자 ID와 비밀번호를 추출합니다 - UserDetailsService를 통해 데이터베이스에서 사용자 정보를 조회합니다 - 비밀번호를 검증하고, 일치하지 않으면 BadCredentialsException을 발생시킵니다 - 인증이 성공하면 새로운 UsernamePasswordAuthenticationToken을 생성하여 반환합니다 |
supports(Class<?> authentication) | - 이 AuthenticationProvider가 지원하는 인증 토큰 타입을 확인합니다 - UsernamePasswordAuthenticationToken 클래스와 일치하는지 확인합니다 |
package com.adjh.springboot3security.config.handler;
import com.adjh.springboot3security.model.dto.UserDetailsDto;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 전달받은 사용자의 아이디와 비밀번호를 기반으로 비즈니스 로직을 처리하여 사용자의 ‘인증’에 대해서 검증을 수행하는 클래스입니다.
* CustomAuthenticationFilter로 부터 생성한 토큰을 통하여 ‘UserDetailsService’를 통해 데이터베이스 내에서 정보를 조회합니다.
*/
@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@NonNull
private BCryptPasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.debug("2.CustomAuthenticationProvider");
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
// 'AuthenticationFilter' 에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
String userId = token.getName();
String userPw = (String) token.getCredentials();
// Spring Security - UserDetailsService를 통해 DB에서 아이디로 사용자 조회
UserDetailsDto userDetailsDto = (UserDetailsDto) userDetailsService.loadUserByUsername(userId);
if (!(userDetailsDto.getUserPw().equalsIgnoreCase(userPw))) {
throw new BadCredentialsException(userDetailsDto.getUserNm() + "Invalid password");
}
return new UsernamePasswordAuthenticationToken(userDetailsDto, userPw, userDetailsDto.getAuthorities());
}
//
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
10. UserDetailsServiceImpl
💡UserDetailsServiceImpl
- 사용자 인증 정보를 로드하고 UserDetails 객체를 생성하는 역할을 담당합니다.
package com.adjh.springboot3security.service.impl;
import com.adjh.springboot3security.model.dto.UserDetailsDto;
import com.adjh.springboot3security.model.dto.UserDto;
import com.adjh.springboot3security.service.UserService;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import java.util.Collections;
/**
* UserDetailsService 구현체
* 사용자 인증 정보를 로드하고 UserDetails 객체를 생성하는 역할을 담당
*
* @author : jonghoon
* @fileName : UserDto
* @since : 10/1/24
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
public UserDetailsServiceImpl(UserService userService) {
this.userService = userService;
}
/**
* 사용자 ID를 받아 해당 사용자의 인증 정보를 로드합니다.
*
* @param userId the username identifying the user whose data is required.
* @return
*/
@Override
public UserDetails loadUserByUsername(String userId) {
// [STEP1] 사용자 아이디를 조회하여 존재하지 않는 경우 오류를 반환합니다.
if (userId == null || userId.isEmpty()) {
throw new AuthenticationServiceException("사용자 ID가 비어있습니다.");
}
// [STEP2] 서비스를 호출하여 실제 데이터베이스 조회를 통해서 사용자 정보를 조회합니다.
return userService.login(UserDto.builder().userId(userId).build())
.map(user -> new UserDetailsDto(user, Collections.singleton(new SimpleGrantedAuthority(user.getUserId()))))
.orElseThrow(() -> new BadCredentialsException("사용자 정보가 올바르지 않습니다: " + userId));
}
}
11. UserService
💡 UserService
- 사용자 정보를 조회해 오기 위한 인터페이스입니다.
package com.adjh.springboot3security.service;
import com.adjh.springboot3security.model.dto.UserDto;
import java.util.List;
import java.util.Optional;
/**
* 사용자 정보를 조회해오기 위한 인터페이스입니다.
*/
public interface UserService {
Optional<UserDto> login(UserDto userVo);
List<UserDto> selectUserList(UserDto userDto);
}
12. CustomAuthSuccessHandler
💡 CustomAuthSuccessHandler
- 사용자의 '인증'에 대해 성공하였을 때, 수행하여 사용자에게 사용자 정보 및 JWT에 대한 응답 값을 제공해 주는 Handler입니다.
package com.adjh.springboot3security.config.handler;
import com.adjh.springboot3security.common.utils.TokenUtils;
import com.adjh.springboot3security.model.dto.UserDetailsDto;
import com.adjh.springboot3security.model.dto.UserDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 사용자의 '인증'에 대해 성공하였을때, 수행하여 사용자에게 사용자 정보 및 JWT에 대한 응답 값을 제공해주는 Handler입니다.
*
* @author : jonghoon
* @fileName : CustomAuthenticationFilter
* @since : 10/1/24
*/
@Slf4j
@Configuration
public class CustomAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
log.debug("3.1. CustomLoginSuccessHandler");
// [STEP1] 사용자와 관련된 정보를 모두 조회합니다.
UserDto userDto = ((UserDetailsDto) authentication.getPrincipal()).getUserDto();
// [STEP2] 응답 데이터를 구성합니다.
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("userInfo", userDto);
// [STEP3] 사용자의 상태에 따라 응답 데이터를 설정합니다.
if ("D".equals(userDto.getUserSt())) {
responseMap.put("resultCode", 9001);
responseMap.put("token", null);
responseMap.put("failMsg", "휴면 계정입니다.");
} else {
responseMap.put("resultCode", 200);
responseMap.put("failMsg", null);
String token = TokenUtils.generateJwtToken(userDto);
responseMap.put("token", token);
response.addHeader("Authorization", "BEARER " + token);
}
// [STEP4] 구성한 응답 값을 JSON 형태로 전달합니다.
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
printWriter.write(objectMapper.writeValueAsString(responseMap));
printWriter.flush();
printWriter.close();
}
}
13. CustomAuthFailureHandler
💡 CustomAuthFailureHandler
- 사용자의 '인증'에 대해 실패하였을 때, 수행하여 사용자에게 응답 값을 제공해 주는 Handler입니다.
package com.adjh.springboot3security.config.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 사용자의 '인증'에 대해 실패하였을떄, 수행하여 사용자에게 응답 값을 제공해주는 Handler입니다.
*
* @author : jonghoon
* @fileName : CustomAuthenticationFilter
* @since : 10/1/24
*/
@Slf4j
@Configuration
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
log.debug("3.2. CustomAuthFailureHandler");
String failMsg = "";
// [STEP2] 발생한 Exception 에 대해서 확인합니다.
if (exception instanceof AuthenticationServiceException ||
exception instanceof BadCredentialsException ||
exception instanceof LockedException ||
exception instanceof DisabledException ||
exception instanceof AccountExpiredException ||
exception instanceof CredentialsExpiredException) {
failMsg = "로그인 정보가 일치하지 않습니다.";
}
// [STEP3] 응답 값을 구성하고 전달합니다.
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
log.debug(failMsg);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("userInfo", null);
resultMap.put("resultCode", 9999);
resultMap.put("failMsg", failMsg);
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(resultMap);
PrintWriter printWriter = response.getWriter();
printWriter.print(jsonResponse);
printWriter.flush();
printWriter.close();
}
}
5) 결과화면
1. 데이터베이스 확인
💡 데이터베이스 확인
- 데이터베이스에 tb_user 테이블 내에 가입이 된 사용자가 아래와 같이 있다는 가정하에 이를 로그인하는 결과화면을 확인해 봅니다.
2. 클라이언트 로그인 : 실패
💡 클라이언트 로그인 : 실패
- 아래와 같이 데이터베이스에 있는 사용자 정보가 아닌 다른 비밀번호를 입력하였을 때, “로그인 정보가 일치하지 않습니다”라는 메시지와 함께 결과 코드를 9999라는 임시 응답 코드를 확인할 수 있습니다.
💡 API 서버 로그를 확인합니다.
- CustomAuthenticationFilter → CustomAuthenticationProvider → (데이터베이스 조회) → CustomAuthFailureHandler 과정으로 수행이 됨이 확인되었습니다.
- 최종적으로 데이터베이스 조회를 통해 인증이 되지 않은 사용자이기에 CustomAuthFailureHandler에서 처리하는 응답값을 클라이언트에게 반환하였습니다.
3. 클라이언트 로그인 : 성공
💡 클라이언트 로그인 : 성공
- 아래와 같이 데이터베이스에 있는 사용자 정보와 동일하게 입력되었을 때, 200이라는 결과 코드를 받고 token이라는 값을 통해 JWT를 발급받음을 확인하였습니다.
💡 API 서버 로그를 확인합니다.
- CustomAuthenticationFilter → CustomAuthenticationProvider → (데이터베이스 조회) → CustomLoginSuccessHandler 과정으로 수행이 됨이 확인되었습니다.
- 최종적으로 데이터베이스 조회를 통해 인증된 사용자이기에 CustomLoginSuccessHandler에서 처리하는 사용자 정보 + JWT를 응답값으로 클라이언트에게 반환하였습니다.
4. 클라이언트 : 토큰 기반 리소스 접근
💡 클라이언트 : 토큰 기반 리소스 접근
- 인증을 받아서 발급 받은 jwt를 기반으로 리소스(Controller)에서 결과값을 받아오는 결과입니다.
💡 API를 호출할때 Header 내에 Authorization의 값으로 Bearer Token으로 accessToken을 전달합니다.
💡 리소스 접근 버튼을 누르면 API 호출이 되고 아래와 같이 콘솔에 리소스 정보를 조회해오는 것을 확인하였습니다.
오늘도 감사합니다. 😀
반응형