안녕하세요 오늘 작업을 하기위해서는 새로운 라이브러리를 설치해야 합니다.
코드를 먼저 보여드린 후 설명을 하도록 하겠습니다.

 // spring security 추가 implementation 'org.springframework.boot:spring-boot-starter-security' // jwt compile group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' runtime group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' runtime group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

Spring Security와 로그인 후 JWT 토큰을 주기위해 jjwt가 필요합니다.

1. Controller

 @PostMapping("/login") public ResponseEntity<SuccessResponse<TokenDTO>> login(@RequestBody @Valid UserLoginDTO userLoginDTO) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userLoginDTO.getEmail(), userLoginDTO.getPassword()); Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); String token = tokenProvider.createToken(authentication); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + token); SuccessResponse<TokenDTO> successResponse = SuccessResponse.success(TokenDTO.builder().token(token).build()); return new ResponseEntity<>(successResponse, httpHeaders, HttpStatus.OK); }

2. LoginDTO, TokenDTO

@Data @AllArgsConstructor @NoArgsConstructor @Builder public class LoginDTO { @Email(message = "이메일 형식이 아닙니다.") @NotBlank(message = "이메일을 입력해주세요") private String email; @NotBlank(message = "비밀번호를 입력해주세요") private String password; } @Builder @AllArgsConstructor @NoArgsConstructor @Data public class TokenDTO { private String token; } 

3. Security Config

@EnableWebSecurity @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserAuthenticationProvider userAuthenticationProvider; private final TokenProvider tokenProvider; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { JwtFilter customFilter = new JwtFilter(tokenProvider); http .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers("/signup").permitAll() .anyRequest().authenticated() .and() .formLogin().disable() .csrf().disable() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) ; http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder managerBuilder) throws Exception { managerBuilder.authenticationProvider(userAuthenticationProvider); } }

- UserAuthenticationProvider

@Component @RequiredArgsConstructor public class UserAuthenticationProvider implements AuthenticationProvider { private final UserService userService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String email = authentication.getName(); String password = (String) authentication.getCredentials(); UserDetails userDetails = userService.loadUserByUsername(email); if (!checkPassword(password, userDetails.getPassword()) || !userDetails.isEnabled()) { throw new BadCredentialsException(email); } return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } private boolean checkPassword(String loginPassword, String dbPassword) { return BCrypt.checkpw(loginPassword, dbPassword); } }

- service의 loadUserByUsername

@Service @RequiredArgsConstructor @Slf4j public class UserService implements UserDetailsService { private final UserRepository userRepository; public User createUser(UserDTO userDTO) { User user = User.builder() .email(userDTO.getEmail()) .nickname(userDTO.getNickname()) .createdBy(LocalDateTime.now()) .enabled(true) .build(); user.encryptPassword(userDTO.getPassword()); return userRepository.save(user); } @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Optional<User> byEmail = userRepository.findByEmail(email); User user = byEmail.orElseThrow(() -> new UsernameNotFoundException("아이디나 비밀번호가 틀립니다.")); log.debug(String.valueOf(user.isEnabled())); return User.builder() .email(user.getEmail()) .password(user.getPassword()) .nickname(user.getNickname()) .authority(user.getRole()) .enabled(user.isEnabled()) .build(); } } 

- User Entity의 UserDetails

@Entity @Builder @AllArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String email; private String password; private String nickname; private LocalDateTime createdBy; @Builder.Default private String role = "ROLE_USER"; private String authority; private boolean enabled = true; public void encryptPassword(String password) { this.password = BCrypt.hashpw(password, BCrypt.gensalt()); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Set<GrantedAuthority> auth = new HashSet<>(); auth.add(new SimpleGrantedAuthority(authority)); return auth; } @Override public String getUsername() { return this.getEmail(); } @Override public boolean isAccountNonExpired() { return false; } @Override public boolean isAccountNonLocked() { return false; } @Override public boolean isCredentialsNonExpired() { return false; } @Override public boolean isEnabled() { return this.enabled; } }

4. JWT 관련 코드

1. JwtFilter

API에 요청이 오게되면 헤더에 토큰이 있나없나 확인을 하기 위해 사용하였습니다.

@RequiredArgsConstructor @Slf4j public class JwtFilter extends GenericFilterBean { public static final String AUTHORIZATION_HEADER = "Authorization"; private final TokenProvider tokenProvider; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String jwt = resolveToken(httpServletRequest); String requestURI = httpServletRequest.getRequestURI(); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { Authentication authentication = tokenProvider.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI); } else { log.info("유효한 JWT 토큰이 없습니다, uri: {}", requestURI); } filterChain.doFilter(servletRequest, servletResponse); } private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader(AUTHORIZATION_HEADER); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }


2. TokenProvider
jwt 토큰을 생성하고 검증하는 로직입니다.

@Component @Slf4j public class TokenProvider implements InitializingBean { private static final String AUTHORITIES_KEY = "auth"; private final String secret; private final long tokenValidityInMilliseconds; private Key key; public TokenProvider( @Value("${jwt.secret}") String secret, @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) { this.secret = secret; this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000; } @Override public void afterPropertiesSet() { byte[] keyBytes = Decoders.BASE64.decode(secret); this.key = Keys.hmacShaKeyFor(keyBytes); } public String createToken(Authentication authentication) { String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); long now = (new Date()).getTime(); Date validity = new Date(now + this.tokenValidityInMilliseconds); return Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .signWith(key, SignatureAlgorithm.HS512) .setExpiration(validity) .compact(); } public Authentication getAuthentication(String token) { Claims claims = Jwts .parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); User principal = new User(claims.getSubject(), "", authorities); return new UsernamePasswordAuthenticationToken(principal, token, authorities); } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { log.info("잘못된 JWT 서명입니다."); } catch (ExpiredJwtException e) { log.info("만료된 JWT 토큰입니다."); } catch (UnsupportedJwtException e) { log.info("지원되지 않는 JWT 토큰입니다."); } catch (IllegalArgumentException e) { log.info("JWT 토큰이 잘못되었습니다."); } return false; } }

3.JwtAccessDeniedHandler

@Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { //필요한 권한이 없이 접근하려 할때 403 response.sendError(HttpServletResponse.SC_FORBIDDEN); } }


4. JwtAuthenticationEntryPoint

@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } }


설명


먼저 이메일과 비밀번호를 입력하여 로그인 요청을 하게되면 등록한 JwtFilter가 작동하여 헤더에 토큰이 있는지 없는지 확인을 합니다.
그러나 최초로 로그인 api를 요청하면 최초 로그인이기 때문에 헤더에 토큰이 없는게 당연합니다. 그러므로 진행하게 되고 그 다음 Controller에서

Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
코드를 실행하게 되는데 이 코드는 위에서 만든 UserAuthenticationProvider의 authenticate 메서드를 실행하게 됩니다.
authenticate 메서드에서는 이메일과 비밀번호를 받아 userService의 loadUserByUsername으로 유저를 DB에서 찾아와 유저정보를 만들고 다시 Controller로 돌아와 SecurityContextHolder에 유저정보를 저장하게 됩니다. 그후 그 정보를 기반으로 JWT 토큰을 생성하고ResponseBody에 토큰을 넣어 리턴해 줍니다. 그 이후 다른 API를 요청할때 헤더에 토큰을 넣어 보내면 됩니다.

+ Recent posts