- application-oauth.properties
#Naver
spring.security.oauth2.client.registration.naver.client-id=
spring.security.oauth2.client.registration.naver.client-secret=
spring.security.oauth2.client.registration.naver.client-name=Naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
# Naver Provider ??!
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
# ???? ????? json?? ?????, response?? ???? ?????.
# KAKAO
spring.security.oauth2.client.registration.kakao.client-id=
spring.security.oauth2.client.registration.kakao.client-secret=
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email,gender,age_range,birthday
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
## kAKAO Provider ??!
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
application-oauth.properties의 설정을 마치고
- ouath2Login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<center><a href="http://localhost:8080/oauth2/authorization/naver"><img height="50" src="http://static.nid.naver.com/oauth/small_g_in.PNG"/></a></center>
<center><a href="http://localhost:8080/oauth2/authorization/kakao">카카오 아이디로 로그인</a></center>
</body>
</html>
로그인 버튼을 누르게 되면
- SecurityConfig.java
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserRepository userRepository;
private final CorsFilter corsFilter;
private final OAuth2SuccessHandler successHandler;
private final CustomOAuth2UserService oAuth2UserService;
private final TokenServiceImpl tokenService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(corsFilter)
.formLogin().disable()
.httpBasic().disable()
.addFilter(new JwtAuthenticationFilter(authenticationManager(),tokenService))
.authorizeRequests()
// .antMatchers("/oauth2/**").permitAll() //이거 빼고는 authenticationManager
.antMatchers("/api/v1/user/**")
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
// .anyRequest().authenticated() //토큰을 필요로 한
http.oauth2Login()
.loginPage("/oauth2LoginPage")
.successHandler(successHandler)
.userInfoEndpoint().userService(oAuth2UserService);
http.addFilterBefore(new JwtAuthorizationFilter(authenticationManager(), userRepository,tokenService), UsernamePasswordAuthenticationFilter.class);
}
}
사용자가 이용약관에 동의 하고 oauth2의 redirect도 문제가 없다면 oauth2login의 successHandler 부분으로 이동
(네이버나 카카오에서 제공하는 JSON을 파싱 하는 객체는 스압될 것 같아서 올리지 않겠다..)
- CustomOAuth2UserService.java
@Slf4j
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final BCryptPasswordEncoder passwordEncoder;
public CustomOAuth2UserService(BCryptPasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { //OAuth2를 통해 회원정보 불러오기
OAuth2User oAuth2User = super.loadUser(userRequest);
OAuth2UserInfo oAuth2UserInfo = null;
if (userRequest.getClientRegistration().getRegistrationId().equals("naver")) {
oAuth2UserInfo = new NaverUserInfo((Map<String, Object>) oAuth2User.getAttributes().get("response"));
log.info("네이버 오스 전체 값 확인 " + oAuth2UserInfo.toString());
log.info("네이버 로그인 중");
} else if (userRequest.getClientRegistration().getRegistrationId().equals("kakao")) {
oAuth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
log.info("카카오 오스 전체 값 확인 " + oAuth2UserInfo.toString());
log.info("카카오 로그인 중");
} else {
log.info("이게 작동 ?");
}
log.info("겟 젠더 확인 "+oAuth2UserInfo.getGender());
//소셜 로그인에서 제공하는 회원정보 파싱
String nameValidation = oAuth2UserInfo.getProviderId();
String socialName = oAuth2UserInfo.getProvider();
log.info("authentication principal getName() 값 확인 : " + nameValidation);
String username = socialName+"_"+nameValidation;
char gender = oAuth2UserInfo.getGender().charAt(0); //젠더 문자형으로 변환
String birth = oAuth2UserInfo.getBirthyear()+"-"+oAuth2UserInfo.getBirthday(); //태어난 년과 생일을 합침
String nickname = oAuth2UserInfo.getNickname();
String email = oAuth2UserInfo.getEmail();
String name = oAuth2UserInfo.getName();
String phone = oAuth2UserInfo.getPhone();
Role role = Role.USER; //유저 역할 enum
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); //날짜 날짜 형식 지정
Date BirthYearAddBirthDay = null;
try {
BirthYearAddBirthDay = formatter.parse(birth); //날짜 형식으로 변환
} catch (ParseException e) {
e.printStackTrace();
}
UserInfo userInfo = UserInfo.builder()
.username(username)
.password(passwordEncoder.encode(randomString())) //패스워드 랜덤 문자 저장
.gender(gender)
.nickname(nickname)
.email(email)
.name(name)
.phone(phone)
.birthday(BirthYearAddBirthDay)
.role(role)
.build();
return PrincipalDetails.builder()
.userInfo(userInfo)
.build();
}
public String randomString() {
int leftLimit = 48; // numeral '0'
int rightLimit = 122; // letter 'z'
int targetStringLength = 10;
Random random = new Random();
return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
소셜 로그인으로 받은 회원 정보들을 파싱 해주고 PrincipalDetails에 회원정보를 담아준다. (설명은 밑에)
패스워드는 랜덤 값을 만들고 그 랜덤 값을 다시 암호화해서 객체로 만든다.
@Slf4j
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final TokenServiceImpl tokenService;
private final UserRepository userRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
String username = principalDetails.getUserInfo().getUsername();
UserInfo userInfo = userRepository.findByUsername(username);
log.info("유저인포 값 확인 : " + userInfo);
if (userInfo == null) {
userRepository.save(principalDetails.getUserInfo()); // 최초 로그인이라면 회원가입 처리를 한다.
userInfo = userRepository.findByUsername(username);
}
principalDetails = PrincipalDetails
.builder()
.userInfo(userInfo)
.build();
Authentication auth = getAuthentication(principalDetails);
SecurityContextHolder.getContext().setAuthentication(auth); //@Authentication에 저장
log.info("유저인포 겟 아이디 확인 :" + principalDetails.getUserInfo().getUsername());
Token token = tokenService.generateToken(principalDetails.getUserInfo().getId(), "ROLE_USER"); //토큰 생성
log.info("{}", token); //토큰 값 확인
userInfo.setRefreshtoken(token.getRefreshToken()); //리프레쉬 토큰 유저정보 저장
log.info("업데이트 전 유저인포 값 확인 : " + userInfo);
userRepository.save(userInfo); //유저정보 리프레쉬 토큰 포함 업데이트
writeTokenResponse(response, token); //토큰 전달(?)
}
private void writeTokenResponse(HttpServletResponse response, Token token) throws IOException { //의미 확인 필요{
response.setContentType("text/html;charset=UTF-8");
System.out.println(token.getToken());
String authorizationToken = "Bearer "+token.getToken();
String refreshToken = "Bearer "+token.getRefreshToken();
// response.addHeader("Authorization", authorizationToken); //토큰약속 문자열 Bearer 포함 해야함
// response.addHeader("Refresh", refreshToken);
response.setContentType("application/json;charset=UTF-8");
response.sendRedirect("/sns?authorizationToken="+authorizationToken);
}
public Authentication getAuthentication(PrincipalDetails member) { //Authentication 객체에 담아서 자체적으로 전역으로 불러올 수 있도록 담음
return new UsernamePasswordAuthenticationToken(member, "",
Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
}
}
이제 다 성공하면 PrincipalDetail을 SecurityContextHolder에 담아준다. 여기에 담아주는 이유는 Controller에서 @AuthenticationPrincipal 어노테이션으로 로그인 처리를 굉장히 쉽게 할 수 있기 때문이다.
그리고 writeTokenResponse를 보면 파라미터로 직접 생성한 JWT 엑세스 토큰을 redirect로 보내준다 (이것도 보내주는 이유가 있음)
- UserController.java
/**
* SNS 로그인 성공 시 View단에 토큰 값 뿌려주기 위한 매핑 함수
* @return
*/
@GetMapping("/sns")
public String snsLoginComplete(
@RequestParam(value = "authorizationToken") String authorizationToken,
HttpServletResponse response) throws UnsupportedEncodingException {
System.out.println("SNS LOGIN COMPLETE PRINT : "+authorizationToken);
authorizationToken = URLEncoder.encode(authorizationToken, "utf-8");
Cookie cookie = new Cookie("Authorization", authorizationToken);
response.addCookie(cookie);
return "Login/SnsAuthorization";
}
그럼 받은 토큰을 바로 쿠키에 담아서 html로 리턴한다.
- SnsAuthorization.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SNS로그인 인증중..</title>
</head>
<body>
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script type="text/javascript">
let req = new XMLHttpRequest();
console.log(req.getAllResponseHeaders());
var name = "Authorization";
var answer;
var nameOfCookie = name + "="; //쿠키는 "쿠키=값" 형태로 가지고 있어서 뒤에 있는 값을 가져오기 위해 = 포함
var x = 0;
while (x <= document.cookie.length) { // --현재 세션에 가지고 있는 쿠키의 총 길이를 가지고 반복
var y = (x + nameOfCookie.length); //substring으로 찾아낼 쿠키의 이름 길이 저장
if (document.cookie.substring(x, y) == nameOfCookie) { //잘라낸 쿠키와 쿠키의 이름이 같다면
if ((endOfCookie = document.cookie.indexOf(";", y)) == -1) //y의 위치로부터 ;값까지 값이 있으면
endOfCookie = document.cookie.length; //쿠키의 길이로 적용하고
answer = unescape(document.cookie.substring(y, endOfCookie)); //쿠키의 시작점과 끝점을 찾아서 값을 반환
}
x = document.cookie.indexOf(" ", x) + 1; //다음 쿠키를 찾기 위해 시작점을 반환
if (x == 0){ //쿠키 마지막이면
break; //반복문 빠져나오기
}
}
answer = answer.replace("Bearer+","Bearer ")
console.log(answer);
window.sessionStorage.setItem("token",answer);
window.location.replace("/");
</script>
</body>
</html>
그럼 view단에서 쿠키를 해석해서 해당 엑세스 토큰으로 "/" 메인페이지로 이동한다
근데 여기서 위에 컨트롤러에서 바로 쿠키에 값을 담으면 "Bearer " 의 뒷부분 공백이 +로 바뀌어서 replace를 해줬다.
- index.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Team UP!</title>
<style>
pre{
overflow: scroll;
}
</style>
</head>
</html>
<body>
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script type="text/javascript">
$.ajax({
contentType: "application/json",
type: "GET",
url: "/thymeleaf/getUsername",
headers: {'Authorization':sessionStorage.getItem("token")},
dataType: "json",
}).done(function (res) {
console.log("응답 성공");
console.log(res.username);
if (res.username !== null) {
sessionStorage.setItem("username",res.username);
const loginMypage = document.getElementById("login/mypage");
loginMypage.innerHTML = `<a href="/myPage" id="login/mypage">마이페이지</a>`;
const joinLogout = document.getElementById("join/logout");
joinLogout.innerHTML = `<a href="/logout" id="join/logout">로그아웃</a>`;
}
})
.fail(function (res) {
console.log(res);
alert("알수없는 에러가 발생하였습니다");
});
</script>
<center><h2>TeamUP!</h2></center>
<p><a href="/board">게시판</a></p> <hr/>
<p><a href="/oauth2LoginPage">SNS 로그인</a></p> <hr/>
<p><a id="login/mypage" href="/loginPage">일반 로그인</a></p> <hr/>
<p><a id="join/logout" href="/joinPage">회원가입</a></p> <hr/>
</body>
그럼 이제 발급받은 엑세스 토큰으로 유저 아이디 요청을 해서 최종적으로 로그인 상태로 만든다.
- UserController.java
/**
* 인덱스 페이지 로그인 여부 랜더링 매핑 함수
*
* @param principalDetails
* @return
*/
@GetMapping("/thymeleaf/getUsername")
public ResponseEntity<?> getUsrename(@AuthenticationPrincipal PrincipalDetails principalDetails) {
Map<String, String> map = new HashMap<>();
if (principalDetails != null) {
map.put("username", principalDetails.getUserInfo().getUsername());
System.out.println("로그인 상태");
return ResponseEntity.ok(map);
} else {
map.put("username", null);
System.out.println("비로그인 상태");
return ResponseEntity.ok(map);
}
}
댓글