제가 담당하게 된 파트는 회원가입 및 로그인을 구현하는 것입니다. 평소에 로그인 데이터가 DB 에 저장될 때 암호화 되어지지 않고 저장되는것을 보고 암호화 과정을 거쳐보고 싶은 욕심이 생겨 담당하게 되었고, 그에 따른 Spring Security 와 JWT 를 활용하여 암호화를 진행해보고자 하였습니다.
Spring Security ?
스프링 기반의 애플리케이션의 보안(인증과 권한)을 담당하는 프레임 워크로써 사용자 관리 기능을 구현하는데 도움을 주는 Spring 의 강력한프레임 워크 입니다. Spring Security 을 사용하게 된 이유는 다음과 같습니다.
- Spring Security 는 다양한 보안 표준을 준수하여 강력한 보안 기능을 제공한다.
- Spring Security 는 Spring Framework 와 밀접하게 통합되어 있어 OAuth2.0, SMAL 2.0 과 같은 인증 프로토콜도 지원한다.
- Spring Security 는 보안 설정을 간편하게 구성할 수 있는 다양한 어노테이션과 XML 을 제공한다.
- Spring Security 는 다양한 커스터마이징 포인트를 제공하여 특정 요구사항에 맞게 보안 설정을 조정할 수 있다.
인증 인가를 통하여 특정 주소에 권한이 없으면 접속할 수 없다는게 굉장히 매혹적이었고, Spring 에서 제공하는 프레임 워크라는 점. 이 두개가 가장 큰 특징으로 와닿아 사용하게 되었습니다.
JWT?
JSON Web Token 은 JSON 객체를 사용하여 두 객체 간에 정보를 안전하게 전송하기 위한 방식입니다. JWT 는 짧은 문자열로 구성되어 있어 URL, HTTP 헤더, 쿠키 등에 쉽게 포함하여 전송이 가능하며, 서버 측의 상태를 저장할 필요가 없어 확장성과 성능을 향상 시킬 수 있습니다.
OAuth 2.0 ?
OAuth 2.0 는 사용자 자격 증명을 노출하지 않고 애플리케이션 간에 안전하게 자원을 공유할 수 있도록 하는 표준 인증 프레임워크 입니다. 사용자가 특정 애플리케이션이나 웹사이트에서 카카오, 네이버, 구글 등의 계정을 사용하여 로그인할 수 있게 해주는 인증 프로토콜 입니다. 이 과정에서 사용자의 아이디와 비밀번호는 애플리케이션과 공유되지 않고, 모든 인증 절차는 해당 서버에서 처리한 후에 발급받은 엑세스 토큰을 통해서 접근하기 때문에 보안성과 사용자의 편의성을 모두 높이는 방식입니다.
위와 같은 이유로 저는 이번 프로젝트에서 해당 기능 모두를 사용하기로 마음 먹었고, 해당 내용을 다뤄주는 감사한 분의 설명을 들어가며 기능을 구현하였습니다.
스프링 시큐리티 | https://youtu.be/y0PXQgrkb90?si=rt49CLVOS0rtOnI4 |
스프링 시큐리티 JWT | https://youtu.be/NPRh2v7PTZg?si=DteCRCN-kOSQrSFC |
스프링 OAuth2 Client JWT | https://youtu.be/xsmKOo-sJ3c?si=hm6shCaEGHeUfA6I |
스프링 JWT 심화 | https://youtu.be/SxfweG-F6JM?si=b0lcmlFaH1xuqrly |
개발 과정
1. SecurityConfig 설정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
//AuthenticationManager Bean 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
//BCryptPasswordEncoder : Spring Security 에서 제공하는 암호화 알고리즘
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//csrf disable -> JWT 를 발급 받아 stateless 상태로 사용할거기 때문에 csrf 를 disable 해줘도 된다.
http
.csrf((auth) -> auth.disable());
//Form 로그인 방식 disable -> JWT 방식으로 로그인 할 것이기 때문에 disable 시켜준다.
http
.formLogin((auth) -> auth.disable());
//http basic 인증 방식 disable -> 위와 같은 이유로 disable 시켜준다.
http
.httpBasic((auth) -> auth.disable());
http
.cors((cors -> cors.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:8080"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
return configuration;
}
})));
//경로별 인가 작업
http
.authorizeHttpRequests((auth)-> auth
// Spring Security 실행시 static(css, js, image 등) 파일도 권한을 주지 않으면 layout 실행시 오류가 발생
.requestMatchers("/css/**", "/js/**", "/images/**", "/fonts/**", "/scss/**","/error").permitAll()
// 로그인, 기본, 회원가입, 리뷰, 포트폴리오, 질문 게시판은 로그인을 하지 않아도 보여야 하기 때문에 권한을 준다.
.requestMatchers("/login", "/","/signup/**","/review/list/**","/portfolio/list/**","/qna/list/**","/api/user/**").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/student/**").hasAnyRole("STUDENT")
.requestMatchers("/teacher/**").hasAnyRole("TEACHER")
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.anyRequest().authenticated()
);
// csrf (사이트 위조변경 security) -> post 를 보내줄 때 csrf 토큰도 같이 보내줘야 하기 때문에 disable 시켜준다. 토큰을 보내주지 않으면 오류발생 !
http
.csrf((auth) -> auth.disable());
// 권한이 없는 상태에서 권한이 필요한 페이지로 이동하려고 하면 오류가 뜨는것을 방지하기 위해 넣어줌.
http
.formLogin((auth) -> auth.loginPage("/login")
/* login 주소가 호출되면 security 가 낚아채서 대신 로그인을 진행 */
.loginProcessingUrl("/api/user/login")
.successHandler(customAuthenticationSuccessHandler()) /* 성공했을때 실행시키는 함수 */
.failureHandler(customAuthenticationFailureHandler())); /* 실패했을때 실행시키는 함수 */
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);
//세션 설정 -> JWT 방식에서 세션은 항상 stateless 상태로 관리하기 때문에 설정해줘야 한다. (가장 중요)
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http
.formLogin((form) -> form
.loginPage("/login")
.loginProcessingUrl("/api/user/login")
.usernameParameter("email")
.passwordParameter("password")
.defaultSuccessUrl("/"));
return http.build();
}
}
2. CustomUserDeialService 설정
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
private final MemberMapperInter memberMapperInter;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemberDto memberDto = memberMapperInter.findByUsername(username);
if (memberDto != null) {
return new CustomUserDetails(memberDto);
}
return null;
}
}
3. JWTUtil 설정
@Component
public class JWTUtil {
private final DataSourceTransactionManagerAutoConfiguration dataSourceTransactionManagerAutoConfiguration;
private SecretKey secretKey;
// jwt 사용을 위해 application.properties 에서 선언한 secretKey 를 기반으로 암호화 하여 저장
public JWTUtil(@Value("${spring.jwt.secret}") String secret, DataSourceTransactionManagerAutoConfiguration dataSourceTransactionManagerAutoConfiguration) {
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
this.dataSourceTransactionManagerAutoConfiguration = dataSourceTransactionManagerAutoConfiguration;
}
// 검증을 진행할 3개의 메소드
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
public String getRoles(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
// 토큰을 생성할 메소드
public String createJwt(String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
4. Custom Secret Key 설정
#JWT Custom Secret_Key
spring.jwt.secret=weughfiwuehviuwehoeonkoweihwoekoiwhhpoejrobijetokjowijepgojewoibe
본문에 참조된 링크에 영상을 참고하여 JPA 로 작성된 Entity 와 Repository 등을 MyBatis 로 변환하는 과정을 거쳤고, Postman 을 통해 authentication 토큰을 제대로 발급 받는것을 확인하였습니다.
'Project > DevCampUs' 카테고리의 다른 글
[Semi-Project]회원가입 기능 구현(8) (0) | 2024.07.21 |
---|---|
[Semi-Project]JWT 요청 인가 테스트 실패(7) (0) | 2024.07.21 |
[Semi-Project]Jenkins 를 통한 배포 (5) (0) | 2024.06.21 |
[Semi-Project]thymeleaf & layout (4) (0) | 2024.06.21 |
[Semi-Project] API 명세서 작성 및 ERD 수정 (3) (0) | 2024.06.13 |