Skip to content

Commit

Permalink
[FEATURE] 회원가입 API 재학생 인증 (#97)
Browse files Browse the repository at this point in the history
* feat: 재학생 인증 API authToken 추가 (#93)

* feat: 회원가입 API 권한 추가 (#93)

* refactor: JwtTokenProvider 리팩토링 (#93)

* refactor: controller 리팩토링 (#93)

* feat: AuthVerified 어노테이션 (#93)

* feat: AuthVerified 어노테이션 등록 (#93)

* feat: 회원가입 API 재학생 인증 권한 설정 (#93)
  • Loading branch information
hyunmin0317 authored Nov 10, 2024
1 parent 0efb94d commit 294a44b
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.smunity.server.domain.account.dto.*;
import com.smunity.server.domain.account.service.AccountService;
import com.smunity.server.global.security.annotation.AuthVerified;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -19,19 +20,19 @@ public class AccountController {
private final AccountService accountService;

@PostMapping("/register")
public ResponseEntity<RegisterResponseDto> register(@Valid @RequestBody RegisterRequestDto requestDto) {
RegisterResponseDto responseDto = accountService.register(requestDto);
public ResponseEntity<RegisterResponseDto> register(@AuthVerified String verifiedUser, @RequestBody @Valid RegisterRequestDto requestDto) {
RegisterResponseDto responseDto = accountService.register(verifiedUser, requestDto);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
}

@PostMapping("/login")
public ResponseEntity<LoginResponseDto> login(@Valid @RequestBody LoginRequestDto requestDto) {
public ResponseEntity<LoginResponseDto> login(@RequestBody @Valid LoginRequestDto requestDto) {
LoginResponseDto responseDto = accountService.login(requestDto);
return ResponseEntity.ok(responseDto);
}

@PostMapping("/refresh")
public ResponseEntity<LoginResponseDto> refresh(@Valid @RequestBody RefreshRequestDto requestDto) {
public ResponseEntity<LoginResponseDto> refresh(@RequestBody @Valid RefreshRequestDto requestDto) {
LoginResponseDto responseDto = accountService.refresh(requestDto);
return ResponseEntity.ok(responseDto);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public class AccountService {
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;

public RegisterResponseDto register(RegisterRequestDto requestDto) {
validateUsername(requestDto.username());
public RegisterResponseDto register(String verifiedUser, RegisterRequestDto requestDto) {
validateUser(verifiedUser, requestDto.username());
Member member = requestDto.toEntity();
Year year = yearRepository.findByName(requestDto.username().substring(0, 4))
.orElseThrow(() -> new GeneralException(ErrorCode.YEAR_NOT_FOUND));
Expand Down Expand Up @@ -64,6 +64,17 @@ private LoginResponseDto generateToken(Long memberId, MemberRole memberRole) {
return LoginResponseDto.of(memberId, memberRole, accessToken, refreshToken);
}

private void validateUser(String verifiedUser, String username) {
validateVerified(verifiedUser, username);
validateUsername(username);
}

private void validateVerified(String verifiedUser, String username) {
if (!verifiedUser.equals(username)) {
throw new GeneralException(ErrorCode.UNVERIFIED_USER);
}
}

private void validateUsername(String username) {
if (memberRepository.existsByUsername(username)) {
throw new GeneralException(ErrorCode.ACCOUNT_CONFLICT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ public ResponseEntity<AnswerResponseDto> readAnswer(@PathVariable Long questionI
}

@PostMapping
public ResponseEntity<AnswerResponseDto> createAnswer(@AuthMember Long memberId, @PathVariable Long questionId, @Valid @RequestBody AnswerRequestDto requestDto) {
public ResponseEntity<AnswerResponseDto> createAnswer(@AuthMember Long memberId, @PathVariable Long questionId,
@RequestBody @Valid AnswerRequestDto requestDto) {
AnswerResponseDto responseDto = answerCommandService.createAnswer(memberId, questionId, requestDto);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
}

@PutMapping
public ResponseEntity<AnswerResponseDto> updateAnswer(@AuthMember Long memberId, @PathVariable Long questionId, @Valid @RequestBody AnswerRequestDto requestDto) {
public ResponseEntity<AnswerResponseDto> updateAnswer(@AuthMember Long memberId, @PathVariable Long questionId,
@RequestBody @Valid AnswerRequestDto requestDto) {
AnswerResponseDto responseDto = answerCommandService.updateAnswer(memberId, questionId, requestDto);
return ResponseEntity.ok(responseDto);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,26 @@ public record AuthResponseDto(
String username,
String name,
String department,
String email
String email,
String authToken
) {

private static final Map<String, String> DEPT_MAP = Map.of(
"지능·데이터융합학부", "핀테크전공",
"융합전자공학전공", "지능IOT융합전공"
);

public static AuthResponseDto from(JSONArray json) {
return from(json.getJSONObject(0));
public static AuthResponseDto of(JSONArray json, String authToken) {
return of(json.getJSONObject(0), authToken);
}

private static AuthResponseDto from(JSONObject obj) {
private static AuthResponseDto of(JSONObject obj, String authToken) {
return AuthResponseDto.builder()
.username(obj.getString("STDNO"))
.name(obj.getString("NM_KOR"))
.department(getDepartment(obj.getString("TMP_DEPT_MJR_NM")))
.email(obj.getString("EMAIL"))
.authToken(authToken)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.smunity.server.domain.auth.dto.AuthRequestDto;
import com.smunity.server.domain.auth.dto.AuthResponseDto;
import com.smunity.server.domain.auth.util.AuthUtil;
import com.smunity.server.global.security.provider.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.springframework.stereotype.Service;
Expand All @@ -14,9 +15,12 @@
@RequiredArgsConstructor
public class AuthService {

private final JwtTokenProvider jwtTokenProvider;

public AuthResponseDto authenticate(AuthRequestDto requestDto) {
JSONArray response = AuthUtil.getData(requestDto, "/UsrSchMng/selectStdInfo.do", "dsStdInfoList");
return AuthResponseDto.from(response);
String authToken = jwtTokenProvider.createAuthToken(requestDto.username());
return AuthResponseDto.of(response, authToken);
}

public List<AuthCourseResponseDto> readCourses(AuthRequestDto requestDto) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,19 @@ public ResponseEntity<Void> deleteMember(@AuthMember Long memberId) {
}

@PutMapping("/me")
public ResponseEntity<MemberInfoResponseDto> updateMember(@AuthMember Long memberId, AuthRequestDto requestDto) {
public ResponseEntity<MemberInfoResponseDto> updateMember(@AuthMember Long memberId, @RequestBody @Valid AuthRequestDto requestDto) {
MemberInfoResponseDto responseDto = memberCommandService.updateMember(memberId, requestDto);
return ResponseEntity.ok(responseDto);
}

@PatchMapping("/me/password")
public ResponseEntity<MemberInfoResponseDto> changePassword(@AuthMember Long memberId,
@Valid @RequestBody ChangePasswordRequestDto requestDto) {
public ResponseEntity<MemberInfoResponseDto> changePassword(@AuthMember Long memberId, @RequestBody @Valid ChangePasswordRequestDto requestDto) {
MemberInfoResponseDto responseDto = memberCommandService.changePassword(memberId, requestDto);
return ResponseEntity.ok(responseDto);
}

@PatchMapping("/me/department")
public ResponseEntity<MemberInfoResponseDto> changeDepartment(@AuthMember Long memberId,
@Valid @RequestBody ChangeDepartmentRequestDto requestDto) {
public ResponseEntity<MemberInfoResponseDto> changeDepartment(@AuthMember Long memberId, @RequestBody @Valid ChangeDepartmentRequestDto requestDto) {
MemberInfoResponseDto responseDto = memberCommandService.changeDepartment(memberId, requestDto);
return ResponseEntity.ok(responseDto);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public class QuestionController {
private final QuestionCommandService questionCommandService;

@GetMapping
public ResponseEntity<Page<QuestionReadResponseDto>> readQuestions(@AuthMember Long memberId, @ParameterObject @PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
public ResponseEntity<Page<QuestionReadResponseDto>> readQuestions(
@AuthMember Long memberId,
@ParameterObject @PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<QuestionReadResponseDto> responseDtoPage = questionQueryService.readQuestions(memberId, pageable);
return ResponseEntity.ok(responseDtoPage);
}
Expand All @@ -53,7 +55,8 @@ public ResponseEntity<QuestionResponseDto> updateQuestion(
}

@DeleteMapping("/{questionId}")
public ResponseEntity<Void> deleteQuestion(@AuthMember Long memberId, @AuthAdmin Boolean isAdmin, @PathVariable Long questionId) {
public ResponseEntity<Void> deleteQuestion(@AuthMember Long memberId, @AuthAdmin Boolean isAdmin,
@PathVariable Long questionId) {
questionCommandService.deleteQuestion(memberId, isAdmin, questionId);
return ResponseEntity.noContent().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
public enum MemberRole {

ROLE_USER("회원"),
ROLE_ADMIN("관리자");
ROLE_ADMIN("관리자"),
ROLE_VERIFIED("인증된 회원");

private final String description;
}
3 changes: 3 additions & 0 deletions src/main/java/com/smunity/server/global/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.smunity.server.global.security.resolver.AuthAdminArgumentResolver;
import com.smunity.server.global.security.resolver.AuthMemberArgumentResolver;
import com.smunity.server.global.security.resolver.AuthVerifiedArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
Expand All @@ -19,11 +20,13 @@ public class WebConfig implements WebMvcConfigurer {

private final AuthMemberArgumentResolver authMemberArgumentResolver;
private final AuthAdminArgumentResolver authAdminArgumentResolver;
private final AuthVerifiedArgumentResolver authVerifiedArgumentResolver;

// 커스텀 Argument Resolver 추가
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authMemberArgumentResolver);
resolvers.add(authAdminArgumentResolver);
resolvers.add(authVerifiedArgumentResolver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum ErrorCode {
INVALID_REFRESH_TOKEN(401, "ACCOUNT009", "리프레시 토큰이 유효하지 않습니다. 다시 로그인 해주세요."),
YEAR_NOT_FOUND(404, "ACCOUNT010", "해당 연도를 찾을 수 없습니다."),
DEPARTMENT_NOT_FOUND(404, "ACCOUNT010", "해당 학과를 찾을 수 없습니다."),
UNVERIFIED_USER(401, "ACCOUNT011", "인증되지 않은 사용자입니다."),

// Auth Errors
AUTH_UNAUTHORIZED(401, "AUTH001", "아이디 및 비밀번호가 일치하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.smunity.server.global.security.annotation;

import io.swagger.v3.oas.annotations.Parameter;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Parameter(hidden = true)
public @interface AuthVerified {

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// H2 콘솔과 Swagger UI 및 API 문서에 대한 접근 허용
.requestMatchers("/h2-console/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()

// 재학생 인증을 완료한 사용자 (ROLE_VERIFIED)
.requestMatchers("/api/v1/accounts/register").hasRole("VERIFIED")

// 모든 사용자
.requestMatchers("/api/v1/accounts/**", "/api/v1/auth", "/actuator/info").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/questions/**").permitAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,23 @@
@RequiredArgsConstructor
public class JwtTokenProvider {

private static final String CLAIM_IS_ACCESS_TOKEN = "isAccessToken";
private static final String CLAIM_MEMBER_ROLE = "memberRole";

private final JwtProperties jwtProperties;

/**
* JWT access 토큰 생성
*/
public String createAccessToken(Long memberId, MemberRole memberRole, boolean isRefresh) {
return Jwts.builder()
.subject(String.valueOf(memberId))
.claim("memberRole", memberRole.name())
.signWith(jwtProperties.getSecretKey())
.expiration(expirationDate(isRefresh))
.compact();
return createToken(String.valueOf(memberId), true, memberRole, isRefresh);
}

/**
* JWT auth 토큰 생성 (재학생 인증)
*/
public String createAuthToken(String username) {
return createToken(username, false, MemberRole.ROLE_VERIFIED, false);
}

/**
Expand Down Expand Up @@ -74,12 +79,31 @@ public boolean validateToken(String token, boolean isRefresh) {
*/
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
String memberRole = claims.get("memberRole").toString();
String memberRole = claims.get(CLAIM_MEMBER_ROLE, String.class);
Collection<? extends GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(memberRole));
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}

/**
* JWT auth 토큰의 사용자 이름 추출 (재학생 인증)
*/
public String getUsername(String token) {
Claims claims = getClaims(token);
return !claims.get(CLAIM_IS_ACCESS_TOKEN, Boolean.class) ? claims.getSubject() : null;
}

// JWT 토큰 생성
private String createToken(String subject, boolean isAccessToken, MemberRole memberRole, boolean isRefresh) {
return Jwts.builder()
.subject(subject)
.claim(CLAIM_IS_ACCESS_TOKEN, isAccessToken)
.claim(CLAIM_MEMBER_ROLE, memberRole.name())
.signWith(jwtProperties.getSecretKey())
.expiration(expirationDate(isRefresh))
.compact();
}

// 액세스 토큰의 만료 시간 계산
private Date expirationDate(boolean isRefresh) {
Date now = new Date();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.smunity.server.global.security.resolver;

import com.smunity.server.global.security.annotation.AuthVerified;
import com.smunity.server.global.security.provider.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

/**
* 컨트롤러 메서드의 파라미터가 @AuthVerified String 타입일 때 해당 파라미터를 처리하도록 지정하는 클래스
*/
@Component
@RequiredArgsConstructor
public class AuthVerifiedArgumentResolver implements HandlerMethodArgumentResolver {

private final JwtTokenProvider jwtTokenProvider;

/**
* 파라미터 타입 확인 (@AuthVerified, String)
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasAuthMemberAnnotation = parameter.hasParameterAnnotation(AuthVerified.class);
boolean isStringType = String.class.isAssignableFrom(parameter.getParameterType());
return hasAuthMemberAnnotation && isStringType;
}

/**
* 해당 컨트롤러 메서드의 파라미터 처리
*/
@Override
public String resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer,
@NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String jwt = jwtTokenProvider.resolveToken(request);
return jwtTokenProvider.getUsername(jwt);
}
}

0 comments on commit 294a44b

Please sign in to comment.