보라코딩
JWT로 백엔드 인증 구현하기 본문
JWT ?
최신 웹 애플리케이션에서 간단하게 인증을 구현하는 방법
크기가 아주 작기 때문에 URL Post 매개변수 또는 헤더에 넣어서 전송
JWT는 마침표로 세 부분으로 구성됨
xxxxx.yyyyy.zzzzz
ㄴ 첫번째 xxxxx : 토큰의 유형과 해싱 알고리즘 정의하는 헤더
ㄴ 두번째 yyyyy : 페이로드, 인증의 경우 사용자 정보를 포함
ㄴ 세번째 zzzzz : 토큰이 변조되지 않았음을 증명하기 위한 서명
클라이언트 ------(사용자가 사용자 이름과 암호를 이용해 로그인한다)------> 서버
서버 ---(사용자가 인증되면 JWT 토큰이 생성되어 클라이언트로 전송된다)--> 클라이언트
자바와 안드로이드용 JWT 라이브러리인 jjwt 라이브러리를 이용하자.
pom.xml
의존성 추가
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
백엔드에서 JWT 인증 구현하기
1. 서명된 JWT 생성하고 검증하는 클래스 작성 ( JwtService )
JwtService
package com.packet.cardatabase.service;
import java.security.Key;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
@Component
public class JwtService {
static final long EXPIRATIONTIME = 86400000; // 토큰 만료 시간 (1일을 미리초로 계산)
static final String PREFIX = "Bearer"; // 토큰의 접두사로 Bearer 스키마 일반적 이용
// 비밀키 생성 (시연용도로만 이용)
// 비밀키는 jjwt 라이브러리의 secretKeyFor 메서드로 생성
// 애플리케이션 구성에서 읽을 수 있음
static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 서명된 JWT 토큰 생성 (getToken 메서드로 토큰 생성하고 반환)
public String getToken(String username) {
String token = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
.signWith(key)
.compact();
return token;
}
// 요청 권한 부여 헤더에서 토큰 가져와 토큰 확인하고 사용자 이름 얻음
// getAuthUser 메서드는 Authorization 헤더에서 토큰 가져옴
public String getAuthUser(HttpServletRequest request) {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (token != null) {
// jjwt라이브러리 parseBuilder 메서드로 JwtParserBuilder 인스턴스 생성
String user = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token.replace(PREFIX, ""))
.getBody()
.getSubject();
if (user != null) return user;
}
return null;
}
}
2. 인증을 위한 자격 증명을 포함할 간단한 POJO 클래스 추가
username과 password 필드만 있다. DB에 저장하진 않기에 @Entity 어노테이션 지정하지 않음
AccountCredentials
package com.packet.cardatabase.domain;
public class AccountCredentials {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
3. 로그인을 위한 controller
사용자 이름과 암호를 넣고 POST 방식으로 /login 엔드포인트 호출하면 로그인 가능
LoginContorller에는 로그인 성공 시 서명된 JWT를 생성하는데 필요한 JwtService 인스턴스 주입해야 한다.
getToken 메서드는 로그인 기능을 처리한다.
LoginController
package com.packet.cardatabase.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.info.ProjectInfoProperties.Build;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.packet.cardatabase.domain.AccountCredentials;
import com.packet.cardatabase.service.JwtService;
@RestController
public class LoginController {
@Autowired
private JwtService jwtService;
@Autowired
AuthenticationManager authenticationManager;
@RequestMapping(value="/login", method = RequestMethod.POST)
public ResponseEntity<?> getToken(@RequestBody AccountCredentials credentials){
// 토큰 생성하고 응답의 Authorization 헤더로 보냄
UsernamePasswordAuthenticationToken creds =
new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
credentials.getPassword());
Authentication auth = authenticationManager.authenticate(creds);
// 토큰생성
String jwts = jwtService.getToken(auth.getName());
// 생성된 토큰으로 응답을 생성
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + jwts)
.header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Authorization")
.build();
}
}
4. SecurityConfig 클래스
package com.packet.cardatabase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.packet.cardatabase.service.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return authenticationManager();
}
}
5. 스프링 시큐리티 기능 구성!
스프링 시큐리티의 configure 메서드는 보호되는 경로와 그렇지 않은 경로를 정의
SecurityConfig 클래스에 configure 메서드를 추가
SecurityConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
// 세션 생성하지 않도록 csrf 비활성화
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// /login 엔드포인트에 대한 POST 요청은 보호되지 않음
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 다른 모든 요청은 보호됨
.anyRequest().authenticated();
}
참고!!
deprecated 메세지가 뜨면
spring-boot version을
2.6.12로 변경하면 된다!!
로그인 단계 완료 되었고
수신요청에서 인증을 처리하는 단계
1. 필터 클래스는 모든 요청을 인증하는데 이용됨
AuthenticationFilter 클래스 만들고
스프링 시큐리티의 OncePerRequestFilter 인터페이스 확장함
AuthenticationFilter
package com.packet.cardatabase.web;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import com.packet.cardatabase.service.JwtService;
@Component
public class AuthenticationFilter extends OncePerRequestFilter{
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, java.io.IOException {
// Authorization 헤더에서 토큰을 가져옴
String jws = request.getHeader(HttpHeaders.AUTHORIZATION);
if (jws != null) {
// 토큰 확인하고 사용자 얻음
String user = jwtService.getAuthUser(request);
// 인증
Authentication authentication =
new UsernamePasswordAuthenticationToken(user, null, java.util.Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
2. 필터 클래스를 스프링 시큐리티 구성에 추가하고 configure 메서드 수정
SecurityConfig
@Autowired
private AuthenticationFilter authenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 세션 생성하지 않도록 csrf 비활성화
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// /login 엔드포인트에 대한 POST 요청은 보호되지 않음
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 다른 모든 요청은 보호됨
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
5. 인증 예외 처리
스프링 시큐리티에는 예외 처리하는데 이용하는 AuthenticationEntryPoint 인터페이스가 있다. 새 클래스 생성!
AuthEntryPoint
package com.packet.cardatabase;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class AuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.println("Error : " + authException.getMessage());
}
}
6. 예외처리 위해 스프링 시큐리티 구성
SecurityConfig
AuthEntryPoint 클래스 주입하고 configure 메서드 수정
@Autowired
private AuthEntryPoint exceptionHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 세션 생성하지 않도록 csrf 비활성화http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// /login 엔드포인트에 대한 POST 요청은 보호되지 않음
.antMatchers(HttpMethod.POST, "/login")
.permitAll()
// 다른 모든 요청은 보호됨
.anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(exceptionHandler)
.and().addFilterBefore(authenticationFilter,
UsernamePasswordAuthenticationFilter.class);}
8. SecurityConfig 에 CORS 필터 활성화
(import 주의하기)
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(false);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
'코딩 > Spring' 카테고리의 다른 글
Spring Boot 와 React.js 연동하기! (0) | 2023.08.05 |
---|---|
스프링부트 테스트 (0) | 2023.07.25 |
스프링부트로 RESTful 웹 서비스 만들기 (0) | 2023.07.24 |
JPA를 이용한 데이터베이스 생성 및 접근 (0) | 2023.07.23 |
스프링 시큐리티 설정하기 (0) | 2023.07.18 |