본 포스팅은 JWT와 access token, refresh token, Spring Security의 기본 개념을 숙지하고 있다는 가정하에 작성되었습니다.
장점
이번 포스팅에서는 인증의 제일 첫번째 관문인 로그인에 대해 알아보겠습니다.
Spring Security을 사용하여 로그인 인증 절차 구현되었습니다.
플로우 차트
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
configure
메소드를 오버라이딩 하여 접근 권한을 작성 할 수 있다.@EnableWebSecurity
: web security 용도로 사용하겠다는 어노테이션, 스프링 시큐리티를 사용하기 위해 추가.@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
private final CookieProvider cookieProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Custom Login Authentication 필터를 사용함
LoginAuthenticationFilter loginAuthenticationFilter =
new LoginAuthenticationFilter(authenticationManagerBean(), jwtTokenProvider, refreshTokenServiceImpl, cookieProvider);
loginAuthenticationFilter.setFilterProcessesUrl("/login");
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().anyRequest().permitAll();
http.addFilter(loginAuthenticationFilter);
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
configure(AuthenticationManagerBuilder auth)
: password를 해싱하기 위해 BCrypt 알고리즘을 사용http.authorizeRequests()
: 시큐리티 처리에 HttpServletRequest를 사용하겠다는 의미antMatchers()
: url 패턴을 지정permitAll()
: 모든 사용자가 접근 할 수 있다.configure(HttpSecurity http)
: 스프링 시큐리티 규칙을 작성해줍니다.@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class UserServiceImpl implements UserService, UserDetailsService {
private final CustomerRepository customerRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found in the database"));
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(user.getDtype()));
return new org.springframework.security.core.userdetails.User(user.getId().toString(), user.getPassword(), authorities);
}
}
/login
@RequiredArgsConstructor
@Slf4j
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
private final CookieProvider cookieProvider;
// login 리퀘스트 패스로 오는 요청을 판단
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
Authentication authentication;
try {
// POST 요청으로 들어오는 email과 password
LoginRequest credential = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
// UsernamePasswordAuthenticationToken을 통해
// loadUserByUsername 메소드에서 로그인 판별
authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(credential.getEmail(), credential.getPassword())
);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return authentication;
}
// 로그인 성공 이후 토큰 생성
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
org.springframework.security.core.userdetails.User user = (User) authResult.getPrincipal();
List<String> roles = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String userId = user.getUsername();
// response body에 넣어줄 access token 및 expired time 생성
String accessToken = jwtTokenProvider.createJwtAccessToken(userId, request.getRequestURI(), roles);
Date expiredTime = jwtTokenProvider.getExpiredTime(accessToken);
// 쿠키에 넣어줄 refresh token 생성
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
// redis에 새로 발행된 refresh token 값 갱신
refreshTokenServiceImpl.updateRefreshToken(Long.valueOf(userId), jwtTokenProvider.getRefreshTokenId(refreshToken));
// 쿠키 설정
ResponseCookie refreshTokenCookie = cookieProvider.createRefreshTokenCookie(refreshToken);
Cookie cookie = cookieProvider.of(refreshTokenCookie);
response.setContentType(APPLICATION_JSON_VALUE);
response.addCookie(cookie);
// body 설정
Map<String, Object> tokens = Map.of(
"accessToken", accessToken,
"expiredTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiredTime)
);
new ObjectMapper().writeValue(response.getOutputStream(), Result.createSuccessResult(tokens));
}
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class RefreshTokenServiceImpl implements RefreshTokenService {
private final UserRepository userRepository;
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
@Transactional
@Override
public void updateRefreshToken(Long id, String uuid) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + id + "는 없는 사용자입니다."));
refreshTokenRedisRepository.save(RefreshToken.of(user.getId().toString(), uuid));
}
}
@Component
@Slf4j
public class JwtTokenProvider {
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String createJwtAccessToken(String userId, String uri, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("roles", roles);
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(uri)
.compact();
}
public String createJwtRefreshToken() {
Claims claims = Jwts.claims();
claims.put("value", UUID.randomUUID());
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String getUserId(String token) {
return getClaimsFromJwtToken(token).getSubject();
}
public String getRefreshTokenId(String token) {
return getClaimsFromJwtToken(token).get("value").toString();
}
public List<String> getRoles(String token) {
return (List<String>) getClaimsFromJwtToken(token).get("roles");
}
public void validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
} catch (SignatureException | MalformedJwtException |
UnsupportedJwtException | IllegalArgumentException | ExpiredJwtException jwtException) {
throw jwtException;
}
}
private Claims getClaimsFromJwtToken(String token) {
try {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
@Component
public class CookieProvider {
@Value("${token.refresh-expired-time}")
private String refreshTokenExpiredTime;
public ResponseCookie createRefreshTokenCookie(String refreshToken) {
return ResponseCookie.from("refresh-token", refreshToken)
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(Long.parseLong(refreshTokenExpiredTime)).build();
}
public ResponseCookie removeRefreshTokenCookie() {
return ResponseCookie.from("refresh-token", null)
.build();
}
public Cookie of(ResponseCookie responseCookie) {
Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue());
cookie.setPath(responseCookie.getPath());
cookie.setSecure(responseCookie.isSecure());
cookie.setHttpOnly(responseCookie.isHttpOnly());
cookie.setMaxAge((int) responseCookie.getMaxAge().getSeconds());
return cookie;
}
}
https://github.com/Development-team-1/just-pickup
감사합니다!