-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #28 from Kusitms-28th-HDmedi-B/feat/login
Feat/login
- Loading branch information
Showing
19 changed files
with
694 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package kusitms.hdmedi.config; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
|
||
@Configuration | ||
public class AppConfig { | ||
@Bean | ||
public BCryptPasswordEncoder bCryptPasswordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package kusitms.hdmedi.config; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import kusitms.hdmedi.domain.user.User; | ||
import kusitms.hdmedi.service.user.UserService; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
@RequiredArgsConstructor | ||
public class JwtTokenFilter extends OncePerRequestFilter { | ||
|
||
private final UserService userService; | ||
private final String secretKey; | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); | ||
|
||
// authorizationHeader 비어있으면 로그인 X | ||
if(authorizationHeader == null) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
// authorizationHeader 값이 'Bearer '로 시작 안 하면 X | ||
if(!authorizationHeader.startsWith("Bearer ")) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
// 전송받은 값에서 'Bearer ' 뒷부분(JWT) 추출 | ||
String token = authorizationHeader.split(" ")[1]; | ||
|
||
// JWT 만료 시 인증 X | ||
if(JwtTokenUtil.isExpired(token, secretKey)) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
// JWT loginId 추출 | ||
String loginId = JwtTokenUtil.getLoginId(token, secretKey).get(); | ||
|
||
// 추출한 loginId 로 User 찾아오기 | ||
User loginUser = userService.getLoginUserByLoginId(loginId); | ||
|
||
// loginUser 정보로 UsernamePasswordAuthenticationToken 발급 | ||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( | ||
loginUser.getLoginId(), null, List.of(new SimpleGrantedAuthority(loginUser.getRole().name()))); | ||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
|
||
// 권한 부여 | ||
SecurityContextHolder.getContext().setAuthentication(authenticationToken); | ||
filterChain.doFilter(request, response); | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package kusitms.hdmedi.config; | ||
|
||
|
||
import com.auth0.jwt.JWT; | ||
import com.auth0.jwt.algorithms.Algorithm; | ||
import com.auth0.jwt.exceptions.JWTDecodeException; | ||
import com.auth0.jwt.interfaces.Claim; | ||
import com.auth0.jwt.interfaces.DecodedJWT; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
import java.util.Date; | ||
import java.util.Optional; | ||
|
||
@Slf4j | ||
public class JwtTokenUtil { | ||
|
||
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken"; | ||
|
||
private static final String ID_CLAIM = "loginId"; | ||
|
||
// JWT 발급 | ||
public static String createToken(String loginId, String key, long expireTimeMs) { | ||
|
||
Date now = new Date(); | ||
return JWT.create() | ||
.withSubject(ACCESS_TOKEN_SUBJECT) | ||
.withExpiresAt(new Date(now.getTime() + expireTimeMs)) // 토큰 만료 시간 설정 | ||
.withClaim(ID_CLAIM, loginId) | ||
.sign(Algorithm.HMAC512(key)); | ||
} | ||
|
||
// Claims 속 loginId 꺼내기 | ||
public static Optional<String> getLoginId(String token, String secretKey) { | ||
try { | ||
return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey)) | ||
.build() | ||
.verify(token) | ||
.getClaim(ID_CLAIM) | ||
.asString()); | ||
} catch (Exception e) { | ||
log.error("토큰이 유효하지 않습니다."); | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
// 토큰에서 만료 시간을 얻고, 현재 시간과 비교하여 토큰이 만료되었는지 확인 | ||
public static boolean isExpired(String token, String secretKey) { | ||
try { | ||
DecodedJWT jwt = JWT.require(Algorithm.HMAC512(secretKey)) | ||
.build() | ||
.verify(token); | ||
return jwt.getExpiresAt().before(new Date()); | ||
} catch (Exception e) { | ||
return true; // 토큰이 유효하지 않거나 파싱 중 오류가 발생하면 만료된 것으로 처리 | ||
} | ||
} | ||
|
||
// 토큰을 파싱하여 클레임을 얻는 메소드 | ||
public static Optional<Claim> getClaim(String token, String claimName, String secretKey) { | ||
try { | ||
DecodedJWT jwt = JWT.require(Algorithm.HMAC512(secretKey)) | ||
.build() | ||
.verify(token); | ||
return Optional.ofNullable(jwt.getClaim(claimName)); | ||
} catch (JWTDecodeException e) { | ||
return Optional.empty(); // 토큰이 유효하지 않거나 파싱 중 오류가 발생하면 빈 Optional 반환 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package kusitms.hdmedi.config; | ||
|
||
import kusitms.hdmedi.domain.user.UserRole; | ||
import kusitms.hdmedi.service.user.UserService; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.config.http.SessionCreationPolicy; | ||
import org.springframework.security.crypto.factory.PasswordEncoderFactories; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; | ||
import org.springframework.web.cors.CorsConfiguration; | ||
import org.springframework.web.cors.CorsConfigurationSource; | ||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | ||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector; | ||
|
||
import java.util.Arrays; | ||
|
||
import static org.springframework.security.config.Customizer.withDefaults; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
@RequiredArgsConstructor | ||
public class SecurityConfig { | ||
private final UserService userService; | ||
private static String secretKey = "hdmedi-team-b"; | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { | ||
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector); | ||
http | ||
.formLogin(formLogin -> formLogin.disable()) | ||
.httpBasic(httpBasic -> httpBasic.disable()) | ||
.csrf(csrf -> csrf.disable()) | ||
.cors(withDefaults()) | ||
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())) | ||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) | ||
.authorizeHttpRequests(request -> | ||
request.requestMatchers(mvcMatcherBuilder.pattern("/auth/login")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/css/**")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/js/**")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/images/**")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/error")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/favicon.ico")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/swagger-ui/**")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/swagger-resources/**")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/v3/api-docs/**")).permitAll() | ||
.requestMatchers(mvcMatcherBuilder.pattern("/auth/admin/**")).hasAuthority(UserRole.ADMIN.name()) | ||
.anyRequest().authenticated()) | ||
.addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class);; | ||
|
||
return http.build(); | ||
} | ||
|
||
@Bean | ||
protected CorsConfigurationSource corsConfigurationSource() { | ||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | ||
source.registerCorsConfiguration("/**", getDefaultCorsConfiguration()); | ||
|
||
return source; | ||
} | ||
|
||
// https://hdmedi.site/swagger-ui/index.html#/ | ||
private CorsConfiguration getDefaultCorsConfiguration() { | ||
CorsConfiguration configuration = new CorsConfiguration(); | ||
configuration.setAllowedOrigins( | ||
Arrays.asList("http://localhost:8080", "https://hdmedi.site", "http://localhost:3000")); | ||
configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 header 에 응답을 허용 | ||
configuration.setAllowedMethods(Arrays.asList("*")); // 모든 get,post,patch,put,delete 요청 허용 | ||
configuration.setAllowedOrigins(Arrays.asList("*")); // 모든 ip 응답을 허용 | ||
configuration.setAllowCredentials(true); // 내 서버가 응답할 때 json 을 자바스크립트에서 처리할 수 있게 할지를 설정하는 것 | ||
configuration.setMaxAge(3600L); | ||
|
||
return configuration; | ||
} | ||
|
||
// 비밀번호 암호화 | ||
@Bean | ||
public PasswordEncoder passwordEncoder() { | ||
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); | ||
} | ||
|
||
} |
45 changes: 45 additions & 0 deletions
45
src/main/java/kusitms/hdmedi/controller/user/UserController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package kusitms.hdmedi.controller.user; | ||
|
||
import kusitms.hdmedi.dto.request.user.LoginRequest; | ||
import kusitms.hdmedi.dto.response.user.InfoResponse; | ||
import kusitms.hdmedi.dto.response.user.LoginResponse; | ||
import kusitms.hdmedi.exception.base.BaseResponse; | ||
import kusitms.hdmedi.service.user.UserService; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.validation.BindingResult; | ||
import org.springframework.validation.ObjectError; | ||
import org.springframework.validation.annotation.Validated; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/auth") | ||
public class UserController { | ||
private final UserService userService; | ||
|
||
// 로그인 | ||
@PostMapping("/login") | ||
public BaseResponse<LoginResponse> webLogin(@RequestBody @Validated LoginRequest req, BindingResult bindingResult) throws Exception { | ||
if (bindingResult.hasErrors()) { | ||
ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); | ||
return BaseResponse.onFailure(400, objectError.getDefaultMessage(), null); | ||
} | ||
|
||
LoginResponse result = userService.webLogin(req); | ||
return BaseResponse.onSuccess(result); | ||
} | ||
|
||
// 유저 정보 | ||
@GetMapping("/info") | ||
public BaseResponse<InfoResponse> userInfo(Authentication auth) { | ||
|
||
InfoResponse result = userService.getUserInfo(auth.getName()); | ||
return BaseResponse.onSuccess(result); | ||
} | ||
|
||
@GetMapping("/admin") | ||
public String adminPage() { | ||
return "관리자 페이지 접근 성공"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package kusitms.hdmedi.domain.user; | ||
|
||
import jakarta.persistence.*; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor | ||
@Entity | ||
@Getter | ||
public class User { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
private String loginId; | ||
|
||
private String password; | ||
|
||
private String nickname; | ||
|
||
@Enumerated(value = EnumType.STRING) | ||
private UserRole role; | ||
|
||
@Builder | ||
public User(Long id, String loginId, String password, String nickname, UserRole role) { | ||
this.id = id; | ||
this.loginId = loginId; | ||
this.password = password; | ||
this.nickname = nickname; | ||
this.role = role; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package kusitms.hdmedi.domain.user; | ||
|
||
public enum UserRole { | ||
USER, ADMIN | ||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/kusitms/hdmedi/dto/request/user/LoginRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package kusitms.hdmedi.dto.request.user; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Data | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class LoginRequest { | ||
@NotBlank(message = "아이디를 입력해주세요.") | ||
|
||
private String loginId; | ||
@NotBlank(message = "비밀번호를 입력해주세요.") | ||
private String password; | ||
|
||
@Builder | ||
public LoginRequest(String loginId, String password) { | ||
this.loginId = loginId; | ||
this.password = password; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/main/java/kusitms/hdmedi/dto/response/user/InfoResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package kusitms.hdmedi.dto.response.user; | ||
|
||
import kusitms.hdmedi.domain.user.UserRole; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor | ||
@Getter | ||
public class InfoResponse { | ||
|
||
private String loginId; | ||
private String nickname; | ||
private UserRole role; | ||
@Builder | ||
public InfoResponse(String loginId, String nickname, UserRole role) { | ||
this.loginId = loginId; | ||
this.nickname = nickname; | ||
this.role = role; | ||
} | ||
} |
Oops, something went wrong.