React
action함수, useActionData
- action함수 데이터 읽는 법
-> formData에 input의 입력값 존재,
-> input의 name속성의 이름이 존재해야 하고, 그 값을 formData.get()
안에 넣음
import React from 'react';
import {Form, Link, redirect, useActionData} from 'react-router-dom';
import styles from './LoginForm.module.scss';
import { AUTH_URL } from "../../config/host-config";
const LoginForm = () => {
const errorText = useActionData();
return (
<>
<Form method="post" className={styles.form}>
<h1>Log in</h1>
<p>
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" required/>
</p>
<p>
<label htmlFor="password">Password</label>
<input id="password" type="password" name="password" required/>
</p>
<div className={styles.actions}>
<button type="submit" className={styles.loginButton}>Login</button>
</div>
{errorText && <p className={styles.error}>{errorText}</p>}
<div className={styles.registerLink}>
<Link to="/sign-up">회원이 아니십니까? 회원가입을 해보세요</Link>
</div>
</Form>
</>
);
}
export default LoginForm;
export const loginAction = async ({ request }) => {
const formData = await request.formData();
const payload = {
email: formData.get('email'),
password: formData.get('password'),
};
const response = await fetch(`${AUTH_URL}/sign-in`, {
method: 'POST',
headers: { 'Content-Type' : 'application/json' },
body: JSON.stringify(payload)
})
if (response.status === 422) {
const errorText = await response.text();
return errorText
}
const responseData = await response.json();
console.log(responseData)
localStorage.setItem('userData', JSON.stringify(responseData))
return redirect('/')
}
route에서 로그인 회원 관리하기
- route의 최상위 꼭대기에 loader를 설정하면 하위 컴포넌트는 사용할 수 있지만 children한테는 loader를 보낼 수 없다.
-> id를 부여하면 모든 곳에서 데이터를 사용할 수 있다.
- auth.js
const getUserData = () => {
const userDataJson = localStorage.getItem('userData')
return JSON.parse(userDataJson);
};
export const getUserToken = () => {
return getUserData().token;
};
export const userDataLoader = () => {
return getUserData();
}
export const router = createBrowserRouter([
{
path: '/',
element: <RootLayout/>,
errorElement: <ErrorPage/>,
loader: userDataLoader,
id: 'user-data',
children: [
......
]
loader 데이터 사용하기
- 하위컴포넌트가 아닌 children에서는
useLoaderData()
가 아닌 useRouteLoaderData()
를 사용!
- 로그인시 입력받은 정보들을 모두 사용할 수 있음
import React from 'react';
import LoginForm from "../components/auth/LoginForm";
import Main from "../components/auth/Main";
import {useRouteLoaderData} from "react-router-dom";
const WelcomePage = () => {
const userData = useRouteLoaderData('user-data');
return (
<>
{ <LoginForm/>}
{ <Main/>}
</>
);
};
export default WelcomePage;
로그아웃
- Logout.js 라는 컴포넌트를 만들되, return할 컴포넌트를 만들지 않고
export할 액션 함수만 작성, 이 경우 반드시 redirect가 존재해야함
- Logout.js
import {redirect} from "react-router-dom";
export const logoutAction = () => {
localStorage.removeItem('userData');
return redirect('/')
};
const homeRouter = [
{
index: true,
element: <WelcomePage />,
action: loginAction
},
{
path: 'sign-up',
element: <SignUpPage />
},
{
path: 'logout',
action: logoutAction
}
];
- Main.js
- 웰컴페이지에 이미 로그인 액션이 걸려있으므로 로그아웃 액션을 걸기위해선 직접 액션을 걸어준다
const Main = () => {
const userData = useRouteLoaderData('user-data');
return (
<>
<h2>{userData.email}님 환영합니다.</h2>
<h3>현재 권한: [ {userData.role} ]</h3>
{}
{}
<Form action='/logout' method="POST">
<button>Logout</button>
</Form>
</>
);
};
- 프론트에서 fetch를 보낼때, 토큰 정보를 headers에 담아서 보내야 한다.
const response = await fetch(`${EVENT_URL}/page/${currentPage}?sort=date`,{
headers: { 'Authorization': `Bearer ` + token }
});
event게시판에 로그인 조건 걸기
- 로그인 했을 경우에만 event 게시판에 들어갈 수 있게 처리
- auth.js
export const authCheckLoader = () => {
const userData = getUserData();
if (!userData) {
alert('로그인이 필요한 서비스입니다.');
return redirect('/');
}
return null;
};
- route-config.js
- eventsRouter에 모두 조건을 걸어야 하기 때문에 eventsRouter를 children으로 가지고있는 곳에 걸어줌
import React from 'react';
import { createBrowserRouter } from 'react-router-dom';
import Home from '../pages/Home';
import RootLayout from '../layout/RootLayout';
import ErrorPage from '../pages/ErrorPage';
import Events from '../pages/Events';
import EventDetail,
{ loader as eventDetailLoader, action as deleteAction }
from '../pages/EventDetail';
import EventLayout from '../layout/EventLayout';
import NewEvent from '../pages/NewEvent';
import EditPage from '../pages/EditPage';
import { action as manipulateAction }
from '../components/EventForm';
import WelcomePage from '../pages/WelcomePage';
import SignUpPage from '../pages/SignUpPage';
import { loginAction } from '../components/auth/LoginForm';
import { authCheckLoader, userDataLoader } from './auth';
import { logoutAction } from '../pages/Logout';
const eventsRouter = [
{
index: true,
element: <Events />,
},
{
path: ':eventId',
loader: eventDetailLoader,
id: 'event-detail',
children: [
{
index: true,
element: <EventDetail />,
action: deleteAction
},
{
path: 'edit',
element: <EditPage />,
action: manipulateAction
},
]
},
{
path: 'new',
element: <NewEvent />,
action: manipulateAction
},
];
const homeRouter = [
{
index: true,
element: <WelcomePage />,
action: loginAction
},
{
path: 'sign-up',
element: <SignUpPage />
},
{
path: 'logout',
action: logoutAction
}
];
export const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
loader: userDataLoader,
id: 'user-data',
children: [
{
path: '/',
element: <Home />,
children: homeRouter
},
{
path: 'events',
element: <EventLayout />,
loader: authCheckLoader,
children: eventsRouter
},
]
},
]);
JPA
토큰 위조 검사 후 인증 완료 처리
- JwtAuthFilter
- userId 이외에 사용할 정보가 더 필요하므로 tokenInfo로 수정
- 토큰 위조 검사후에 토큰 인증 완료 처리
- 인증 완료 후에 auth에 인증 정보들을 넣어두고 클라이언트에서 사용
public class JwtAuthFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = parseBearerToken(request);
log.info("토큰 위조 검사 필터 작동!");
if (token != null) {
TokenUserInfo tokenInfo = tokenProvider.validateAndGetTokenInfo(token);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(tokenInfo.getRole().toString()));
AbstractAuthenticationToken auth
= new UsernamePasswordAuthenticationToken(
tokenInfo,
null,
authorities
);
auth.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
log.warn("토큰이 위조되었습니다.");
e.printStackTrace();
}
filterChain.doFilter(request, response);
}
글쓰기 제한
- EventService
- 일반 회원은 4개의 이벤트만 등록가능
-> 4개이상 등록시 에러 발생
public void saveEvent(EventSaveDto dto, String userId) {
EventUser eventUser = eventUserRepository.findById(userId).orElseThrow();
if (
eventUser.getRole() == Role.COMMON
&& eventUser.getEventList().size() >= 4
) {
throw new IllegalStateException("일반 회원은 더이상 이벤트를 등록할 수 없습니다.");
}
Event newEvent = dto.toEntity();
newEvent.setEventUser(eventUser);
Event savedEvent = eventRepository.save(newEvent);
log.info("saved event: {}", savedEvent);
}
권한에 따른 조건부 렌더링1
직접 걸어주어 조건부 렌더링 가능
- EventController
- PREMIUM, ADMIN만 detail 조회 가능
@PreAuthorize("hasAuthority('PREMIUM') or hasAuthority('ADMIN')")
@GetMapping("/{eventId}")
public ResponseEntity<?> getEvent(@PathVariable Long eventId) {
권한에 따른 조건부 렌더링2
SecurityConfig에서 설정 가능
- SecurityConfig
- antMatchers를 통해 삭제 요청은 ADMIN만 가능, 수정요청은 COMMON도 가능하게 설정 가능
- hasAnyAuthority를 이용하면 권한 이름 여러개 가능 ("ADMIN", "COMMON")...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE,"/events/*").hasAnyAuthority("ADMIN")
.antMatchers(HttpMethod.PUT, "/auth/promote").hasAuthority("COMMON")
.antMatchers("/", "/auth/**").permitAll()
.anyRequest().authenticated()
;
http.addFilterAfter(jwtAuthFilter, CorsFilter.class);
return http.build();
}
등급업 처리
- EventUserController
- Service에서 로그인한 사용자의 토큰정보에서 userId를 통해 확인
- 등급 변경을 진행
- 토큰 재발급 필요 (변경 사항이 있으면)
- 재발급 후 응답DTO에 담아서 반환
@PutMapping("/promote")
public ResponseEntity<?> promote(
@AuthenticationPrincipal TokenUserInfo userInfo
) {
try {
LoginResponseDto dto = eventUserService.promoteToPremium(userInfo.getUserId());
return ResponseEntity.ok().body(dto);
} catch (NoSuchElementException e) {
log.warn(e.getMessage());
return ResponseEntity.badRequest().body(e.getMessage());
}
}
public LoginResponseDto promoteToPremium(String userId) {
EventUser eventUser = eventUserRepository.findById(userId).orElseThrow();
eventUser.promoteToPremium();
EventUser promotedUser = eventUserRepository.save(eventUser);
String token = tokenProvider.createToken(eventUser);
return LoginResponseDto.builder()
.token(token)
.role(promotedUser.getRole().toString())
.email(promotedUser.getEmail())
.build();
}