#1. 프로젝트 생성
Intellij 의 New Spring Boot Project 생성, 혹은 start.spring.io 를 통해 신규 프로젝트를 생성합니다.
Dependencies 설정은 아래와 같이 심플하게 시작합니다.
생성된 프로젝트에 추가로 JWT 관련 Dependencies를 추가 해줍니다.
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
최종 의존성은 아래와 같습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
필요한 의존성를 전부 로드하고, 이제 나머지 필요한 코드들을 작성합니다.
#2. User , UserDetailsService 클래스
사용자의 정보를 담을 User 클래스와, 정보를 가져올 UserDetailsService를 구현합니다.
UserDetailsService 인터페이스를 구현하여 loadUserByUsername 메소드를 오버라이드 합니다.
예시에서는 JPA가 아닌 Mybatis를 사용하여 DB repository를 구현합니다.
@Getter
public class User {
private Long userId;
private String username;
private String password;
}
-
@Mapper
public interface UserDetailsMapper {
@Select("SELECT userId, username, password FROM EMP WHERE username = #{username}")
User getUserByUsername(String username);
}
-
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserDetailsMapper userDetailsMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername");
User user = userDetailsMapper.getUserByUsername(username);
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), true, true, true, true, new ArrayList<>());
}
}
#3. JwtUtil
JwtUtil 클래스는 토큰의 생성, request 헤더에서 토큰의 추출, 정합성 검사 등의 역할을 수행 합니다.
Jwt.parserBuilder() 메소드는 최신버전에서 Deprecated 되었으므로, 아래의 소스를 참고하여 작성합니다.
@Slf4j
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
private SecretKey secretKey;
@Value("${jwt.access-token-expiry}")
private long accessTokenExpiry;
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
@PostConstruct
protected void init() {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
public String createAccessToken(String username) {
log.info("createAccessToken");
return Jwts.builder()
.signWith(secretKey)
.subject(username)
.issuedAt(Date.from(Instant.now()))
.expiration(Date.from(Instant.now().plusSeconds(accessTokenExpiry)))
.compact();
}
public Claims extractToken(String token) {
log.info("extractToken");
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
public String extractUsername(String token) {
log.info("extractUsername");
return extractToken(token).getSubject();
}
public String extractBearerToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}
public Boolean validateToken(final String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (SecurityException e) {
log.warn("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.warn("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.warn("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
#4. JwtRequestFilter
JwtRequestFilter는 OncePerRequestFilter를 상속받아 작성합니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.info("doFilterInternal");
final String authorizationHeader = request.getHeader("Authorization");
String jwtToken = null;
String username = null;
if(authorizationHeader != null) {
jwtToken = jwtUtil.extractBearerToken(request);
if(jwtUtil.validateToken(jwtToken)) {
username = jwtUtil.extractUsername(jwtToken);
}
}
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
}
#5. SecurityConfig
SecurityConfig에서는 인증매커니즘, URL패턴 정의, jwt 토큰을 사용하여 인증을 처리하기 위한 필터 처리 등이 포함됩니다.
예제에서는 /auth 경로만 접근을 허용하여 토큰을 발급 받도록 합니다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// jwtRequestFilter -> UsernamePasswordAuthenticationFilter 순서
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/auth").permitAll()
.anyRequest().authenticated());
return http.build();
}
}
#6. 컨트롤러 구현
토큰 발급에 사용될 /auth 와 테스트를 위한 /hello 메소드를 구현합니다.
먼저 인증 Request / Response 구조체를 생성하고
@Getter
public class AuthRequest {
private String username;
private String password;
}
@Getter
@AllArgsConstructor
public class AuthResponse {
private String token;
}
AuthController를 작성합니다.
@Slf4j
@RestController
@RequiredArgsConstructor
public class AuthController {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@PostMapping("/auth")
public AuthResponse createToken(@RequestBody AuthRequest authRequest) {
log.info("createToken");
userDetailsService.loadUserByUsername(authRequest.getUsername());
String accessToken = jwtUtil.createAccessToken(authRequest.getUsername());
return new AuthResponse(accessToken);
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
#7. 테스트
테스트는 intellij의 HTTP Client 플러그인을 사용해서 간단히 수행합니다.
에디터에 아래와 같은 내용으로 .http 파일을 생성하여 실행하기만 하면 됩니다.
# AuthController
### GET without token
GET http://localhost:8080/hello
Content-Type: application/json
### POST create a token
POST http://localhost:8080/auth
Content-Type: application/json
{
"username": "user1",
"password": "1234"
}
> {% client.global.set("auth_token", response.body.token); %}
### GET with token
GET http://localhost:8080/hello
Content-Type: application/json
Authorization: Bearer {{auth_token}}
먼저, 토큰정보 없이 /hello 를 수행하면
다음으로 /auth를 실행하여 토큰을 발급받습니다.
이제 발급된 토큰정보와 함께 다시한번 /hello 를 실행합니다.
정상적으로 결과를 받아오는 것을 확인할 수 있습니다.
이것으로 기본적인 토큰 처리 절차가 모두 완료되었습니다.
fin.
'Spring Boot' 카테고리의 다른 글
javax.inject.Provider 를 사용할 때 UnsatisfiedDependencyException 발생 (0) | 2023.03.12 |
---|---|
[Spring Boot] REST API 제작기 - 2.DB연결(Mybatis) (0) | 2022.03.28 |
[Spring Boot] REST API 제작기 - 1.프로젝트 생성 (0) | 2022.02.17 |
[Spring Boot] Spring Rest Doc 설정(gradle) (0) | 2021.06.03 |
댓글