1. 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}
2. Config
2.1. SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtUtils jwtUtils;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf(csrf -> csrf.disable())
.formLogin((form) -> form.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/main", "/login", "/signUp").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterAt(new CustomUsernamePasswordAuthenticationFilter(
authenticationManager(authenticationConfiguration), jwtUtils
), UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
new JwtFilter(jwtUtils), CustomUsernamePasswordAuthenticationFilter.class
);
return httpSecurity.build();
}
}
3. JWT 관련 클래스
3.1. JwtUtils
@Component
public class JwtUtils {
private final SecretKey secretKey;
private final long tokenValidTime;
public JwtUtils(@Value("${jwt.secretKey}") String secretKey,
@Value("${jwt.tokenValidTime}") long tokenValidTime) {
this.secretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
this.tokenValidTime = tokenValidTime;
}
public String createToken(String username, String role) {
Date createTime = new Date();
Date expireTime = new Date(createTime.getTime() + this.tokenValidTime);
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(createTime)
.expiration(expireTime)
.signWith(secretKey)
.compact();
}
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
}
3.2. JwtFilter
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
private static final String AUTHORIZATION_HEADER = "Authorization";
private final JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (!StringUtils.hasText(bearerToken) || !bearerToken.startsWith("Bearer ")) {
logger.info("토큰이 null이거나 jwt 토큰이 아닙니다.");
filterChain.doFilter(request, response);
return;
}
String token = bearerToken.substring(7);
if (jwtUtils.isExpired(token)) {
logger.info("토큰의 유효 기간이 만료되었습니다.");
filterChain.doFilter(request, response);
return;
}
Member member = Member.builder()
.username(jwtUtils.getUsername(token))
.password("tempPassword")
.role(jwtUtils.getRole(token))
.build();
CustomUserDetails userDetails = new CustomUserDetails(member);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
3.3. CustomUserDetails
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final Member member;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
}
3.4. CustomUsernamePasswordAuthenticationFilter
@RequiredArgsConstructor
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String AUTHORIZATION_HEADER = "Authorization";
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
return authenticationManager.authenticate(authToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication) throws IOException, ServletException {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> authIterator = authorities.iterator();
GrantedAuthority auth = authIterator.next();
String role = auth.getAuthority();
String token = jwtUtils.createToken(username, role);
response.addHeader(AUTHORIZATION_HEADER, "Bearer " + token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(401);
}
}
4. 도메인
4.1. Member
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int memberId;
@NonNull
@Column(name = "username")
private String username;
@NonNull
@Column(name = "password")
private String password;
@NonNull
@Column(name = "role")
private String role;
}
5. DTO
5.1. SignUpRequest
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class SignUpRequest {
@NotBlank(message = "Please submit your id.")
private String username;
@NotBlank(message = "Please submit your pw.")
private String password;
}
5.2. SignUpResponse
@Getter
@AllArgsConstructor
public class SignUpResponse {
Boolean isSuccess;
}
6. 컨트롤러
6.1. SignUpController
@RestController
@RequiredArgsConstructor
public class SignUpController {
private final SignUpService signUpService;
@PostMapping("/signUp")
public ResponseEntity<SignUpResponse> signUp(@Valid @RequestBody SignUpRequest request) {
return ResponseEntity.ok(signUpService.signUp(request));
}
}
6. 서비스
6.1. CustomUserDetailsService
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> memberOptional = memberRepository.findByUsername(username);
if (memberOptional.isPresent()) {
return new CustomUserDetails(memberOptional.get());
}
return null;
}
}
6.2. SignUpService
@Service
@RequiredArgsConstructor
public class SignUpService {
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
public SignUpResponse signUp(SignUpRequest request) {
Boolean isExisted = memberRepository.existsByUsername(request.getUsername());
if (isExisted) {
return new SignUpResponse(false);
}
Member member = Member.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.role("ROLE_USER")
.build();
memberRepository.save(member);
return new SignUpResponse(true);
}
}
7. 레퍼지토리
7.1. MemberRepository
public interface MemberRepository extends JpaRepository<Member, Integer> {
Boolean existsByUsername(String username);
Optional<Member> findByUsername(String username);
}