+) Disclaimer : 필자는 현재 Spring boot를 활용한 Rest API를 개발 중.
1. JWT 인증 구현
1) JwtService 클래스 구현
- JWT token을 생성하고, claim에 담긴 정보를 추출하는 메소드 개발.
package com.example.demo.utils;
//라이브러리 생략
@Service
public class JwtService {
/*
JWT 생성
@param userNum
@return String
*/
public String createJwt(int userNum){
Date now = new Date();
return Jwts.builder()
.setHeaderParam("type","jwt")
.claim("userNum",userNum)
.setIssuedAt(now)
.setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*365))) //발급날짜 계산
.signWith(SignatureAlgorithm.HS256, Secret.JWT_SECRET_KEY) //signature 부분
.compact();
}
/*
Header에서 X-ACCESS-TOKEN 으로 JWT 추출
@return String
*/
public String getJwt(){
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
return request.getHeader("X-ACCESS-TOKEN");
}
/*
JWT에서 userIdx 추출
@return int
@throws BaseException
*/
public int getUserNum() throws BaseException{
//1. JWT 추출
String accessToken = getJwt();
if(accessToken == null || accessToken.length() == 0){
throw new BaseException(EMPTY_JWT);
}
// 2. JWT parsing
Jws<Claims> claims;
try{
claims = Jwts.parser()
.setSigningKey(Secret.JWT_SECRET_KEY)
.parseClaimsJws(accessToken);
} catch (Exception ignored) {
throw new BaseException(INVALID_JWT);
}
// 3. userNum 추출
return claims.getBody().get("userNum",Integer.class);
}
}
2) JWT 로그인 인증 구현
- jwt token을 활용한 실제 로그인 과정 구현
1️⃣ AccountProvider : userId, userPW 담은 객체 PostAuthReq을 가져와 입력값에 대한 validation 처리 후, 로그인 처리를 하러 accountProvider로 이동.
//AccountController 부분
/**
* 유저 로그인
* [POST] /accounts/auth
* @return BaseResponse<String>
*/
@ResponseBody
@PostMapping("/auth")
public BaseResponse<PostAuthRes> accountAuth(@RequestBody PostAuthReq postAuthReq) throws BaseException {
//입력값에 대한 validation
if(postAuthReq.getUser_id()==null){
return new BaseResponse<>(POST_USERS_EMPTY_EMAIL);
}
if(!isRegexEmail(postAuthReq.getUser_id())){
return new BaseResponse<>(POST_USERS_INVALID_EMAIL);
}
int checkAccountStatus=accountProvider.checkAccountStatus(postAuthReq.getUser_id());
if(checkAccountStatus==user_deactivated){
//비교시에 상수처리
return new BaseResponse<>(ACCOUNT_DEACTIVATED);
}
try{
PostAuthRes postAuthRes=accountProvider.accountAuth(postAuthReq);
return new BaseResponse<>(postAuthRes);
}catch(BaseException exception){
return new BaseResponse<>(exception.getStatus());
}
}
2️⃣ AccountProvider
- 입력값에 대한 비교 처리
- 기존 db에는 회원가입 시에 pw값을 알고리즘에 따라 encrypt해 저장하였으므로, request로 날아온 pw값을 encrypt한후, 실제 유저 pw와 대조하는 과정 거침
- 이후 jwt발급하여 로그인 처리.
public PostAuthRes accountAuth(PostAuthReq postAuthReq) throws BaseException {
Account acc=accountDao.getPwd(postAuthReq.getUser_id());
String encryptedPwd;
try{
encryptedPwd= new SHA256().encrypt(postAuthReq.getUser_pw());
}catch(Exception ignored) {
throw new BaseException(PASSWORD_ENCRYPTION_ERROR);
}
if(acc.getUser_pw().equals(encryptedPwd)){
int userNum=acc.getUser_num();
String jwt=jwtService.createJwt(userNum);
return new PostAuthRes(userNum,jwt);
}
else{
throw new BaseException(FAILED_TO_LOGIN);
}
}
3️⃣ AccountDao
- userId, userPW DB와 대조하여 각각 필요한 값 select 해옴.
public int checkAccountStatus(String userId){
String checkAccQuery = "select user_activate_status from User where user_id=?";
String checkAccParam=userId;
return this.jdbcTemplate.queryForObject(checkAccQuery,
int.class,
checkAccParam);
}
public Account getPwd(String userId){
String getPwdQuery = "select * from User where user_id=?";
String getPwdParam=userId;
return this.jdbcTemplate.queryForObject(getPwdQuery,
(rs,rowNum)-> new Account(
rs.getInt("user_num"),
rs.getString("user_id"),
rs.getString("user_pw"),
rs.getInt("user_nation_id"),
rs.getString("user_phone"),
rs.getInt("user_membership_id"),
rs.getDate("user_membership_date"),
rs.getTimestamp("user_join_date"),
rs.getInt("user_payment_id")
),getPwdParam);
}
- 차후에 로그인이 수행된 후 처리되어야 할 메소드들에 관해서 JWT 유효성 검증후에 남은 작업 수행.
- EX) 유저 정보 수정, 프로필 정보 수정, 영상물 리스트 조회..etc
- 실제 사용 예시
- postman을 통해서 id, pw를 json 객체로 생성하여 request했다.
- 일련의 과정을 거친 이후에 user number와 jwt 토큰이 response로 돌아왔다.
2. Interceptor를 활용한 회원 인가 프로세스 구현
- interceptor를 활용하기 이전에 필자는 jwt토큰을 검증하는 코드를 모든 컨트롤러 메소드에 일일히 추가해 넣었다. 이는 매우 비효율적이며, 반복적인 코드를 계속 작성하게 한다.
- 따라서 http request가 서버로 오는 동시에 자동적으로 먼저 request를 intercept해 jwt 검증 과정을 진행하면 따로 컨트롤러에 코드를 추가할 필요가 없다.
1️⃣ Interceptor 구현
- 인터셉터 동작 프로세스
- 우선 인가가 필요한 api인지 확인한다. 예를 들어 회원정보 변경, 게시물 작성..등등 많은 api들이 인증을 필요로 한다.
- 만약 인가가 필요하지 않은 api는 noAuth 어노테이션을 포함해주었으니 따로 인증이 필요하지 않으므로 바로 true를 리턴하여 메소드를 실행한다.
- 만약 인가가 필요한 api라면 “X-ACCESS-TOKEN”에 담긴 useridx값을 추출하여 request값에 포함하고, 다시 컨트롤러로 넘겨 남은 작업을 수행한다.
- 만약 jwt가 유효하지 않거나 존재하지 않으면 로그인을 다시 수행해야 하므로 로그인 api로 넘겨주고, 추후 인증이후에 다시 해당 페이지로 돌아올수 있도록 redirect_uri를 추가해서 보내준다.
@RequiredArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {
private final JwtService jwtService;
private final ObjectMapper objectMapper; //자바 객체를 json으로 serialization
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {
boolean check=checkAnnotation(handler, NoAuth.class);
if(check) return true;
try{
int userNumByJwt = jwtService.getUserNum();
request.setAttribute("userNum",userNumByJwt);
}catch(BaseException exception){
String requestURI= request.getRequestURI();
Map<String,String> map=new HashMap<>();
map.put("requestURI","/app/accounts/auth?redirectURI="+requestURI);
//redirectURI는 로그인 절차가 끝내고 다시 시도했던 페이지로 돌아가기 위해 JSON 정보에 포함시킨다.
String json=objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(map);
response.getWriter().write(json);
return false;
}
return true;
}
private boolean checkAnnotation(Object handler,Class cls){
HandlerMethod handlerMethod=(HandlerMethod) handler;
if(handlerMethod.getMethodAnnotation(cls)!=null){ //해당 어노테이션이 존재하면 true.
return true;
}
return false;
}
}
2️⃣ WebConfig 추가
- Configuration 파일을 만들어 controller 수행 이전에 interceptor가 작동하도록 해준다.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtService jwtService;
private final ObjectMapper objectMapper;
@Override
public void addInterceptors(InterceptorRegistry reg){
reg.addInterceptor(new AuthenticationInterceptor(jwtService,objectMapper))
.order(1)
.addPathPatterns("/**"); //interceptor 작업이 필요한 path를 모두 추가한다.
//.excludePathPatterns("app/accounts","/app/accounts/auth","app/videos/**");
// 인가작업에서 제외할 API 경로를 따로 추가할수도 있으나, 일일히 따로 추가하기 어려우므로 어노테이션을 따로 만들어 해결한다.
}
}
3️⃣ NoAuth 어노테이션 추가
- 따로 인증이 필요하지 않은 api 메소드 위에 어노테이션 형식으로 추가하기 위해 커스텀 어노테이션을 하나 만들어주도록 하겠다.
- configuration에 필터로 path를 일일히 추가할수도 있으나, 일일히 체크하는건 비효율적이다.
- 추후에 enum을 추가하여 회원/ 관리자 등의 role을 생성할 수도 있다.
@Retention(RetentionPolicy.RUNTIME) //어노테이션 레벨을 결정짓는다.
@Target({ElementType.TYPE,ElementType.METHOD})//선언된 어노테이션이 적용될수 있는 위치를 결정. TYPE-class,interface,enum에 적용.
public @interface NoAuth {
}
- 실제 사용 예시
- Postman을 통해서 인가가 필요한 api를 호출할때, header에 "X-ACCESS-TOKEN" 값으로 앞서 발급받은 jwt 토큰 값을 추가해주었다.
- 인가가 성공하면 다음 프로세스를 자동으로 실행한다.
'Web > Java (Spring+JSP)' 카테고리의 다른 글
Spring Webflux/Netty/MongoDB로 채팅 서버 구현 (0) | 2022.05.24 |
---|---|
Spring Boot에서 구글 소셜 로그인 REST 방식으로 구현하기 (14) | 2022.03.11 |
🍃 Spring Boot로 개발한 Restful API 작동 프로세스 (0) | 2022.03.03 |
Spring 핵심 원리 #9- 컴포넌트 스캔 (0) | 2022.01.15 |
Spring 핵심 원리 #8- 싱글톤 컨테이너 (0) | 2022.01.12 |