Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

#1 인증 토큰 spring security, JWT #5

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
<scope>test</scope>
</dependency>

<!-- JSON Web Token Support -->
<!-- JSON Web Token Support
Spring Boot 및 Spring Security 덕분에
JWT 인증 서비스를 기록적인 시간 내에 가동 및 실행 -->
Copy link
Member

Choose a reason for hiding this comment

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

이것도 이 라이브러리의 용도와 다른 주석 같네요

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
Expand Down
19 changes: 12 additions & 7 deletions src/main/java/com/pay/billing/common/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,37 @@
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
/*
WebSecurityConfigurerAdapter는 사용자 지정 보안 구성을 제공하도록 확장 됩니다.
이 클래스에서 아래 Bean이 구성되고 인스턴스화됩니다.
- JwtTokenFilter
- PasswordEncoder
*/
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtTokenProvider jwtTokenProvider;

@Override
// 메서드 내에서 보호, 비보호 API 엔드 포인트를 정의하는 패턴을 구성합니다.
// 쿠키를 사용하지 않기 때문에 CSRF 보호를 비활성화했습니다.
protected void configure(HttpSecurity http) throws Exception {
Copy link
Member

Choose a reason for hiding this comment

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

이 메소드에 있는 전체 설정 한줄한줄에 대한 의도를 설명이 가능하실까요?


// Disable CSRF (cross site request forgery)
http.csrf().disable();

// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

// Entry points
http.authorizeRequests()//
.antMatchers("/users/signin").permitAll()//
.antMatchers("/users/signup").permitAll()//
.antMatchers("/h2-console/**/**").permitAll()
// Disallow everything else..
// 다른것들은 모두 비활성화
.anyRequest().authenticated();

// If a user try to access a resource without having enough permissions
// 요구사항을 만족하지 않는다면 예외(exceptionHandling)를 발생 시킨다.
Copy link
Member

Choose a reason for hiding this comment

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

이건 이 코드의 동작에 대해 설명하는게 아니라 단순 번역같네요. 내용도 다릅니다~ 서비스개발자는 꼼꼼함이 중요합니다. 그 꼼꼼함이 제일 치명적인 약점으로 보입니다.

http.exceptionHandling().accessDeniedPage("/login");

// Apply JWT
// Security에 JWT 적용
http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider));

// Optional, if you want to test the API from a browser
Expand All @@ -52,7 +57,7 @@ protected void configure(HttpSecurity http) throws Exception {

@Override
public void configure(WebSecurity web) throws Exception {
// Allow swagger to be accessed without authentication
// 인증없이 swagger에는 사용하게 설정
web.ignoring().antMatchers("/v2/api-docs")//
.antMatchers("/swagger-resources/**")//
.antMatchers("/swagger-ui.html")//
Expand Down
54 changes: 32 additions & 22 deletions src/main/java/com/pay/billing/common/security/JwtTokenFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,41 @@
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// We should use OncePerRequestFilter since we are doing a database call, there is no point in doing this more than once
/*
OncePerRequestFilter를 상속하여 구현한 경우 doFilter 대신 doFilterInternal 메서드를 구현
이렇게 필터를 정의하였으면 bean 선언만 하면 spring boot를 사용하는 경우 자동으로 filter가 추가되게 된다.

JwtTokenFilter 역할: JwtTokenFilter필터는 각각의 API에 인가된다 ex) /users/signin ,/users/signup.
1. Authorization 헤더에서 액세스 토큰을 확인한다.
2. 헤더에 액세스 토큰이있는 경우 인증을 위임한다.
3. JwtTokenProvider은 인증 프로세스의 결과에 따라 미충족시 인증 예외를 발생 시킵니다.
*/
public class JwtTokenFilter extends OncePerRequestFilter {

private JwtTokenProvider jwtTokenProvider;

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

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(httpServletRequest);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (CustomException ex) {
//this is very important, since it guarantees the user is not authenticated at all
SecurityContextHolder.clearContext();
httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
return;
private JwtTokenProvider jwtTokenProvider;

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

filterChain.doFilter(httpServletRequest, httpServletResponse);
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 헤더의 req.getHeader("Authorization") 값을 가져와 유효 하다면 값을 가져온다.
String token = jwtTokenProvider.resolveToken(httpServletRequest);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (CustomException ex) {
// 인증 실패시 호출
SecurityContextHolder.clearContext();
Copy link
Member

Choose a reason for hiding this comment

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

실패시에 이 코드를 왜 호출해야할까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Security에서 인증이 완료되고 인증 매니저가 해당 유저의 세션 생성 후 인가를 하는과정에서
인메모리 세션저장소인 SecurityContextHolder에 저장되는데, 해당 인가과정에서 유저 정보가 요청 권한이
부족할때 위 에러가 발생하는데, 처리 과정의 SecurityContextHolder 데이터가 threadlocal에 저장 되기 떄문에
clearContext(); 메서드를 통해 쓰레드 변수를 삭제해야 합니다.

httpServletResponse.sendError(ex.getHttpStatus().value(), ex.getMessage());
return;
}

// 인증 성공시 호출
filterChain.doFilter(httpServletRequest, httpServletResponse);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class JwtTokenFilterConfigurer extends SecurityConfigurerAdapter<DefaultS

private JwtTokenProvider jwtTokenProvider;

// 스프링 부트 security JwtTokenFilter을 추가합니다
public JwtTokenFilterConfigurer(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
Expand Down
94 changes: 47 additions & 47 deletions src/main/java/com/pay/billing/common/security/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,66 +24,66 @@
import java.util.stream.Collectors;

@Component
/*
1. 액세스 토큰 서명 확인
2. 액세스 토큰 에서 ID 및 권한 부여 클레임(사용자가 누구인지 URI, 무엇에 접근 할 수 있는지, 토큰 만료 시간 확인) 을 추출하고 이를 사용하여 UserContext를 만듭니다.
3. 액세스 토큰의 형식이 잘못 되었거나 만료 되었거나 토큰이 적절한 서명 키로 서명되지 않은 경우 인증 예외가 발생시킵니다.
*/
public class JwtTokenProvider {
@Value("${security.jwt.token.secret-key:secret-key}")
private String secretKey;

/**
* THIS IS NOT A SECURE PRACTICE! For simplicity, we are storing a static key here. Ideally, in a
* microservices environment, this key would be kept on a config-server.
*/
@Value("${security.jwt.token.secret-key:secret-key}")
private String secretKey;
@Value("${security.jwt.token.expire-length:3600000}")
private long validityInMilliseconds = 3600000; // 1h

@Value("${security.jwt.token.expire-length:3600000}")
private long validityInMilliseconds = 3600000; // 1h
@Autowired
private MyUserDetails myUserDetails;

@Autowired
private MyUserDetails myUserDetails;

@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
@PostConstruct
Copy link
Member

Choose a reason for hiding this comment

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

생성자 주입을 사용하면 이걸 실행 안시켜줘도 됩니다. 이 코드도 인터넷 어딘가에서 본 기억이 있네요. 자신의 것으로 만드셔야합니다. 가져온 코드를 고민 없이 그대로 사용하신 것 같습니다. 여태까지 공부했던 내용들이 녹아있지가 않네요

protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}

public String createToken(String username, List<Role> roles) {
public String createToken(String username, List<Role> roles) {

Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));
Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));

Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);

return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, secretKey)//
.compact();
}
return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, secretKey)//
.compact();
}

public Authentication getAuthentication(String token) {
UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}

public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
return null;
}

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR);
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import org.springframework.stereotype.Service;

@Service
/*
1. UserDetails 인터페이스는 사용자 관련 데이터를 검색하는데 사용한다.
2. 사용자 이름을 기반으로 사용자 엔터티를 찾는 loadUserByUsername() 이라는
하나의 메서드가 있으며 이를 재정 의하여 사용자를 찾는 프로세스를 사용자 지정
*/
public class MyUserDetails implements UserDetailsService {

@Autowired
Expand Down