작업환경 : intellij
파일 구성
package kokoafriends.back.config.jwt;
// 인터페이스로 설정
public interface JwtProperties {
// JWT의 Signature를 해싱할 때 사용되는 비밀 키
String SECRET = "{}";
// 토큰 만료 기간. 초 단위로 계산. <refresh_token을 사용하지않는다면 설정할 것!>
int EXPIRATION_TIME = 864000000;
// 토큰 앞에 붙는 정해진 형식. Bearer 뒤에 한 칸 공백을 넣어줘야 함
String TOKEN_PREFIX = "Bearer ";
// 헤더의 Authorization 이라는 항목에 토큰을 넣어줄 것
String HEADER_STRING = "Authorization";
}
package kokoafriends.back.config.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import kokoafriends.back.repositorty.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RequiredArgsConstructor
// Bean을 따로 등록하지 않아도 사용가능
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
UserRepository userRepository;
// OncePerRequestFilter 메소드를 오버라이드
// 컴파일 체크
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청 헤더의 Authorization 항목 값을 가져와 jwtHeader 변수에 담음.
String jwtHeader = ((HttpServletRequest)request).getHeader(JwtProperties.HEADER_STRING);
if(jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)){
filterChain.doFilter(request, response);
return;
}
String token = jwtHeader.replace(JwtProperties.TOKEN_PREFIX, "");
Long userCode = null;
try {
userCode = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(token)
.getClaim("id").asLong();
} catch (TokenExpiredException e){
e.printStackTrace();
request.setAttribute(JwtProperties.HEADER_STRING, "토큰 만료");
} catch (JWTVerificationException e){
e.printStackTrace();
request.setAttribute(JwtProperties.HEADER_STRING, "유효하지않는 토큰");
}
request.setAttribute("userCode", userCode);
filterChain.doFilter(request, response);
}
}
// 설정 클래스
// 프론트와 통신할 시 설정값
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); // 서버 응답시 json 을 자바스크립트에서 처리할 수 있음
config.addAllowedOriginPattern("*"); // 모든 ip 에 응답 허용
config.addAllowedHeader("*"); // 모든 header 응답 허용
config.addExposedHeader("*"); // 모든 응답을 허용
config.addAllowedMethod("*"); // 모든 요청 메소드 응답 허용
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
// 유효기간 토큰 확인 후 예외처리 과정
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException {
String exception = (String) request.getAttribute(JwtProperties.HEADER_STRING);
String errorCode;
// JwtRequestFilter에서 Exception 발생 시 request에 추가한 요소를 불러와 담음
if(exception.equals("토큰 만료")){
errorCode = "토큰 만료";
setResponse (response, errorCode);
}
if (exception.equals("유효하지않는 토큰")){
errorCode = "유효하지않는 토큰";
setResponse (response, errorCode);
}
}
// 오류메세지를 담아주는 메서드를 생성
private void setResponse(HttpServletResponse response, String errorCode) throws IOException{
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(JwtProperties.HEADER_STRING + " : " + errorCode);
}
}
// Oauth 로그인 진행 순서
// 1. 인가 코드 발급(회원 인증)
// 2. 엑세스 토큰 발급(접근 권한 부여)
// 3. 액세스 토큰을 이용해 사용자 정보 불러오기
// 4. 불러온 사용자 정보를 토대로 자동 회원가입/로그인 진행
// ※ 소셜 플랫폼의 로그인과 프로젝트 앱의 로그인은 별개임!!
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserRepository userRepository;
public static final String FRONT_URL = "http://localhost:3000";
private CorsFilter corsFilter;
public SecurityConfig(CorsFilter corsFilter) {
this.corsFilter = corsFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement() // session 을 사용하지 않음
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().disable()
.formLogin().disable()
.addFilter(corsFilter); // @CrossOrigin(인증X), 시큐리티 필터에 등록 인증(O)
http.authorizeRequests()
.antMatchers(FRONT_URL+"/**")
.authenticated()
.anyRequest().permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.addFilterBefore(new JwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
package kokoafriends.back.Controller;
import com.fasterxml.jackson.databind.JsonNode;
import kokoafriends.back.config.jwt.JwtProperties;
import kokoafriends.back.model.User;
import kokoafriends.back.model.oauth.OauthToken;
import kokoafriends.back.service.UserService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.transaction.Transactional;
@RestController
@RequestMapping("/api")
@Transactional
public class UserController {
private UserService userService;
public UserController(@Qualifier("userService") UserService userService) {
this.userService = userService;
}
// 프론트에서 인가코드 돌려 받는 주소
// 인가 코드로 엑세스 토큰 발급 -> 사용자 정보 조회 -> DB 저장 -> jwt 토큰 발급 -> 프론트에 토큰 전달
@GetMapping("/oauth/callback/kakao/token")
public ResponseEntity getLogin(@RequestParam(value = "code", required = false) String code){
OauthToken oauthToken = userService.getAccessToken(code);
// 발급 받은 accessToken 으로 카카오 회원 정보 DB 저장
String jwtToken = userService.SaveUserAndGetToken(oauthToken.getAccess_token());
HttpHeaders headers = new HttpHeaders();
headers.add(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + jwtToken);
System.out.println("code : " + code);
System.out.println("oauthToken : " + oauthToken);
System.out.println("jwtToken : " + jwtToken);
System.out.println("headers : " + headers);
return ResponseEntity.ok().headers(headers).body("success");
// return oauthToken;
}
// jwt 토큰으로 유저정보 요청하기
@GetMapping("/me")
public ResponseEntity<Object> getCurrentUser(HttpServletRequest request){
User user = userService.getUser(request);
System.out.println("user" + user);
return ResponseEntity.ok().body(user);
}
}
package kokoafriends.back.model.oauth;
import lombok.Data;
@Data
public class KakaoProfile {
public Long id;
public String connected_at;
public Properties properties;
public KakaoAccount kakao_account;
@Data
public class Properties { //(1)
public String nickname;
public String profile_image; // 이미지 경로 필드1
public String thumbnail_image;
}
@Data
public class KakaoAccount { //(2)
public Boolean profile_nickname_needs_agreement;
public Boolean profile_image_needs_agreement;
public Profile profile;
public Boolean has_email;
public Boolean email_needs_agreement;
public Boolean is_email_valid;
public Boolean is_email_verified;
public String email;
@Data
public class Profile {
public String nickname;
public String thumbnail_image_url;
public String profile_image_url; // 이미지 경로 필드2
public Boolean is_default_image;
}
}
}
package kokoafriends.back.model.oauth;
import lombok.*;
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자를 만듦
@NoArgsConstructor // 파라미터가 없는 기본 생성자를 생성
@Data // data 어노테이션에서 getter와 setter 부분이 작동하지 않아서 아래 추가함.
@Getter
@Setter
// json형태 OauthToken 객체 데이터값 저장
public class OauthToken {
private String token_type;
private String access_token;
private int expires_in;
private String refresh_token;
private int refresh_token_expires_in;
private String scope;
}
package kokoafriends.back.model;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.sql.Timestamp;
@Entity
@Data
@NoArgsConstructor
// DB 테이블명이 클래스명과 다를 시 작성
@Table(name = "user_master")
public class User {
@Id
// auto_increment로 설정했다면 타입 설정할 것
@GeneratedValue(strategy = GenerationType.IDENTITY)
// 필드명이 다를 시 설정
@Column(name = "user_code")
private Long userCode;
@Column(name = "kakao_id")
private Long kakaoId;
@Column(name = "kakao_profile_img")
private String kakaoProfileImg;
@Column(name = "kakao_nickname")
private String kakaoNickname;
@Column(name = "kakao_email")
private String kakaoEmail;
@Column(name = "user_role")
private String userRole;
@Column(name = "create_time")
// current_timestamp를 설정했다면 어노테이션 설정할 것
@CreationTimestamp
private Timestamp createTime;
@Builder
public User(Long kakaoId, String kakaoProfileImg, String kakaoNickname,
String kakaoEmail, String userRole) {
this.kakaoId = kakaoId;
this.kakaoProfileImg = kakaoProfileImg;
this.kakaoNickname = kakaoNickname;
this.kakaoEmail = kakaoEmail;
this.userRole = userRole;
}
}
package kokoafriends.back.repositorty;
import kokoafriends.back.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
// 기본적인 CRUD 함수를 가지고 있음
// JpaRepository를 상속했기 때문에 @Repository 어노테이션 불필요
public interface UserRepository extends JpaRepository<User, Long> {
// JPA findBy 규칙
// select * from user_master where kakao_email = ?
public User findByKakaoEmail(String kakaoEmail);
public User findByUserCode(Long userCode);
}
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private UserRepository userRepository;
@Value("${kakao.clientId}")
String client_id;
@Value("${kakao.secret}")
String client_secret;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public OauthToken getAccessToken(String code){
OauthToken oauthToken = null;
try {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HttpBody 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type","authorization_code");
params.add("client_id", client_id);
params.add("redirect_uri", "http://localhost:3000/oauth/callback/kakao");
params.add("code", code);
params.add("client_secret", client_secret);
// headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers);
ObjectMapper objectMapper =
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// new ObjectMapper();
// POST 방식으로 key=value 데이터 요청
ResponseEntity<String> accessTokenResponse = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);
oauthToken = objectMapper.readValue(accessTokenResponse.getBody(), OauthToken.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return oauthToken;
}
public String SaveUserAndGetToken(String token) {
//(1)
KakaoProfile profile = findProfile(token);
//(2)
User user = userRepository.findByKakaoEmail(profile.getKakao_account().getEmail());
//(3)
if(user == null) {
user = User.builder()
.kakaoId(profile.getId())
//(4)
.kakaoProfileImg(profile.getKakao_account().getProfile().getProfile_image_url())
.kakaoNickname(profile.getKakao_account().getProfile().getNickname())
.kakaoEmail(profile.getKakao_account().getEmail())
//(5)
.userRole("ROLE_USER").build();
userRepository.save(user);
}
return createToken(user);
}
public String createToken(User user) {
String jwtToken = JWT.create()
.withSubject(user.getKakaoEmail())
.withExpiresAt(new Date(System.currentTimeMillis()+ JwtProperties.EXPIRATION_TIME))
.withClaim("id", user.getUserCode())
.withClaim("nickname", user.getKakaoNickname())
.sign(Algorithm.HMAC512(JwtProperties.SECRET));
return jwtToken;
}
public User getUser(HttpServletRequest request){
Long userCode = (Long) request.getAttribute("userCode");
User user = userRepository.findByUserCode(userCode);
return user;
}
//(1-1)
public KakaoProfile findProfile(String token) {
//(1-2)
RestTemplate rt = new RestTemplate();
//(1-3)
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token); //(1-4)
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
//(1-5)
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest =
new HttpEntity<>(headers);
//(1-6)
// Http 요청 (POST 방식) 후, response 변수에 응답을 받음
ResponseEntity<String> kakaoProfileResponse = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoProfileRequest,
String.class
);
//(1-7)
ObjectMapper objectMapper = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
kakaoProfile = objectMapper.readValue(kakaoProfileResponse.getBody(), KakaoProfile.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return kakaoProfile;
}
/* public JsonNode Logout(String autorize_code){
final String RequestUrl = "https://kapi.kakao.com/v1/user/logout";
final HttpClient client = HttpClientBuilder.create().build();
final HttpPost post =new HttpPost(RequestUrl);
post.addHeader("Authorization","Bearer" + autorize_code);
JsonNode returnNode =null;
try{
final HttpResponse response = client.execute(post);
ObjectMapper mapper = new ObjectMapper();
returnNode = mapper.readTree(response.getEntity().getContent());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch(ClientProtocolException e){
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
} finally{
}
return returnNode;}*/
}
server:
servlet:
encoding:
force: 'true'
enabled: 'true'
charset: UTF-8
context-path: /
port: '8080'
logging:
level:
org:
apache:
http: DEBUG
httpclient:
wire: DEBUG
spring:
jpa:
database: mysql
show-sql: 'true'
format-sql: 'true'
datasource:
password: '12345678'
driver-class-name: com.mysql.cj.jdbc.Driver
username: admin
url: mysql database 소스
kakao:
clientId : "REST API 키"
secret: "Client Secret 키"
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
id 'war'
}
group = 'kokoafriends'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '5.6.3'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.13.1'
// https://mvnrepository.com/artifact/com.auth0/java-jwt
implementation group: 'com.auth0', name: 'java-jwt', version: '3.10.2'
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
implementation 'com.google.code.gson:gson:2.8.7'
}
tasks.named('test') {
useJUnitPlatform()
}
참고 사이트