Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT 회원관리(로그인/로그아웃/회원가입) 구현 #11

Merged
merged 7 commits into from
Jul 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ dependencies {

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4'

//jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
}

tasks.named('test') {
Expand All @@ -58,21 +62,21 @@ task copyGitSubmodule(type: Copy) {
into './src/main/resources'
}

/*
* queryDSL 설정 추가
* https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81
*/
//Querydsl Q Class 생성 위치
def querydslDir = "$buildDir/generated/querydsl"
//Querydsl Q Class 생성 위치 지정
tasks.withType(JavaCompile) {
/*
* queryDSL 설정 추가
* https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81
*/
//Querydsl Q Class 생성 위치
def querydslDir = "$buildDir/generated/querydsl"
//Querydsl Q Class 생성 위치 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}
//java source set 에 Querydsl Q Class 위치 추가
sourceSets {
//java source set 에 Querydsl Q Class 위치 추가
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
//gradle clean 시, Q Class 디렉토리까지 삭제하도록 설정
clean {
//gradle clean 시, Q Class 디렉토리까지 삭제하도록 설정
clean {
delete file(querydslDir)
}
2 changes: 1 addition & 1 deletion config
2 changes: 2 additions & 0 deletions src/main/java/com/pingpong/iosapp/IosAppApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class IosAppApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.pingpong.iosapp.sys.config;

import com.pingpong.iosapp.sys.config.jwt.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;

private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer ";

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
String token = getAccessToken(authorizationHeader);

if (tokenProvider.validToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String getAccessToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) {
return authorizationHeader.substring(TOKEN_PREFIX.length());
}

return null;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.pingpong.iosapp.sys.config;

import com.pingpong.iosapp.sys.service.UserDetailService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {

private final UserDetailService userService;

// 무시할 요청들에 대한 패턴 정의
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers("/static/**"); //정적 리소스 such as 이미지 or html
}

// HTTP 요청에 대한 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.authorizeRequests() // 인증,인가
.requestMatchers("/v3/api-docs/**", "/swagger*/**", "/login", "/signup").permitAll() //이 요청들은 인증/인가 작업을 수행하지 않음.
.anyRequest().authenticated() //나머지 요청에 대해서는 인가(권한)는 필요하지 않지만 인증(사용자정보)은 진행함.
.and()
.formLogin().disable()
.logout().invalidateHttpSession(true)
.and()
.build();
}

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.pingpong.iosapp.sys.config.jwt;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {

private String issuer;
private String secretKey;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.pingpong.iosapp.sys.config.jwt;

import com.pingpong.iosapp.sys.domain.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;

@RequiredArgsConstructor
@Service
public class TokenProvider {

private final JwtProperties jwtProperties;

public String generateToken(User user, Duration expiredAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}

private String makeToken(Date expiry, User user) {
Date now = new Date();

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getEmail())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}

public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);

return true;
} catch (Exception e) {
return false;
}
}


public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));

return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject
(), "", authorities), token, authorities);
}

public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}

private Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.pingpong.iosapp.sys.controller;

import com.pingpong.iosapp.sys.dto.CreateAccessTokenRequestDto;
import com.pingpong.iosapp.sys.dto.CreateAccessTokenResponseDto;
import com.pingpong.iosapp.sys.service.TokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class TokenController {

private final TokenService tokenService;

@PostMapping("/token")
public ResponseEntity<CreateAccessTokenResponseDto> createNewAccessToken(@RequestBody CreateAccessTokenRequestDto request) {
String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());

return ResponseEntity.status(HttpStatus.CREATED)
.body(new CreateAccessTokenResponseDto(newAccessToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.pingpong.iosapp.sys.controller;

import com.pingpong.iosapp.sys.dto.AddUserRequestDto;
import com.pingpong.iosapp.sys.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class UserController {

private final UserService userService;

@PostMapping("/signup")
public Long signup(@RequestBody AddUserRequestDto requestDto) {
return userService.save(requestDto);
}

@GetMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
}

}
35 changes: 35 additions & 0 deletions src/main/java/com/pingpong/iosapp/sys/domain/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.pingpong.iosapp.sys.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class RefreshToken {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false)
private Long id;

@Column(name = "user_id", nullable = false, unique = true)
private Long userId;

@Column(name = "refresh_token", nullable = false)
private String refreshToken;

public RefreshToken(Long userId, String refreshToken) {
this.userId = userId;
this.refreshToken = refreshToken;
}

public RefreshToken update(String newRefreshToken) {
this.refreshToken = newRefreshToken;

return this;
}
}

Loading