Spring-Boot: 2.7.7
Vue3
JWT
📂 디렉터리 구조
├─main
├─java
│ └─com
│ └─Project
│ │ Project.java
│ │
│ ├─domain
│ │ └─user
│ │ ├─controller
│ │ │ MemberController.java
│ │ │ TokenController.java
│ │ │
│ │ ├─dto
│ │ │ MemberAccessDto.java
│ │ │
│ │ ├─entity
│ │ │ Member.java
│ │ │
│ │ ├─ouath
│ │ │ OAuth2SuccessHandler.java
│ │ │ Token.java
│ │ │
│ │ ├─repository
│ │ │
│ │ └─service
│ │ CustomOAuth2UserService.java
│ │ MemberService.java
│ │ OAuth2Attribute.java
│ │ TokenService.java
│ │
│ └─global
│ ├─config
│ │ RedisConfig.java
│ │ SecurityConfig.java
│ │
│ ├─error
│ │ │ ErrorCode.java
│ │ │ ErrorResponse.java
│ │ │ GlobalExceptionHandler.java
│ │ │
│ │ └─exception
│ │ AccessDeniedException.java
│ │ NotFoundException.java
│ │
│ ├─filter
│ │ JwtAuthFilter.java
│ │
│ ├─response
│ BaseResponse.java
│
│
│
│
└─resources
application-env.yml
application.yml
💡 Spring Security란
인증 / 인가 및 보호 기능을 제공하는 프레임워크. Spring 기반 애플리케이션을 보호하기 위한 표준. 다양한 보안 솔루션을 제공한다.
- 다양한 인증방식 HTTP, Digest, OAuth2 등을 지원, CSRF와 같은 웹 공격 방어, SST/TLS 기능 제공
Srping Security 5.x 이상의 버전에서는 Java 8부터 지원
Spring-Security 5.4V 이상부터는 WebSecurityConfigurerAdapter 를 지원하지않고
SecurityFilterChain을 사용하여 Bean에 등록하는 것을 권장한다.
spring.io/blog
@Configuration public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authz) -> authz .anyRequest().authenticated() ) .httpBasic(withDefaults()); return http.build(); } }
아래와 같이 oauth2와 함께 커스텀해서 사용할 수 있다.
소셜로그인만 구현했을 경우.@Bean protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .httpBasic().disable() // Http basic Auth 기반으로 로그인 인증창이 뜸. disable 시에 인증창 뜨지 않음. .csrf().disable() // rest api이므로 csrf 보안이 필요없으므로 disable처리. .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS)// jwt token으로 인증하므로 stateless 하도록 처리. .and() .authorizeRequests() // 인증권한 페이지 설정 .antMatchers(HttpMethod.GET, "/api/~~").permitAll() //인증 무시하기 .anyRequest().authenticated() //인증 하기 .and() //Jwt토큰을 체크하는 부분(커스텀 필터) 추가 OAuth2login을 진행한 이후 //UsernamePasswordAuthenticationFilter.class .addFilterAfter(new JwtAuthFilter(tokenService, memberRepository), OAuth2LoginAuthenticationFilter.class) .oauth2Login() .loginPage("http://localhost:8080") //login이 필요한 경우 .successHandler(successHandler) //로그인이 정상적으로 성공했을 때 필요한 핸들러 ex) 회원가입, 토큰 생성 .userInfoEndpoint().userService(customOAuth2UserService); //유저 정보를 가져왔을 때 successHandler에서 사용하기 위한 추가적인 구현 부분 // .failureHandler(oAuth2AuthenticationFailureHandler); return http.build();
2023-05-25 추가
JwtAuthFilter : JWT 작성글에서 자세히 다룰 예정
successHandler : social login이 정상적으로 인증된 후 추가할 로직 처리 (ex. 회원가입 등..)
customOAuth2UserService : 유저 정보(Authentication)를 처리하고 저장할 로직
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final TokenService tokenService;
private final MemberRepository memberRepository;
private final RedisTemplate redisTemplate;
@Value("${LOGIN_SUCCESS_URL}") //client의 login 성공 페이지
private String loginSuccessUrl;
@Value("${jwt.refresh-token.expire-length}") //refresh Token 사용할 경우
private long refreshTokenExpiretime;
/**
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication 인증 프로세스 중에 생성된 Authentication 객체 (CustomUserService 에서 생성된다)
* the authentication process.
* @throws IOException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
Map<String, Object> attributes = oAuth2User.getAttributes();
Member member = memberRepository.findByEmail((String) attributes.get("email"));
if (member == null) { //회원 가입.
member = Member.builder()
.email((String) attributes.get("email"))
.nickname((String) attributes.get("nickname"))
.build();
Member save = memberRepository.save(member);
}
if (member.getDelFlag() == 1) { //재 가입 불가능 throw 던지기, Filter ExceptionHandler 작성
return;
}
Token token = tokenService.generateToken(member.getId(), "USER");
Cookie cookie = new Cookie("refresh-token", token.getRefreshToken());
// expires in 7 days
cookie.setMaxAge(60 * 60 * 24 * 14);
// optional properties
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setPath("/api");
// add cookie to response
response.addCookie(cookie);
// + refresh Token 저장 로직 추가
//client로 token 전달
response.sendRedirect(loginSuccessUrl + token.getToken());
}
}
Spring Security Ouath2에서는 google, facebook 등 Provider를 기본 제공해주는 기능이 있다.
하지만 오늘은 kakao Login 예제를 통해 Provider 설정까지 해볼 예정이다.
spring:
security:
oauth2:
client:
registration:
kakao:
client-id: ${KAKAO_CLIENT_ID} #REST API 키
redirect-uri: ${KAKAO_REDIRECT_URL} #설정한 redirect 주소
client-authentication-method: POST #변경 X
client-secret: ${KAKAO_CLIENT_SECRET} #보안 설정 시 Client Secret
authorization-grant-type: authorization_code #변경 X
scope: #동의항목을 통해 가져올 사용자 정보
- profile_nickname
- account_email
client_name: kakao
provider: #인증 및 정보를 가져오기 위한 provider 설정
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
@Builder(access = AccessLevel.PRIVATE)
class OAuth2Attribute {
private Map<String, Object> attributes;
private String attributeKey;
private String email;
private String nickname;
/**
*
* @param provider client 식별 name ex) kakao
* @param attributeKey attribute 식별 값
* @param attributes 전달 받은 정보
* @return
*/
static OAuth2Attribute of(String provider, String attributeKey,
Map<String, Object> attributes) {
switch (provider) {
case "kakao": //다른 소셜로그인 추가
return ofKakao(attributeKey, attributes);
default:
throw new RuntimeException();
}
}
/**
*
* @param attributeKey OAuth2 유저를 식별하기 위한 Key
* @param attributes OAuth2 유저 정보. 소셜 마다 attributes의 구조가 다를 수 있음
* @return 필요한 유저 정보
*/
private static OAuth2Attribute ofKakao(String attributeKey,
Map<String, Object> attributes) {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");
return OAuth2Attribute.builder()
.nickname((String) kakaoProfile.get("nickname"))
.email((String) kakaoAccount.get("email"))
.profile((String) kakaoProfile.get("profile_image_url"))
.attributes(kakaoAccount)
.attributeKey(attributeKey)
.build();
}
/**
*
* @return User 정보가 들어있는 Map
*/
Map<String, Object> convertToMap() {
Map<String, Object> map = new HashMap<>();
map.put("key", attributeKey);
map.put("nickname", nickname);
map.put("email", email);
map.put("profile", profile);
map.put("gender", gender);
map.put("age", age);
return map;
}
}
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
//Success Handler가 사용할 수 있도록 등록
OAuth2Attribute oAuth2Attribute =
OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
var memberAttribute = oAuth2Attribute.convertToMap();
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
memberAttribute, "email");
}
}
💡 OAuth2UserService 인터페이스란
번역: 인터페이스의 구현체는 클라이언트에게 부여된 액세스 토큰을 사용하여 UserInfo 엔드포인트에서 엔드 유저 (리소스 소유자)의 사용자 속성을 가져오고, OAuth2User 형태의 인증된 주체(AuthenticatedPrincipal)를 반환하는 역할을 담당합니다.
💡 DefaultOAuth2User 란
DefaultOAuth2User.java 에 적혀있는 설명
OAuth2User의 기본 구현입니다.
사용자 속성 이름은 제공자 간에 표준화되어 있지 않으므로 사용자의 "name" 속성을 위한 키를 생성자 중 하나에 제공해야 합니다. 이 키는 getAttributes()를 통해 Principal(사용자)의 "name"에 액세스하고 getName()에서 반환하는 데 사용됩니다.DefaultOAuth2User는 OAuth2User 인터페이스의 기본 구현체입니다. 이를 사용하는 이유는 다음과 같습니다:
표준화되지 않은 사용자 속성 이름: OAuth2 공급자마다 사용자 속성의 이름이 일관되지 않을 수 있습니다. 예를 들어, Google과 Facebook은 사용자의 이름 속성을 각각 "name"과 "displayName"으로 제공할 수 있습니다. DefaultOAuth2User는 이러한 다양한 속성 이름을 처리하고, 사용자의 이름 속성에 액세스하기 위해 사용자가 지정한 키를 사용할 수 있습니다.