SpringSecurity를 의존성 주입하여 인증, 인가처리를 해주고 토큰을 쿠키에 넣어 인증처리를 한다.
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// JWT
compileOnly 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'
추가할 클래스별 역할
WebSecurityConfig
- 빈객체에 추가할 인증 및 인가에 관한 설정들을 모아둔 클래스
- 기존 필터에 필요한 설정 및 만든 필터를 연결해 관리를 할 수 있다.
- 인증처리 유무를 설정할 수 있다.
JwtAuthenticationFilter
- UsernamePasseordAuthenticationFilter를 상속받아 구현한 로그인 관련 검증 필터
- 로그인 인증용 토큰 객체를 생성 및 로그인 성공 여부에 따른 처리 구현
JwtAuthorizationFilter
- OncePerRequestFilter를 상속받아 구현한 토큰 검증 필터
- JWT를 이용해 권한을 검증해 접근 여부를 판단해 처리한다.
JwtUtil
- JWT를 사용하기 위해 설정한 클래스
- 토큰을 생성하고 검증하기 위해 사용할 함수들을 만들어두고 리턴한다.
UserDetailsImpl
- Spring Security에서 사용자의 정보를 담는 인터페이스를 구현한 클래스
- 사용자의 권한, 비밀번호, 계정 고윳값 등을 리턴한다.
UserDetailsServiceImpl
- Spring Security에서 유저의 정보를 가져오는 인터페이스를 구현한 클래스
- 조회에 성공하면 UserDetails 타입의 객체를 리턴한다.
구현 코드
WebSecurityConfig
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtUtil jwtUtil;
// 비밀번호 암호화
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// AuthenticationManager 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
// 로그인 필터
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
// 토큰 검증 필터
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
// 필터 연결
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/api/auth/**").permitAll() // '/api/auth/'로 시작하는 요청 모두 접근 허가
.requestMatchers(HttpMethod.GET, "/api/board/**").permitAll() // '게시물 조회 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
// 필터 관리
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
JwtAuthenticationFilter
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/auth/login");
}
// 로그인 토큰 생성
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginRequestDto loginRequestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
loginRequestDto.getUserId(),
loginRequestDto.getPassword(),
null
)
);
} catch (IOException e) {
e.printStackTrace();
}
return super.attemptAuthentication(request, response);
}
// 로그인 성공 시
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
String userId = ((UserDetailsImpl)authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getUser_role();
String accessToken = jwtUtil.createAccessToKen(userId, role);
jwtUtil.addJwtToCookie(accessToken, response);
response.setContentType("application/json; charset=utf-8");
MessageResponseDto message = new MessageResponseDto("로그인 성공하였습니다." , HttpStatus.OK.value());
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(message));
}
// 로그인 실패 시
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.error("로그인 실패");
response.setStatus(401);
response.setContentType("application/json; charset=utf-8");
MessageResponseDto message = new MessageResponseDto("로그인 실패하였습니다." , HttpStatus.UNAUTHORIZED.value());
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(message));
}
}
JwtAuthorizationFilter
@RequiredArgsConstructor
@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
// JWT를 이용한 필터 검증
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = jwtUtil.getJwtFromRequest(request);
if(StringUtils.hasText(accessToken)) {
accessToken = jwtUtil.subStringToken(accessToken);
if(!jwtUtil.validateToken(accessToken)) {
log.error("토큰정보가 일치하지 않습니다.");
return;
}
Claims info = jwtUtil.getUerInfoFromToken(accessToken);
try{
setAuthentication(info.getSubject());
}catch (Exception e) {
log.error(e.toString());
return;
}
}
filterChain.doFilter(request, response);
}
// 권한 인증 처리
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 인증 객체 생성
private Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());
}
}
JwtUtil
secretkey는 자신이 사용할 암호를 base64 형식으로 인코딩하여 공유가 안 되는 yml이나 properties에 숨겨서 사용하면 된다.
@Component
@Slf4j
public class JwtUtil {
// 토큰 해더 키값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 키
public static final String AUTHORIZATION_KEY = "auth";
// 토큰 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 유효 시간
private final long TOKEN_TIME = 60 * 60 * 1000L; //60분
// 암호화 시킨 사용할 키
@Value("${jwt.secret.key}")
private String secretKey;
// 암호화 키
private Key key;
// 암호화 알고리즘
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 암호화 키 세팅
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// 토큰생성
public String createAccessToKen(String userId, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(userId) // 사용자 식별아이디
.claim(AUTHORIZATION_KEY, role) //유저 권한정보
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) //만료시간
.setIssuedAt(date) //발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact(); //끝
}
// 쿠키에 jwt 저장
public void addJwtToCookie(String token, HttpServletResponse response) {
try{
token = URLEncoder.encode(token, "UTF-8").replaceAll("\\+", "%20");
Cookie accessCookie = new Cookie(AUTHORIZATION_HEADER, token);
accessCookie.setPath("/");
response.addCookie(accessCookie);
}catch (UnsupportedEncodingException e) {
log.error(e.toString());
}
}
// 토큰 자르기
public String subStringToken(String tokenValue) {
if(StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
throw new NullPointerException("유효하지 않은 토큰입니다.");
}
// 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰 사용자 정보 가져오기
public Claims getUerInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
// 쿠키에서 토큰값 가져오기
public String getJwtFromRequest(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if(cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8");
} catch (UnsupportedEncodingException e) {
log.error(e.toString());
return null;
}
}
}
}
return null;
}
}
UserDetailsImpl
@Getter
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
// 계정의 권한 목록 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getUser_role();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
// 계정의 비밀번호 리턴
@Override
public String getPassword() {
return user.getUser_pw();
}
// 계정의 고유값 리턴 (중복이 없는 계정의 식별자로 사용할 것으로 선정)
@Override
public String getUsername() {
return user.getUser_id();
}
// 계정의 만료 여부 리턴 (true 만료 안됨)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정의 잠김 여부 리턴 (true 잠기지 않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호 만료 여부 리턴 (true 만료 안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정의 활성화 여부 리턴 (true 활성화 됨)
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsServiceImpl
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
// 유저 정보를 리턴
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
User user = userRepository.findUserByUserId(userId).orElseThrow(() -> new UsernameNotFoundException("해당 유저를 찾을 수 없습니다. " + userId));
return new UserDetailsImpl(user);
}
}
기본적으로 검증을 할 수 있는 부분들을 구현을 하고 다시 진행 흐름을 이해할 수 있도록 반복을 하였다.
리프레시토큰과 Oauth는 기본적인 API를 구현을 하고 보안강화 목적으로 추가를 하면서 진행해 볼 예정이다.
'생각정리 > 사이드 프로젝트' 카테고리의 다른 글
사이드 프로젝트 : 중고 거래 플랫폼 - 리엑트 로그인, 회원가입 (0) | 2024.01.19 |
---|---|
사이드 프로젝트 : 중고 거래 플랫폼 - 게시글 (0) | 2024.01.18 |
사이드 프로젝트 : 중고 거래 플랫폼 - 타임스탬프, MariaDB, 순환 참조 (0) | 2024.01.15 |
사이드 프로젝트 : 중고 거래 플랫폼 - 엔티티 (0) | 2024.01.13 |
사이드 프로젝트 : 중고 거래 플랫폼 - API 명세, ERD 설계 (0) | 2024.01.10 |