※ 이 글은 2023-1 캡스톤디자인프로젝트 기술블로그 제출용 글입니다. 전반적인 프로젝트의 내용을 간략하게 기술한 뒤 백엔드 구현스킬 중 하나인, Spring Security와 JWT Token을 통한 로그인 구현에 대해 설명하겠습니다.
참고: 이전 학기 제출한 기술 블로그
2022.11.21 - [캡스톤디자인프로젝트] - [캡스톤A] YOLOv5 Custom dataset 학습 및 Object detection
[캡스톤A] YOLOv5 Custom dataset 학습 및 Object detection
안녕하세요. '대면편취형 보이스피싱 예방을 위한 AI 모니터링 알림 서비스'를 주제로 프로젝트를 진행하고 있는 peacebite 팀의 팀원입니다. 이번 포스트에서는 YOLOv5 아키텍쳐 분석 및 Colab을 이용
rimse.tistory.com
안녕하세요.
'대면편취형 보이스피싱 예방을 위한 AI 모니터링 알림 서비스'를 주제로 프로젝트를 진행하고 있는 peacebite 팀의 팀원입니다. 이번 포스트에서는 Spring Security와 JWT Token을 통한 로그인 구현을 다루어 보려고 합니다.
💡 프로젝트 개요
대면편취형 보이스피싱 예방을 위한 AI 모니터링 알림 서비스
💡 개발 배경
경찰청 자료에 따르면 코로나를 기점으로 보이스피싱 건수는 줄었으나 보이스피싱 피해액은 증가하고 있음을 알 수 있습니다. 또한 보이스피싱의 피해금을 편취하는 수법 중에서 "대면 편취형"으로 피해금을 전달하는 건수가 2019년에 비해 2021년에 7배나 증가했다는 것을 알 수 있습니다. 보이스피싱의 3단계 중 세 번째 단계 출금에 해당하는 "대면 편취형" 보이스피싱의 경우, 다른 단계들에 비해 솔루션이 특히 미흡하였습니다. 재산상 손해 뿐만 아니라 정신적 고통까지 받고 있는 보이스피싱 피해자가 더는 늘어나지 않도록 하기 위해, 대면편취형 보이스피싱 예방을 위한 AI 모니터링 알림 서비스(이하 PONITOR)를 제안하게 되었습니다.
저희는 ATM을 사용하는 고객이 보이스피싱 피해자임을 판단하기 위한 기준으로 세 가지를 고려하였습니다. 이는 은행사의 보이스피싱 대응 지침을 기반으로 설정되었습니다.
첫째, obect detection으로 고객이 통화 하고 있는 phone 인식
둘째, emotion recognition으로 고객의 불안함, 초조함 등 부정적인 감정 탐지
셋째, 50만원 이상의 고액 출금
이 세 결과값을 종합하여 고객이 보이스피싱을 당하고 있는 상황임을 판단한 후, 출금을 지연하고 경찰과의 연결을 제안하는 등의 대응을 제공할 생각입니다. 고객 탐지는 CCTV에 내장되어 있는 카메라를 활용할 것이며, 최종 촬영된 결과는 DB에 저장되게 하여 관리자가 나중에 다시 볼 수 있게 만들 예정입니다.
💡 주요 기능
PONITOR는 서버 클라이언트 구조를 가지는 REST API 기반의 서비스입니다.
🔧 로그인 - USER(ATM) 계정, ADMIN 계정
🔧 USER
- 실제 ATM기 화면 구성과 동일한 방식으로 출금 진행
- 메인메뉴 - 예금 인출
- 금액 선택 시 인출액 저장
- 고객 이미지 저장
- 고객 동영상 저장
- 보이스피싱 피해 의심 시 ALERT 후 출금 지연 및 담당 부서 신고 접수
🔧 ADMIN
- ATM 고객 영상 리스트 조회
- 보이스피싱 피해 의심 고객의 분석 결과 확인
💡 ERD
Spring Security와 JWT Token을 이용한 로그인 구현
본격적으로 Spring Security와 JWT Token을 통한 로그인 구현에 대해 설명드리겠습니다.
목차는 다음과 같습니다.
✔ Spring Security
✔ JWT (Json Web Token)
✔ Spring Security + JWT
1. Spring Security
Spring Security는 Spring과는 별개로 작동하는 보안 담당 프레임워크입니다.
크게 두 가지의 동작을 수행합니다.
1. Authenticatio(인증) : 특정 대상이 "누구"인지 확인하는 절차
2. Authorization(권한) : 인증된 주체가 특정한 곳에 접근 권한을 확인하는 것
📌 Spring Security 인증 과정
1. 사용자가 로그인 정보와 함께 인증 요청(Http Request)
2. AuthenticationFilter가 이 요청을 가로챕니다. 이 때 가로챈 정보를 통해 UsernamePasswordAuthenticationToken이라는 인증용 객체를 생성합니다.
3. AuthenticationManager의 구현체인 ProviderManager에게 UsernamePasswordAuthenticationToken 객체
를 전달합니다.
4. 다시 AuthenticationProvider에 UsernamePasswordAuthenticationToken 객체를 전달합니다.
5. 실제 데이터베이스에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보(아이디)를 넘겨줍
니다.
6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듭니다. 이 때 UserDetails
는 인증용 객체와 도메인용 객체를 분리하지 않고 인증용 객체에 상속해서 사용하기도 합니다.
7. AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교합니다.
8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.
9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.
10. Authentication 객체를 SecurityContext에 저장합니다.
최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장합니다. 세션에 사용자정보를 저장한다는 것은 스프링 시큐리티가 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미합니다.
📌 Spring Security Filter
스프링 시큐리티는 필터를 기반으로 수행되며, 표준 서블릿 필터를 사용합니다. 다른 요청들과 마찬가지로 HttpServletRequest와 HttpServletResponse를 사용합니다. Spring Security는 서비스 설정에 따라 필터를 내부적으로 구성합니다. 각 필터는 각자 역할이 있고 필터 사이의 종속성이 있으므로 순서가 중요합니다. XML Tag를 이용한 네임스페이스 구성을 사용하는 경우 필터가 자동으로 구성되지만, 네임스페이스 구성이 지원하지않는 기능을 써야하거나 커스터마이징된 필터를 사용해야 할 경우 명시적으로 빈을 등록 할 수 있습니다.
필터와 인터셉터의 차이는 실행되는 시점의 차이입니다.
- 필터: dispatcher servlet으로 요청이 도착하기 전에 동작
- 인터셉터: dispatcher servlet을 지나고 controller에 도착하기 전에 동작
📌 필터 체인
Spring Security Filter Chain는 아래와 같이 다양하며 커스마이징이 가능합니다.
새로운 Filter를 생성하고자 할 때는, securityConfig에 Filter 체인을 추가 등록해주면 됩니다.
- SecurityContextPersistentFilter : SecurityContextRepository에서 SecurityContext를 가져와서 SecurityContextHolder에 주입하거나 반대로 저장하는 역할을 합니다.
- LogoutFilter : logout 요청을 감시하며, 요청시 인증 주체(Principal)를 로그아웃 시킵니다.
- UsernamePasswordAuthenticationFilter : login 요청을 감시하며, 인증 과정을 진행합니다.
- DefaultLoginPageGenerationFilter : 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지로 넘어가게 합니다.
- BasicAuthenticationFilter : HTTP 요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장합니다.
- RememberMeAuthenticationFilter : SecurityContext에 인증(Authentication) 객체가 있는지 확인하고 RememberMeServices를 구현한 객체 요청이 있을 경우, RememberMe를 인증 토큰으로 컨텍스트에 주입합니다.
- AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 익명 사용자로 취급합니다.
- SessionManagementFilter : 요청이 시작된 이후 인증된 사용자인지 확인하고, 인증된 사용자일 경우 SessionAuthenticationStrategy를 호출하여 세션 고정 보호 매커니즘을 활성화 하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행합니다.
- ExceptionTranslationFilter : 필터체인 내에서 발생되는 모든 예외를 처리합니다.
- FilterSecurityInterceptor : AccessDecisionManager로 권한부여처리를 위임하고 HTTP 리소스의 보안 처리를 수행합니다.
모든 필터를 다 외우고 있을 필요까지는 없겠지만 대략적인 내용을 이해하고 있으면 사용할 때 훨씬 쉽게 관련된 정보를 찾아볼 수 있을 것입니다.
2. JWT (Json Web Token)
📌 Cookie & Session & JWT
Cookie, Session, JWT는 모두 비연결성인 네트워크 서버 특징을 연결성으로 사용하기 위한 방법입니다.
JWT: JSON 객체를 통해 안전하게 정보를 전송할 수 있는 웹표준(RFC7519). 일반적으로 클라이언트와 서버 통신 시 권한 인가(Authorization)을 위해 사용하는 토큰
Cookie: 서버가 사용자의 웹 브라우저에 저장하는 데이터로 브라우저마다 저장되는 쿠키가 다름.
Session: 웹사이트의 여러 페이지에 걸쳐 사용되는 사용자 정보를 저장하는 방법으로, 사용자가 브라우저를 닫아 서버와의 연결을 끝내는 시점까지를 세션이라고 함.
- Cookie & Session은 서버의 어떠한 저장소에 해당 값과 매칭되는 value를 가지고 있어야 합니다. 그래서 서버 자원이 많이 사용되는 단점이 있습니다.
- JWT는 Cookie & Session의 자원 문제를 해결하기 위한 방법입니다. JWT는 토큰 자체에 유저 정보를 담아서 암호화한 토큰이라고 생각하면 됩니다. 암호화된 내용은 디코딩 과정을 통해서 해석이 가능합니다.
- 기존의 세션-쿠키 기반의 로그인이 아니라 JWT같은 토큰 기반의 로그인을 하게 되면 세션이 유지되지 않는 다중 서버 환경에서도 로그인을 유지할 수 있게 되고 한 번의 로그인으로 유저정보를 공유하는 여러 도메인에서 사용할 수 있습니다.
📌 JWT 구조
JWT에는 3개의 구역이 있습니다.
header. payload. verify signature
- header : Header, Payload, Verify Signature 를 암호화할 방식(alg), 타입(Type) 등 포함
- Payload :서버에서 보낼 데이터 - 일반적으로 user의 id, 유효기간 포함
- Verify Signature : Base64 방식으로 인코딩한 Header, Payload, Secret key 를 더한 값
📌JWT를 통한 인증절차
1. 사용자가 로그인을 합니다.
2. 서버에서는 계정 정보를 읽어 사용자를 확인 후, 사용자의 고유 ID 값을 부여한 후 기타 정보와 함께 Payload 에 집어넣습니다.
3. JWT 토큰의 유효기간을 설정합니다.
4. 암호화할 Secret key 를 이용해 Access Token 을 발급합니다.
5. 사용자는 Access Token 을 받아 저장 후, 인증이 필요한 요청마다 토큰을 헤더에 실어 보냅니다.
6. 서버에서는 해당 토큰의 Verify Signature 를 Secret key 로 복호화한 후, 조작 여부, 유효기간을 확인합니다..
7. 검증이 완료되었을 경우, Payload 를 디코딩 하여 사용자의 ID 에 맞는 데이터를 가져옵니다.
3. Spring Security + JWT
📌Spring Security + JWT 기본 동작 원리
이제 직접 작성한 코드와 함께 설명하도록 하겠습니다.
🚩 Gradle
먼저 Spring Security를 gradle로 추가해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'
jwt 관련 라이브러리도 추가합니다.
implementation 'io.jsonwebtoken:jjwt:0.9.1'
🚩 Config
가장 먼저 Security와 Filter관련 설정을 해주어야 합니다.
package pebite.Ponitor_BE.security;
@Slf4j
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Autowired
JwtTokenProvider jwtTokenProvider;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
@Order(2)
public SecurityFilterChain securityfilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.GET,"/users/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().disable()
.csrf().disable()
.headers().disable()
.httpBasic().disable()
.rememberMe().disable()
.logout().disable()
.exceptionHandling()
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.apply(new JwtSecurityConfigurer(jwtTokenProvider));
return http.build();
}
@Bean
@Order(1)
public SecurityFilterChain exceptionSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
}
}
- antMatchers() : 해당 URL로 요청 시 설정을 해줍니다.
- authenticated() : andMatchers에 속해있는 URL로 요청이 오면 인증이 필요하다고 설정합니다.
- anyRequest().authenticated(): 모든 리소스(anyRequest)는 인증 절차를 거친 사용자만 접근이 가능하다는 의미입니다. antMatchers().permitAll()로 설정한 URL 이외의 모든 URL은 인증을 거쳐야 함을 의미합니다.
- formLogin().disable() : Spring Security에서는 아무 설정을 하지 않으면, 기본으로 FormLogin 형식의 로그인을 제공합니다. 자체 Login으로 로그인을 진행할 것이므로, 기본 방식을 disable했습니다.
- httpBasic().disable(): JWT 토큰을 사용한 로그인(Bearer 방식)이기 때문에 기본 설정인 httpBasic은 disable했습니다.
- csrf().disable(): REST API를 사용하여 서버에 인증 정보를 저장하지 않고, 요청 시 인증 정보(JWT 토큰)를 담아서 요청하므로 보안 기능인 csrf가 필요가 없으므로 disable했습니다.
- http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) : 세션을 사용하지 않는다고 설정합니다.
🚩 Custom Provider
추가된 라이브러리를 사용해서 JWT를 생성하고 검증하는 컴포넌트를 만들어 보도록 하겠습니다. JWT에는 토큰 만료 시간이나 회원 권한 정보등을 저장할 수 있습니다.
package pebite.Ponitor_BE.security;
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private String secretKey = "secret";
//사용자 토큰 유효기간 1년
private long userAccessTokenValidTIme = 60 * 60 * 24 * 30 * 12 * 1000L;
//관리자 토큰 유효기간 1일
private long adminAccessTokenValidTIme = 60 * 60 * 1 * 1000L;
private final UserDetailsService userDetailsService;
// 객체 초기화, secretKey를 Base64로 인코딩
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
// JWT 토큰 생성
public String createToken(String memberId, Authority authority, Role roles) {
Claims claims = Jwts.claims().setSubject(memberId); // JWT payload에 저장되는 정보 단위
claims.put("roles", roles); // 정보 저장 (key-value)
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + (authority == Authority.ROLE_ADMIN ? adminAccessTokenValidTIme : userAccessTokenValidTIme))) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과 signature에 들어갈 secret 값 세팅
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getMemberId(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getMemberId(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져온다. "Authorization": "Bearer " + "TOKEN 값"
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}else{
return null;
}
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
🚩 Custom Filter
토큰을 생성하고 검증하는 컴포넌트를 완성했지만 실제로 이 컴포넌트를 이용하는 것은 인증 작업을 진행하는 Filter 입니다. 해당 클래스는 JwtTokenProvider가 검증을 끝낸 Jwt로부터 유저 정보를 조회해와서 UserPasswordAuthenticationFilter 로 전달합니다.
쉽게 말해서, Username + Password를 통한 인증을 Jwt를 통해 수행한다는 것입니다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 헤더에서 JWT를 받아옴
String token = jwtTokenProvider.resolveToken(request);
// 유효한 토큰인지 확인
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받아옴
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}catch (UsernameNotFoundException e){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ResponseMessage responseMessage = new ResponseMessage(Message.ERR_1030);
// JSON 응답을 클라이언트에게 전송
Gson gson = new Gson();
String jsonString = gson.toJson(responseMessage);
response.getWriter().write(jsonString);
}
}
}
🚩 JwtAccessDeniedHandler
403 Fobidden Exception 처리를 위한 클래스 입니다.
package pebite.Ponitor_BE.security;
import com.google.gson.Gson;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import pebite.Ponitor_BE.response.Message;
import pebite.Ponitor_BE.response.ResponseMessage;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component // 서버에 요청을 할 때 액세스가 가능한지 권한을 체크한 후 액세스 할 수 없는 요청 했을 때 동작
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//필요한 권한이 없이 접근하려 할때 403
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ResponseMessage responseMessage = new ResponseMessage(Message.ERR_1000);
// JSON 응답을 클라이언트에게 전송
Gson gson = new Gson();
String jsonString = gson.toJson(responseMessage);
response.getWriter().write(jsonString);
}
🚩 JwtAccessDeniedHandler
401 Unauthorized Exception 처리를 위한 클래스 입니다.
package pebite.Ponitor_BE.security;
import com.google.gson.Gson;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import pebite.Ponitor_BE.response.Message;
import pebite.Ponitor_BE.response.ResponseMessage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component // 인증이 되지 않은 유저가 요청했을 때 동작
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ResponseMessage responseMessage = new ResponseMessage(Message.ERR_1020);
// JSON 응답을 클라이언트에게 전송
Gson gson = new Gson();
String jsonString = gson.toJson(responseMessage);
response.getWriter().write(jsonString);
}
}
🚩 Users
User 정보를 담을 Entity 객체를 생성합니다.
package pebite.Ponitor_BE.model;
import lombok.*;
import javax.persistence.*;
@Entity
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
@Table(name="users")
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="userId")
private Long userId;
@Column(name="username")
private String username;
@Column(name="password")
private String password;
@Column(name="authority")
private String authority;
@Column(name="atmBranch")
private String atmBranch;
@Column(name="atmId")
private String atmId;
}
🚩 UsersRepositoryCustom
DB에 접근하는 메서드(ex) findAll()) 들을 사용하기 위해 Repository를 생성합니다.
package pebite.Ponitor_BE.repository.users;
public interface UsersRepositoryCustom {
Optional<Users> findByUsername(String username);
String findByPasswordEncode(String password);
}
🚩 UsersRepositoryImpl
package pebite.Ponitor_BE.repository.users;
public class UsersRepositoryImpl extends QuerydslRepositorySupport implements UsersRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Autowired
EntityManager entityManager;
public UsersRepositoryImpl(JPAQueryFactory queryFactory) {
super(Users.class);
this.queryFactory = queryFactory;
}
@Override
public Optional<Users> findByUsername(String username){
QUsers users = QUsers.users;
JPQLQuery<Users> query;
query = from(users)
.where(users.username.eq(username));
return Optional.ofNullable(query.fetchOne());
}
@Override
public String findByPasswordEncode(String password) {
String sql = "select md5(?0)";
Query query = entityManager.createNativeQuery(sql);
query.setParameter(0, password);
return (String)query.getSingleResult();
}
}
※Querydsl 구문 사용
QueryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크입니다.
문자가 아닌 코드 쿼리를 작성함으로써 컴파일 시점에 쿼리의 문법적인 오류를 조기에 발견하고 가독성을 증대시켰습니다.
🚩 UserDetailsService
토큰에 저장된 유저 정보를 활용해야 하기 때문에 SecurityUserDetatilService 라는 이름의 클래스를 만들고 UserDetailsService를 상속받아 재정의 하는 과정을 진행합니다.
package pebite.Ponitor_BE.security;
@Service
public class SecurityUserDetailService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Users> optionalUsers = usersRepository.findByUsername(username);
if(optionalUsers.isEmpty()) {
throw new UsernameNotFoundException(username + " 사용자가 존재하지 않습니다.");
} else {
Users users = optionalUsers.get();
return new SecurityUser(users);
}
}
}
위 코드를 보면 UserRepository 에서 Username을 통해 유저를 찾는 findByUsername 메소드를 사용하는 것을 알 수 있습니다.
🚩 UserService
비즈니스 로직을 수행하는 Service 클래스를 작성합니다.
package pebite.Ponitor_BE.service;
@Slf4j
@Service
@Transactional
public class UserService {
@Autowired
UsersRepository usersRepository;
@Autowired
JwtTokenProvider jwtTokenProvider;
public UsersLoginResDto login(String username, String password) throws AuthenticationException {
Users users = usersRepository.findByUsername(username)
.orElseThrow(()->new AuthenticationException(Message.ERR_1010));
String encodedPassword = usersRepository.findByPasswordEncode(password);
if(!encodedPassword.equals(users.getPassword())){
throw new AuthenticationException(Message.ERR_1010);
}
String atmId = users.getAtmId();
String atmBranch = users.getAtmBranch();
String authority = users.getAuthority();
Authority jwtRoleType = "ROLE_ADMIN".equals(authority) ? Authority.ROLE_ADMIN : Authority.ROLE_USER;
//AccessToken 지급
String accessToken = jwtTokenProvider.createToken(users.getUsername(), jwtRoleType, null);
return UsersLoginResDto.builder()
.accessToken(accessToken)
.username(username)
.authority(authority)
.atmBranch(atmBranch)
.atmId(atmId)
.build();
}
}
🚩 UserController
/users/login 요청이 왔을 때 인증 처리를 하는 클래스입니다. Controller 에서 회원 가입과 로그인을 통한 인증 과정을 진행하였습니다.
package pebite.Ponitor_BE.controller;
@Slf4j
@RestController
@RequiredArgsConstructor
public class UserController {
@Autowired
UserService userService;
@GetMapping(value = "/users/login", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity login(@RequestParam(name = "username") @NotNull @NotEmpty @NotBlank String username,
@RequestParam(name = "password") @NotNull @NotEmpty @NotBlank String password,
HttpServletRequest request) {
ResponseEntity responseEntity = null;
try {
UsersLoginResDto usersLoginResDto = userService.login(username, password);
responseEntity = ResponseEntity
.status(HttpStatus.OK)
.body(usersLoginResDto);
}catch (AuthenticationException e){
responseEntity = ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getResponseMessage());
}
return responseEntity;
}
}
📣Testing
애플리케이션을 실행하고 포스트맨으로 로그인 해보겠습니다.
ADMIN 계정
USER 계정
프론트로부터 response로 username과 password를 받으면, request로 accessToken, username, atmBranch, atmId를 잘 보내주는 것을 확인하였습니다.
이렇게 Spring Security와 JWT를 이용해서 로그인 기능을 구현해 봤습니다.
🤷♀️ 후기
spring security를 활용한 로그인이 어떤 원리로 돌아가는지 구체적으로 알 수 있었습니다. JWT는 REST API에서 많이 사용하는 인증방식이므로 더욱 자세히 알 필요가 있다고 생각했습니다. 아직 refresh token은 구현하지 못해 다음 기회에 구현해보아야 겠습니다.
Reference
https://gksdudrb922.tistory.com/217
[Spring] Spring Security + JWT 토큰을 통한 로그인
JWT JWT(Json Web Token)은 일반적으로 클라이언트와 서버 통신 시 권한 인가(Authorization)을 위해 사용하는 토큰이다. 현재 앱개발을 위해 REST API를 사용 중인데, 웹 상에서 Form을 통해 로그인하는 것이
gksdudrb922.tistory.com
Spirng Security + Jwt 로그인 적용하기
프로젝트를 진행하면서 Spring Security + Jwt를 이용한 로그인을 구현하게 되었다. 목차 Spring Security JWT Spring SEcurity + JWT Spring Security > 가장먼저 스프링 시큐리티에 대해서 알아보자. Sprin
velog.io
https://webfirewood.tistory.com/m/115?category=694472
SPRING SECURITY + JWT 회원가입, 로그인 기능 구현
이전에 서블릿 보안과 관련된 포스트(링크)를 작성했던 적이 있습니다. 서블릿 기반의 웹 애플리케이션에서 인증과 인가 과정을 간단하게 설명했습니다. 스프링에서는 마찬가지로 이런 인증과
webfirewood.tistory.com
https://aljjabaegi.tistory.com/659
SpringBoot + JWT + Security + JPA 인증 구현, JWT란?
해당 포스팅은 인프런의 무료강의를 참고하여 작성되었습니다. Link: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt/dashboard 이해하기 쉽게 설명되어 있으니 참고하시면 좋을 것 같
aljjabaegi.tistory.com
'캡스톤디자인프로젝트' 카테고리의 다른 글
AWS에 DB 환경 만들기(AWS RDS) (0) | 2023.05.11 |
---|---|
AWS 서버 환경 만들기(AWS EC2) (0) | 2023.05.10 |
[캡스톤A] YOLOv5 Custom dataset 학습 및 Object detection (1) | 2022.11.21 |