Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jade.xyz] 방탈출 인증 관리 step3 과제 제출 #103

Merged
merged 6 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,29 @@
## 기능 구현 사항

- [x] 예약 생성 API
- [x] 예약 취소 API
- [x] 예약 취소 API

# Step 3

## 기능 요구사항

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요구사항 정리 👍


- [x] 관리자 역할을 추가한다.
- [x] 일반 멤버와 관리자 멤버를 구분한다.
- [x] 관리자 기능을 보호한다.
- [x] 관리자 관련 기능 API는 /admin 붙이고 interceptor로 검증한다.
- [x] 관리자 관련 기능 API는 authorization 헤더를 이용하여 인증과 인가를 진행한다.
- [] 그 외 관리자 API는 자유롭게 설계하고 적용한다.

## 프로그래밍 요구사항
- [x] 관리자를 등록하도록 하기 보다는 애플리케이션이 동작할 때 관리자는 추가될 수 있도록 한다.

## 관리자 기능 보호

- 사용자가 사용할 수 있는 기능과 관리자가 사용할 수 있는 기능을 구분한다.
- 예를 들면 아무나 테마를 추가하거나 삭제할 수 있으면 예약 시스템 관리에 문제가 발생할 수 있다.
- 반면에 예약은 관리자가 아니라도 누구나 할 수 있어야 한다.





32 changes: 32 additions & 0 deletions src/main/java/nextstep/admin/AdminController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nextstep.admin;

import nextstep.theme.ThemeRequest;
import nextstep.theme.ThemeService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;

@RestController
@RequestMapping("/admin/themes")
public class AdminController {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 ThemeController 와 AdminController 를 분리 👍


private final ThemeService themeService;

public AdminController(ThemeService themeService) {
this.themeService = themeService;
}

@PostMapping
public ResponseEntity<Void> createTheme(@RequestBody ThemeRequest themeRequest) {
Long id = themeService.create(themeRequest);
return ResponseEntity.created(URI.create("/themes/" + id)).build();
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTheme(@PathVariable Long id) {
themeService.delete(id);

return ResponseEntity.noContent().build();
}
}
42 changes: 42 additions & 0 deletions src/main/java/nextstep/auth/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package nextstep.auth;

import nextstep.member.Role;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AdminInterceptor implements HandlerInterceptor {

private final JwtTokenProvider jwtTokenProvider;

public AdminInterceptor(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String accessToken = request.getHeader("Authorization");
checkIsValidToken(request, response, accessToken);
return HandlerInterceptor.super.preHandle(request, response, handler);
}

private void checkIsValidToken(HttpServletRequest request, HttpServletResponse response, String accessToken) throws ServletException, IOException {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessToken 만으로 판단해서 예외를 던지는게 어떨까요 ?

if (accessToken == null ||
accessToken.length() < "Bearer ".length()) {
request.setAttribute("exception", "AuthenticationException");
request.getRequestDispatcher("/api/error").forward(request, response);
return;
}
String token = accessToken.substring("Bearer ".length());

if (!jwtTokenProvider.validateToken(token)
|| jwtTokenProvider.getRole(token) != Role.ADMIN) {
request.setAttribute("exception", "UnAuthorizationException");
request.getRequestDispatcher("/api/error").forward(request, response);
}
}
}

2 changes: 1 addition & 1 deletion src/main/java/nextstep/auth/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public TokenResponse createToken(TokenRequest tokenRequest) {
String password = tokenRequest.getPassword();
Member member = login(username, password);

return new TokenResponse(jwtTokenProvider.createToken(String.valueOf(member.getId())));
return new TokenResponse(jwtTokenProvider.createToken(String.valueOf(member.getId()), member.getRole()));
}

private Member login(String username, String password) {
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/nextstep/auth/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nextstep.auth;

import io.jsonwebtoken.*;
import nextstep.member.Role;
import org.springframework.stereotype.Component;

import java.util.Date;
Expand All @@ -11,7 +12,7 @@ public class JwtTokenProvider {
private final String secretKey = "learning-test-spring";
private final long validityInMilliseconds = 3600000;

public String createToken(String principal) {
public String createToken(String principal, Role role) {
Claims claims = Jwts.claims().setSubject(principal);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
Expand All @@ -20,6 +21,7 @@ public String createToken(String principal) {
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.claim("role", role)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
Expand All @@ -28,6 +30,10 @@ public String getPrincipal(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}

public Role getRole(String token) {
return Role.getRole(Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("role", String.class));
}

public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/nextstep/config/WebMvcConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package nextstep.config;

import nextstep.auth.AdminInterceptor;
import nextstep.auth.AuthenticationPrincipalArgumentResolver;
import nextstep.auth.JwtTokenProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;
Expand All @@ -17,6 +19,11 @@ public WebMvcConfiguration(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AdminInterceptor(jwtTokenProvider)).addPathPatterns("/admin/**");
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationPrincipalArgumentResolver(jwtTokenProvider));
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/nextstep/exception/ExceptionController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package nextstep.exception;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class ExceptionController {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별도 예외 처리를 위한 controller 보다는 ExceptionHandler 로 유도할 수는 없을까요 ?

@GetMapping("/api/error")
public void error(HttpServletRequest request) throws AuthenticationException {
String exception = (String) request.getAttribute("exception");

if ("AuthenticationException".equals(exception)) {
throw new AuthenticationException();
}
if ("UnAuthorizationException".equals(exception)) {
throw new UnAuthorizationException();
}
}
}
29 changes: 18 additions & 11 deletions src/main/java/nextstep/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@

public class Member {

private Long id;
private String username;
private String password;
private String name;
private String phone;
private final Long id;
private final String username;
private final String password;
private final String name;
private final String phone;
private final Role role;

public Member(String username, String password, String name, String phone, Role role) {
this(null, username, password, name, phone, role);
}

public Member(Long id, String username, String password, String name, String phone) {
this.id = id;
this.username = username;
this.password = password;
this.name = name;
this.phone = phone;
this(id, username, password, name, phone, Role.USER);
}

public Member(String username, String password, String name, String phone) {
public Member(Long id, String username, String password, String name, String phone, Role role) {
this.id = id;
this.username = username;
this.password = password;
this.name = name;
this.phone = phone;
this.role = role;
}

public Long getId() {
Expand All @@ -43,6 +46,10 @@ public String getPhone() {
return phone;
}

public Role getRole() {
return role;
}

public boolean isWrongPassword(String password) {
return !this.password.equals(password);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nextstep/member/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@RequestMapping("/members")
public class MemberController {

private MemberService memberService;
private final MemberService memberService;

public MemberController(MemberService memberService) {
this.memberService = memberService;
Expand Down
16 changes: 9 additions & 7 deletions src/main/java/nextstep/member/MemberDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Component;

import java.sql.PreparedStatement;
import java.util.Objects;

@Component
public class MemberDao {
Expand All @@ -17,37 +18,38 @@ public class MemberDao {
resultSet.getString("username"),
resultSet.getString("password"),
resultSet.getString("name"),
resultSet.getString("phone")
resultSet.getString("phone"),
Role.getRole(resultSet.getInt("role"))
);

public MemberDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public Long save(Member member) {
String sql = "INSERT INTO member (username, password, name, phone) VALUES (?, ?, ?, ?);";
String sql = "INSERT INTO member (username, password, name, phone, role) VALUES (?, ?, ?, ?, ?);";
KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, new String[] {"id"});
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, member.getUsername());
ps.setString(2, member.getPassword());
ps.setString(3, member.getName());
ps.setString(4, member.getPhone());
ps.setInt(5, member.getRole().getValue());
return ps;

}, keyHolder);

return keyHolder.getKey().longValue();
return Objects.requireNonNull(keyHolder.getKey()).longValue();
}

public Member findById(Long id) {
String sql = "SELECT id, username, password, name, phone from member where id = ?;";
String sql = "SELECT id, username, password, name, phone, role from member where id = ?;";
return jdbcTemplate.queryForObject(sql, rowMapper, id);
}

public Member findByUsername(String username) {
String sql = "SELECT id, username, password, name, phone from member where username = ?;";
String sql = "SELECT id, username, password, name, phone, role from member where username = ?;";
return jdbcTemplate.queryForObject(sql, rowMapper, username);
}
}
18 changes: 12 additions & 6 deletions src/main/java/nextstep/member/MemberRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

public class MemberRequest {

private String username;
private String password;
private String name;
private String phone;
private final String username;
private final String password;
private final String name;
private final String phone;
private final Role role;

public MemberRequest(String username, String password, String name, String phone) {
public MemberRequest(String username, String password, String name, String phone, Role role) {
this.username = username;
this.password = password;
this.name = name;
this.phone = phone;
this.role = role;
}

public String getUsername() {
Expand All @@ -30,7 +32,11 @@ public String getPhone() {
return phone;
}

public Role getRole() {
return role;
}

public Member toEntity() {
return new Member(username, password, name, phone);
return new Member(username, password, name, phone, role);
}
}
2 changes: 1 addition & 1 deletion src/main/java/nextstep/member/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@Service
public class MemberService {

private MemberDao memberDao;
private final MemberDao memberDao;

public MemberService(MemberDao memberDao) {
this.memberDao = memberDao;
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/nextstep/member/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nextstep.member;

import java.util.Arrays;

public enum Role {
USER(0), ADMIN(1);

private final int roleNumber;

Role(int roleNumber) {
this.roleNumber = roleNumber;
}

public int getValue() {
return roleNumber;
}


public static Role getRole(int role) {
return Arrays.stream(Role.values())
.filter(v -> v.getValue() == role)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("권한이 올바르지 않습니다."));
}

public static Role getRole(String role) {
return Arrays.stream(Role.values())
.filter(v -> v.name().equals(role))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("권한이 올바르지 않습니다."));
}
}
Loading