이전시간에는 회원가입과 로그인을 통해 인증을 구현했다면 이번에는 로그인을 했을때 발행되는 토큰을 통해 해당 유저에게 인가를 해주는 기능을 구현할 것이다.
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {
// OncePerRequestFilter : 매번 들어갈때마다 토큰을 체크를 하는 클래스
private final UserService userService;
private final String secretKey; // 외부에서 받아오는 키와 개발자의 고유키가 일치하는지 확인하기 위해 secretKey가 필요함
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 외부에서 Header에 담겨져 있는 토큰을 받아 작업하는 곳
final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); // 외부에서 header에 인증 AUTHORIZATION 값(Bearer 토큰) 담아 전송하는 것을 받을 수 있다.
log.info("authorizationHeader:{}",authorizationHeader);
// 정상적인 토큰이 없는 경우(접근 차단)
// 만약 Header에 토큰이 없거나 토큰이 Bearer로 시작하지 않다면 null을 반환하여 다음 체인(기능)으로 이동
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 정상적인 토큰이 있는 경우
// header에 저장된 token분리
String token;
try {
token = authorizationHeader.split(" ")[1]; // 토큰값만 추출함(Header 토큰에는 "Bearer 토큰"이 실린다. 즉, split을 통해 띄어쓰기를 기준으로 1번재 배열의 값만 추출하면 토큰값이다)
log.info("token:{}",token);
}catch (Exception e){ // 토큰 구조가 이상한 경우
log.error("token 추출에 실패 했습니다.");
filterChain.doFilter(request, response);
return;
}
// Token이 만료 되었는지 Check
// 위에서 Header에서 토큰값만 추출한 값과 고유 비밀키를 넘겨줘 토큰 시간을 체크한다.
if(JwtTokenUtil.isExpired(token, secretKey)){
filterChain.doFilter(request, response);
return;
};
// Token에서 Claim에서 UserName꺼내기
String userName = JwtTokenUtil.getUserName(token,secretKey); // 외부에서 받은 토큰에서 userName값을 추출함
log.info("userName:{}",userName);
// UserDetail 가져오기
UserEntity user = userService.getUserByUserName(userName); // 외부에서 받은 토큰에서 추출한 userName값을 통해 DB에서 해당 데이터를 찾는다.
log.info("userRole :{}",user.getRole());
// 문 열어주는 곳(인가 허용)
// principal에 이름을 넣어 controller에서 해당 이름을 호출하여 사용함
// new SimpleGrantedAuthority(user.getRole().name()) = user.getRole().name()가 USER 권한을 가질때 해당 사람에게만 인가를 해주기 위해 명단 설정(만약 등급별로 나눌때는 여러개의 값이 들어갈 수 있으므로 List로 저장함)
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name())) );
// 다음으로 권한을 넘겨주기 위해 details를 설정해준다.
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 권한 부여(문 열어줌)
// doFilter 다음 체인으로 넘어간다
filterChain.doFilter(request, response);
}
}
@EnableWebSecurity // 스프링 시큐리티를 활성화하는 어노테이션
@Configuration // 스프링의 기본 설정 정보들의 환경 세팅을 돕는 어노테이션
@RequiredArgsConstructor
// @EnableGlobalMethodSecurity(prePostEnabled = true) // Controller에서 특정 페이지에 권한이 있는 유저만 접근을 허용할 경우
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.token.secret}")
private String secretKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity // SecurityFilterChain에서 요청에 접근할 수 있어서 인증, 인가 서비스에 사용
.httpBasic().disable() // http basic auth 기반으로 로그인 인증창이 뜬다. 기본 인증을 이용하지 않으려면 .disable()을 추가해준다.
.csrf().disable() // csrf, api server이용시 .disable (html tag를 통한 공격)
.cors() // 다른 도메인의 리소스에 대해 접근이 허용되는지 체크
.and() // 묶음 구분(httpBasic(),crsf,cors가 한묶음)
.authorizeRequests() // 각 경로 path별 권한 처리
.antMatchers("/api/v1/users/join", "/api/v1/users/login","/api/v1/hello").permitAll() // 안에 작성된 경로의 api 요청은 인증 없이 모두 허용한다.
.antMatchers("/api/v1/**").authenticated() // 문 만들기(인증이 있어야 접근이 가능한 곳)
.and()
.sessionManagement() // 세션 관리 기능을 작동한다. .maximunSessions(숫자)로 최대 허용가능 세션 수를 정할수 있다.(-1로 하면 무제한 허용)
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀(STATELESS는 인증 정보를 서버에 담지 않는다.)
.and()
// UserNamePasswordAuthenticationFilter(로그인 필터)적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
// 로그인하기 전에 토큰을 받아 인가를 부여해주기 위한 기능
.addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
.authorizeRequests() // 각 경로 path별 권한 처리
.antMatchers("/api/v1/users/join", "/api/v1/users/login","/api/v1/hello").permitAll() // 안에 작성된 경로의 api 요청은 인증 없이 모두 허용한다.
.antMatchers("/api/v1/**").authenticated() // 문 만들기(인증이 있어야 접근이 가능한 곳)
따라서 "/api/v1"으로 시작하는 주소중 join,login,hello을 제외한 모든 곳에서는 인가가 필요한 것이다.
@AllArgsConstructor
@Getter
public enum UserRole {
ADMIN,USER
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@Entity
@Setter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@EntityListeners(AuditingEntityListener.class)
public class UserEntity extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 키 값
private String password; // 비밀번호
private String userName; // 유저 아이디
@Enumerated(EnumType.STRING) // Enum 클래스인 UserRole의 값을 받아와 저장함
private UserRole role; // 권한
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class UserJoinRequest {
private String userName;
private String password;
// 유저에게 입력받은 값을 UserEntity에 저장함
public UserEntity toEntity(String pwd){
UserEntity userEntity = UserEntity.builder()
.userName(this.userName)
.password(pwd)
.role(UserRole.USER) // 관리자를 제외한 회원은 모두 일반회원이므로 default값으로 저장
.build();
return userEntity;
}
}
public class JwtTokenUtil {
// 토큰 고유키값을 여기서 지정해서 할 수 있음. 그렇게 되면 아래 createToken에서 생성자에는 key값을 안받아도 됨
// @Value("${jwt.token.secret}")
// private String secretKey;
// 외부에서 받아온 토큰을 고유 키값으로 데이터를 추출한다.
private static Claims extractClaims(String token, String key) {
// 현재 암호화 되어 있는 token을 고유 키값을 가지고 풀어낸다.
return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
}
// 추출된 토큰에서 userName 찾기
public static String getUserName(String token,String secretkey){
return extractClaims(token, secretkey).get("userName").toString(); // 풀어낸 토큰값중 userName의 값을 반환해줌
}
// 추출된 토큰에서 토큰날짜 찾기
public static boolean isExpired(String token, String secretkey) {
// expire timestamp를 return함
Date expiredDate = extractClaims(token, secretkey).getExpiration(); // 풀어낸 토큰값중 날짜만 가져옴
return expiredDate.before(new Date()); // 받아온 토큰에 있는 날짜가 현재 시간보다 전인지 확인함
}
public static String createToken(String userName, long expireTimeMs, String key) {
Claims claims = Jwts.claims(); // 일종의 map
claims.put("userName", userName);
return Jwts.builder() // 토큰 생성
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis())) // 시작 시간 : 현재 시간기준으로 만들어짐
.setExpiration(new Date(System.currentTimeMillis() + expireTimeMs)) // 끝나는 시간 : 지금 시간 + 정해둔 시간
.signWith(SignatureAlgorithm.HS256, key) // 암호화 알고리즘, secret 값 세팅
.compact()
;
}
}