
회원가입과 로그인을 구현하기 위해 JWT 와 Spring Security 에 대해 알아보았다. 이제 개념을 얼추 이해했으니 본격적으로 구현을 해보자.
개발 환경
Spring 프로젝트 생성
📌 Spring 프로젝트 생성 사이트로 들어가서 아래 툴을 Dependencies 에 추가한다.
본인은 아래와 같은 패키지 구조로 진행했다.

📍 Config 와 관련하여 추가적인 설정을 위해서 다음과 같은 방법이 있다.
1.WebSecurityConfigurer를 implements 하는 방법 ✅
2.WebSecurityConfigurerAdapter를 extends 하는 방법
여기서 1번 방법을 채택해 구현하였다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // @PreAuthorize 메소드 적용을 위함
public class SecurityConfig {
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
public SecurityConfig(
final TokenProvider tokenProvider,
final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
final JwtAccessDeniedHandler jwtAccessDeniedHandler
) {
this.tokenProvider = tokenProvider;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// token 을 사용하기 때문에 csrf 를 disable 한다.
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/hello", "/api/authenticate", "/api/signup").permitAll()
.requestMatchers(PathRequest.toH2Console()).permitAll()
.anyRequest().authenticated()
)
// 세션을 사용하지 않기 때문에 STATELESS 로 설정
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// H2 설정
.headers(headers ->
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
)
// JwtSecurityConfig 적용
.with(new JwtSecurityConfig(tokenProvider), customizer -> {});
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@EnableWebSecurity : 기본적인 Web 보안을 활성화하겠다.authorizeRequests : HttpServletRequest 를 사용하는 요청들에 대한 접근제한을 설정하겠다..antMatchers(<API..>).permitAll() : 해당 API 에 대한 요청은 인증없이 접근을 허용하겠다..anyRequest().authenticated() : 그 외 요청은 모두 인증되어야 한다.@Getter @Setter@NoArgsConstructor@AllArgsConstructor Get / Set / Builder / Constructor 관련 코드를 자동으로 주입✔︎ 실무에서는 위 설정들을 잘 고려할 필요가 있다 !
지금은 연습용이기 때문에 편리함을 위해 모두 넣었지만 실무나 어떤 프로젝트를 할 때에는 팀원과 의논하여 설정해야 한다.
✔︎ 특정 문자열에 대한 base64 인코딩 값 도출하기
문자열은 아래 내용이 아니어도 된다. 자신이 원하는 문자열을 넣어서 base64 인코딩 값을 도출하자.

JWT 의존성을 추가하자.
dependencies {
...
// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}
@Component
public class TokenProvider implements InitializingBean {
private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private final String secret;
private final long tokenValidityInMilliseconds;
private Key key;
public TokenProvider(
@Value("${jwt.secret}") final String secret,
@Value("${jwt.token-validity-in-seconds}") final long tokenValidityInMilliseconds
){
this.secret = secret;
this.tokenValidityInMilliseconds = tokenValidityInMilliseconds;
}
@Override
public void afterPropertiesSet() throws Exception {
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public String createToken(final Authentication authentication) {
final String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
final long now = (new Date()).getTime();
final Date validity = new Date(now + this.tokenValidityInMilliseconds);
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS256)
.setExpiration(validity)
.compact();
}
public Authentication getAuthentication(final String token) {
final Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
final Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
final User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(final String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException ex) {
logger.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException ex) {
logger.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException ex) {
logger.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException ex) {
logger.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
}
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = "authorities")
Optional<User> findOneWithAuthoritiesByUsername(final String username);
}
@EntityGraph 쿼리가 수행될 때 Eager 조회로 authorities 정보를 같이 가져오게 된다.Spring Security 에서 중요한 부분 중 하나인 UserDetailService 를 커스텀 구현한 CustomUserDetailService 클래스를 생성하도록 하자.
@Configuration("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(final UserRepository userRepository) {
this.userRepository = userRepository;
}
/**
* 로그인 시 DB 에서 유저정보와 권한정보를 가져와 User 객체 생성
*/
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findOneWithAuthoritiesByUsername(username)
.map(user -> createUser(username, user))
.orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스를 찾을 수 없습니다."));
}
private User createUser(final String username, final leeseunghee.study.jwttutorial.entity.User user) {
if (!user.isActivated()) {
throw new RuntimeException(username + "-> 활성화되어 있지 않습니다.");
}
final List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
.collect(Collectors.toList());
return new User(user.getUsername(), user.getPassword(), grantedAuthorities); // User 객체 생성
}
}
@RestController
@RequestMapping("/api")
public class AuthController {
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public AuthController(final TokenProvider tokenProvider , final AuthenticationManagerBuilder authenticationManagerBuilder) {
this.tokenProvider = tokenProvider;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
@PostMapping("/authenticate")
public ResponseEntity<TokenDto> authorize(@Valid @RequestBody final LoginDto loginDto) {
final UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.username(), loginDto.password());
final Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
final String jwt = tokenProvider.createToken(authentication);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);
return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
}
}
final Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);.authenticate 가 실행될 때, CustomUserDetailService 의 loadUserByUsername 메소드 실행tokenProvider.createToken(authentication)유틸리티 메소드를 만들기 위해 SecurityUtil 클래스를 생성하자.
public class SecurityUtil {
private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
private SecurityUtil() {
}
public static Optional<String> getCurrentUsername() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
logger.debug("Security Context 에 인증 정보가 없습니다.");
return Optional.empty();
}
String username = null;
if (authentication.getPrincipal() instanceof UserDetails) {
final UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
username = springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
username = (String) authentication.getPrincipal();
}
return Optional.ofNullable(username);
}
}
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();테스트 환경
※ Postman 으로 진행합니다.
회원가입
아래와 같이 JSON 형태로 사용자의 정보를 바디에 담아 전달한다.

로그인
로그인 시 사용자 인증을 거쳐 응답값으로 토큰이 발급된다.
