application-oauth.properties
# Twitch
spring.security.oauth2.client.registration.twitch.client-id=클라이언트id
spring.security.oauth2.client.registration.twitch.client-secret=클라이언트시크릿
spring.security.oauth2.client.registration.twitch.client-authentication-method=POST
spring.security.oauth2.client.registration.twitch.scope=openid+chat:read+user:read:follows
spring.security.oauth2.client.registration.twitch.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.twitch.provider=twitch
spring.security.oauth2.client.registration.twitch.client-name=Twitch
spring.security.oauth2.client.registration.twitch.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.twitch.authorization-uri=https://id.twitch.tv/oauth2/authorize?response_type=code&claims=%7B%22userinfo%22%3A%7B%22picture%22%3Anull%2C%22preferred_username%22%3Anull%7D%7D
spring.security.oauth2.client.provider.twitch.token-uri=https://id.twitch.tv/oauth2/token
spring.security.oauth2.client.provider.twitch.user-info-uri=https://id.twitch.tv/oauth2/userinfo
spring.security.oauth2.client.provider.twitch.user-info-authentication-method=POST
spring.security.oauth2.client.provider.twitch.jwk-set-uri=https://id.twitch.tv/oauth2/keys
spring.security.oauth2.client.provider.twitch.user_name_attribute=sub
트위치 콘솔로 가서 OAuth 리디렉션 URL을 아래와 같이 설정한다.
scope에는 openid가 있어야 하며 +로 범위를 추가할 수 있다. scope에 대해서는
https://dev.twitch.tv/docs/api/reference/#get-users
Reference
Twitch Developer tools and services to integrate Twitch into your development or create interactive experience on twitch.tv.
dev.twitch.tv
원하는 정보에 대한 문서에서 아래와 같이 어떤 scope가 필요한지 나와있다.
Member
package com.ewok.twitchmeme.domain.member;
import com.ewok.twitchmeme.domain.BaseTimeEntity;
import com.ewok.twitchmeme.domain.post.Good;
import com.ewok.twitchmeme.domain.post.Post;
import com.ewok.twitchmeme.domain.post.Reply;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@NoArgsConstructor
@Entity
public class Member extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column
private Long twitchId; //트위치 자체id
@Column
private String nickname; //트위치 닉네임
@Column
private String picture; //트위치 프로필 이미지
@Enumerated(EnumType.STRING)
@Column
private Role role; //USER, ADMIN
//회원탈퇴 -> 작성한 게시글, 댓글, 좋아요 표시 모두 삭제
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Post> postList = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Reply> replyList = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Good> goodList = new ArrayList<>();
@Builder
public Member(Long id, Long twitchId, String nickname, String picture, Role role) {
this.id = id;
this.twitchId = twitchId;
this.nickname = nickname;
this.picture = picture;
this.role = role;
}
public Member update(String nickname, String picture) {
this.nickname = nickname;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
//연관관계 메서드
public void addPost(Post post) {
this.postList.add(post);
}
public void addReply(Reply reply) {
this.replyList.add(reply);
}
public void addGood(Good good) {
this.goodList.add(good);
}
}
사용자의 정보를 담당할 도메인이다.
@Enumerated(EnumType.STRING)
- JPA로 데이터베이스로 저장할 때 Enum값을 어떤 형태로 저장할지를 결정한다.
- 기본적으로는 int로 된 숫자가 저장된다.
- 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는지 알 수가 없어 문자열로 저장될 수 있도록 선언하였다.
package com.ewok.twitchmeme.domain.member;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
USER("ROLE_USER", "일반사용자"),
ADMIN("ROLE_ADMIN", "관리자");
private final String key;
private final String title;
}
각 사용자의 권한을 관리할 Enum 클래스이다.
스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야만 한다.
package com.ewok.twitchmeme.domain.member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByTwitchId(Long twitchId);
}
유저의 CRUD를 책임질 인터페이스이다.
findByTwitchId()은 소셜 로그인으로 반환되는 값 중 TwitchId을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메서드이다.
build.gradle에 스프링 시큐리티 관련 의존성이 있어야 한다.
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성이다.
package com.ewok.twitchmeme.dto;
import com.ewok.twitchmeme.domain.member.Member;
import com.ewok.twitchmeme.domain.member.Role;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributesKey;
private String nickName;
private String sub;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributesKey, String nickName, String sub, String picture) {
this.attributes = attributes;
this.nameAttributesKey = nameAttributesKey;
this.nickName = nickName;
this.sub = sub;
this.picture = picture;
}
public static OAuthAttributes ofTwitch(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.attributes(attributes)
.nameAttributesKey(userNameAttributeName)
.nickName((String) attributes.get("preferred_username"))
.sub((String) attributes.get("sub"))
.picture((String) attributes.get("picture"))
.build();
}
public Member toEntity() {
return Member.builder()
.nickname(nickName)
.twitchId(Long.parseLong(sub))
.role(Role.USER)
.picture(picture)
.build();
}
}
package com.ewok.twitchmeme.service;
import com.ewok.twitchmeme.domain.member.Member;
import com.ewok.twitchmeme.domain.member.MemberRepository;
import com.ewok.twitchmeme.dto.OAuthAttributes;
import com.ewok.twitchmeme.dto.SessionMember;
import com.ewok.twitchmeme.dto.twitch.AccessToken;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
private final HttpSession httpSession;
private final AccessToken accessToken;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.ofTwitch(userNameAttributeName, oAuth2User.getAttributes());
accessToken.setAccessToken(userRequest.getAccessToken().getTokenValue());
Member member = saveOrUpdate(attributes);
httpSession.setAttribute("member", new SessionMember(member));
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(member.getRoleKey())), attributes.getAttributes(), attributes.getNameAttributesKey());
}
private Member saveOrUpdate(OAuthAttributes attributes) {
Member member = memberRepository.findByTwitchId(Long.parseLong(attributes.getSub()))
.map(entity -> entity.update(attributes.getNickName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return memberRepository.save(member);
}
}
accessToken.setAccessToken(userRequest.getAccessToken().getTokenValue());
소셜 로그인 시 유저 AccessToken을 가져와 AccessToken DTO에 담는 코드이다. 유저가 팔로우 한 스트리머의 정보를 가져오기 위해 필요하다.
package com.ewok.twitchmeme.config;
import com.ewok.twitchmeme.domain.member.Role;
import com.ewok.twitchmeme.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.authorizeRequests()
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/api/v1/**", "/mypage/**").hasRole(Role.USER.name())
.anyRequest().permitAll()
.and()
.csrf().ignoringAntMatchers("/h2-console/**", "/api/**")
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).clearAuthentication(true)
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.defaultSuccessUrl("/")
.userInfoEndpoint().userService(customOAuth2UserService);
return http.build();
}
}
- headers().frameOptions().disable() : h2-console 화면을 사용하기 위해 옵션들을 disable하는 코드이다.
- authorizeHttpRequests() : URL별 권한 관리를 설정하는 옵션의 시작점이다.
- requestMatchers() : 권한 관리 대상을 지정하는 옵션이다. URL, HTTP 메서드별로 관리가 가능하다.
- anyRequest() : 설정한 값들 이외 나머지 URL들을 나타낸다. permitAll()을 통해 위에 설정한 /api/v1/**, "/mypage/**이외에는 전체 열람 권한을 주었다.
- logout().logoutSuccessUrl() : 로그아웃 기능에 대한 여러 설정의 진입점이다.
- oauth2Login() : OAuth2 로그인 기능에 대한 여러 설정의 진입점이다.
- userInfoEndpoint() : OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당한다.
- userService() : 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록한다. 리소스 서버(소셜 서비스 등)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.
참고
https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#oidc-authorization-code-grant-flow
Using OIDC to get OAuth Access Tokens
Using OIDC to get OAuth Access Tokens
dev.twitch.tv
'SpringBoot > 개인프로젝트' 카테고리의 다른 글
AWS 설정 (0) | 2023.05.01 |
---|---|
Summernote 적용 (0) | 2023.05.01 |
트위치 API 스트림 정보 가져오기 (0) | 2023.05.01 |
트위치 API를 이용해 정보 가져오기 (0) | 2023.05.01 |
트위치 API (0) | 2023.05.01 |