해당 게시글에서는 Access Token만 사용중 💻
#spring.application.name=bulletin_board
spring:
security:
user:
name: user
password: password
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: ...
username: ...
password: ...
jpa:
database: mysql
database-platform: org.hibernate.dialect.MariaDBDialect
generate-ddl: true
hibernate:
ddl-auto: create
show-sql: true
jwt:
expiration: 30
secretKey: mysecret
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// Customized된 JWT필터 주입
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
// 비밀번호 암호화
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf().disable() //csrf 보안 설정 비활성화
//csrf 사용하려면 frontend단에서 csrf token값을 전달해줘야함
.cors().and() //CORS 활성화
.httpBasic().disable()
.authorizeRequests() //보호된 URI에 접근할 수 있는 권한을 설정
.antMatchers("/member/create",
"/doLogin")
.permitAll() // 위 end point들은 인증이 필요 없음
.anyRequest().authenticated() // 나머지는 인증이 필요함
.and()
.sessionManagement() // 세션 관리 기능이 작동
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 더 이상 session cookie 방식의 인증 방식으로 인증 처리 하지 않음
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// 지정된 필터 앞에 커스텀 필터를 추가
// 인증을 처리하는 기본필터 UsernamePasswordAuthenticationFilter 대신
// 별도의 인증 로직을 가진 필터를 생성&사용하고 싶을 때 사용
.build();
}
}
@EnableWebSecurity@Configuration 어노테이션을 포함하고 있음WebSecurityConfigurerAdapter 를 상속하는 방식은 deprecated 됨@EnableMethodSecurity@EnableGlobalMethodSecurity 를 사용하는 것은 deprecated 됨@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) 에서securedEnabled : @Secured 사용 여부prePostEnabled : @PreAuthorize / @PostAuthorize사용 여부@PreAuthorize@PostAuthorizeClient가 요청을 할 때마다 인가 처리 담당
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.security.sasl.AuthenticationException;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Component
public class JwtAuthFilter extends GenericFilter {
@Value("${jwt.secretKey}")
private String secretKey;
public static final String HEADER_KEY = "Authorization";
public static final String PREFIX = "Bearer ";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1) 요청 header에서 Authorization 값을 가져옴
String bearerToken = ((HttpServletRequest) request).getHeader(HEADER_KEY);
try {
if (bearerToken != null) {
if (!bearerToken.substring(0, 7).equals(PREFIX)) {
// Bearer 형식이 아니면 예외가 발생한다.
throw new AuthenticationException("토큰이 일치하지 않아요.");
}
// "Bearer " 이후의 실제 토큰 부분만 추출
String token = bearerToken.substring(7);
//토큰 검증 및 claims 추출
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
// Authentication 객체 생성 위해서는 UserDetails 생성해야함
// [아래는 UserDetails 생성하는 코드]
// Spring Security에서의 권한들과 역할은 기본적으로 GrantedAuthority에 저장
// GrantedAuthority는 권한이나 역할의 이름을 반환하는 메소드를 제공
List<GrantedAuthority> authorities = new ArrayList<>();
// SimpleGrantedAuthority 클래스는 GrantedAuthority 인터페이스의 기본으로 구현되는 객체
// 역할을 설정
authorities.add(new SimpleGrantedAuthority("ROLE_" + claims.get("role")));
UserDetails userDetails = new User(claims.getSubject(), "", authorities);
// [아래는 Authentication 생성하는 코드]
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails,
"", userDetails.getAuthorities());
// 인증 완료
SecurityContextHolder.getContext().setAuthentication(authentication);
}
//filterchain에서 그 다음 filter로 넘어가도록 하는 메서드
chain.doFilter(request, response);
} catch (Exception e) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType("application/json"); // 인증 실패 시, 401 UNAUTHORIZED 응답을 반환
// httpServletResponse.getWriter().write(ErrorResponseDto.makeMessage(HttpStatus.UNAUTHORIZED,
// e.getMessage()).toString());
}
}
}
🦑 Claim
JWT의 본문 (payload)에 포함된 데이터
🦑 왜 "ROLE_" + claims.get("role") 형식?
Spring Security에서는 권한(Authority)을 부여할 때 역할 이름이 "ROLE_"로 시작해야 함
🦑 getSubject()?
UserDetails userDetails = new User(claims.getSubject(), "", authorities);
Claims claims = Jwts.claims().setSubject(email);-> 그래서 getSubject() 를 하면 사용자의 이메일을 가져오게 됨!
🦑 GrantedAuthority & UserDetails & Authentication?
✅ GrantedAuthority
사용자의 role 정보를 담는 객체
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + claims.get("role")));
-> 위에서는 "ROLE_USER" 역할을 가진다는 걸 저장
✅ UserDetails
Spring Security에서 사용자의 정보를 담는 객체
UserDetails userDetails = new User(claims.getSubject(), "", authorities);
1) claims.getSubject() : 이메일 (사용자 이름 / 식별자)
2) "" : 비밀번호 (여기서는 JWT 인증이라 필요 없음)
3) authorities: 권한 리스트
✅ Authentication
Spring Security에서 인증된 사용자의 정보를 저장하는 객체
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
-> UsernamePasswordAuthenticationToken을 사용하면 JWT 기반 인증을 Security Context에 저장 가능
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenProvider {
@Value("${jwt.secretKey}")
private String secretKey;
@Value("${jwt.expiration}")
private int expiration;
public String createToken(String email, String role) {
Claims claims = Jwts.claims().setSubject(email); //이메일을 저장
claims.put("role", role); // 역할(role) 추가
Date now = new Date();
JwtBuilder jwtBuilder = Jwts.builder();
jwtBuilder.setClaims(claims);
jwtBuilder.setIssuedAt(now);
// 만료시간 설정
jwtBuilder.setExpiration(new Date(now.getTime() + expiration * 60 * 1000L));
// 서명
jwtBuilder.signWith(SignatureAlgorithm.HS256, secretKey);
return jwtBuilder.compact(); //토큰 생성
}
}
1) 사용자 email과 role을 Claims에 저장한다.
2) 발급 시간(issuedAt)과 만료 시간(expiration)을 설정한다.
3) HS256 알고리즘을 사용해 서명(Sign)한다.
4) 최종적으로 JWT 토큰을 문자열 형태로 반환한다.
package com.ecommerce.online_shopping.member;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
@Data
public class LoginReqDto {
@NotEmpty(message = "이메일을 입력하세요.")
@Email(message = "옳지 않은 형식의 이메일이에요.")
private String email;
@NotEmpty(message = "비밀번호를 입력하세요.")
@Size(min = 4, message = "비밀번호는 최소 4글자 이상이어야해요.")
private String password;
}
@PostMapping("/doLogin")
public ResponseEntity<CommonResponse> login(@Valid @RequestBody LoginReqDto loginReqDto) {
Member member = memberService.login(loginReqDto); // 이메일과 비밀번호 확인
String jwtToken = jwtTokenProvider.createToken(member.getEmail(), member.getRole().toString()); //JWT 생성
Map<String, Object> member_info = new HashMap<>();
member_info.put("id", member.getId());
member_info.put("token", jwtToken);
return new ResponseEntity<>(new CommonResponse(HttpStatus.OK, "로그인에 성공했어요!", member_info), HttpStatus.OK);
}
1) loginReqDto 에서 이메일과 비밀번호를 받는다.
2) memberService.login() 을 호출해서 이메일과 비밀번호를 확인한다.
3) 로그인 성공시, jwtTokenProvider.createToken() 으로 JWT를 발급한다.
4) 사용자 정보 (id와 token)를 CommonResponse에 담아서 반환한다.
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public Member login(LoginReqDto loginReqDto) throws IllegalArgumentException {
Member member = memberRepository.findByEmail(loginReqDto.getEmail()).orElseThrow(() -> new IllegalArgumentException("해당 이메일을 가지고 있는 회원은 존재하지 않아요."));
if (!passwordEncoder.matches(loginReqDto.getPassword(), member.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않아요.");
}
return member;
}
}
1) email을 기준으로 회원을 찾는다.
2) 회원이 없으면 예외가 발생한다.
3) passwordEncoder.matches() 로 비밀번호가 일치하는지 확인한다.
4) 비밀번호까지 맞으면 Member 객체를 반환한다.
localhost:8080/member/create)
localhost:8080/user/login)1) Body JSON Data (@RequestBody)

2) Tests 설정

var jsonData = pm.response.json();pm.environment.set("my_token", jsonData.result.token);pm.environment.set() 는 Postman에서의 환경변수 설정 / 업데이트하는 함수my_token이라는 환경변수에 JSON Response의 result 부분의 token값을 넣어줌3) Token 구성

4) 추후 API Test

✅ Postman의 Auth에서 Type을 Bearer Token 지정 후, Postman의 환경변수인 {{my_token}}을 설정해줘야함

✅ Postman의 기본 저장소는 'Globals'이기 때문에 반드시 Postman의 environment는 본인이 설정한 environment임을 확인해야함!
출처 : https://velog.io/@shon5544/Spring-Security-4.-%EA%B6%8C%ED%95%9C-%EC%B2%98%EB%A6%AC