양호한 JWT 인증 필터 설계 방법
JWT는 처음입니다.어쩔 수 없이 왔기 때문에 인터넷에는 정보가 별로 없습니다.저는 이미 스프링 세션을 사용하여 스프링 보안을 사용하여 스프링 부트 애플리케이션을 개발했습니다.이제 봄 세션 대신 JWT로 이동합니다.링크를 거의 찾지 못했기 때문에 사용자를 인증하고 토큰을 생성할 수 있게 되었습니다.여기서 어려운 부분은 서버에 대한 모든 요청을 인증하는 필터를 만들고 싶다는 것입니다.
- 필터는 토큰을 어떻게 검증합니까?(서명 확인만으로 충분합니까?)
- 다른 사람이 토큰을 훔쳐서 전화를 걸었을 경우, 어떻게 확인할 수 있습니까?
- 필터에서 로그인 요청을 우회하려면 어떻게 해야 합니까?인증 헤더가 없기 때문에
다음에, 필요한 것을 실행할 수 있는 필터를 나타냅니다.
public class JWTFilter extends GenericFilterBean {
private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = this.resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
if (this.tokenProvider.validateToken(jwt)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
this.resetAuthenticationAfterRequest();
} catch (ExpiredJwtException eje) {
LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
LOGGER.debug("Exception " + eje.getMessage(), eje);
}
}
private void resetAuthenticationAfterRequest() {
SecurityContextHolder.getContext().setAuthentication(null);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
return null;
}
}
필터 체인에 필터 포함:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public final static String AUTHORIZATION_HEADER = "Authorization";
@Autowired
private TokenProvider tokenProvider;
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JWTFilter customFilter = new JWTFilter(this.tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
// @formatter:off
http.authorizeRequests().antMatchers("/css/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/authenticate").permitAll()
.anyRequest().fullyAuthenticated()
.and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and().logout().permitAll();
// @formatter:on
http.csrf().disable();
}
}
TokenProvider 클래스:
public class TokenProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
@Value("${spring.security.authentication.jwt.validity}")
private long tokenValidityInMilliSeconds;
@Value("${spring.security.authentication.jwt.secret}")
private String secretKey;
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);
Date issueDate = Date.from(now.toInstant());
Date expirationDate = Date.from(expirationDateTime.toInstant());
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();
Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
LOGGER.info("Invalid JWT signature: " + e.getMessage());
LOGGER.debug("Exception " + e.getMessage(), e);
return false;
}
}
}
이제 질문에 답변해 드리겠습니다.
- 이 필터로 완료
- HTTP 요청 보호, HTTPS 사용
- 다 해 주세요.
/login
URI)/authenticate
로)로)로)
코드 임플리메이션에 대해서는 언급하지 않고 JWT에 관한 일반적인 힌트에 초점을 맞춘다(다른 답변 참조).
필터는 토큰을 어떻게 검증합니까?(서명 확인만으로 충분합니까?)
RFC7519 에서는 JWT 의 검증 방법이 규정되어 있습니다(7.2 참조). JWT 검증)은 기본적으로 구문 검증 및 서명 검증입니다.
JWT가 인증 흐름에서 사용되는 경우 Open에서 제안된 검증을 확인할 수 있습니다.ID 접속 사양 3.1.3.4 ID 토큰 검증.개요:
iss
에는, ID(및 「」)가 되어 있습니다.aud
client_id
oauth를 사용하는 )의 ★★★★★★★★★★★★★★★★★★★★★ 사이의 시간
iat
★★★★★★★★★★★★★★★★★」exp
개인 키를 사용하여 토큰 서명 확인
sub
유효한 .
다른 사람이 토큰을 훔쳐서 전화를 걸었을 경우, 어떻게 확인할 수 있습니까?
JWT의 소지는 인증의 증거입니다.토큰을 고정하는 공격자는 사용자를 가장할 수 있습니다.토큰을 안전하게 보관합니다.
TLS를 사용한 통신 채널 암호화
토큰에 보안 저장소를 사용합니다.웹 프런트 엔드를 사용하는 경우 로컬 스토리지/쿠키를 XSS 또는 CSRF 공격으로부터 보호하기 위한 추가 보안 조치를 고려할 수 있습니다.
인증 토큰의 만료 시간을 짧게 설정하고 토큰이 만료된 경우 자격 증명 필요
필터에서 로그인 요청을 우회하려면 어떻게 해야 합니까?인증 헤더가 없기 때문에
사용자 자격 증명을 검증하기 위해 로그인 양식에는 JWT 토큰이 필요하지 않습니다.폼은 필터의 범위에서 제외합니다.인증 성공 후 JWT를 발행하여 인증 필터를 나머지 서비스에 적용합니다.
다음으로 필터는 로그인 폼을 제외한 모든 요구를 대행 수신하고 다음 사항을 확인합니다.
사용자 인증을 받은 경우던지지 않으면
401-Unauthorized
사용자에게 요청된 리소스에 대한 권한이 있는지 확인합니다.던지지 않으면
403-Forbidden
접근이 허용됩니다.사용자 데이터를 요청 컨텍스트에 넣습니다(예: ThreadLocal 사용).
이 프로젝트는 매우 적절하게 구현되어 있으며 필요한 문서가 있습니다.
1. 위의 프로젝트에서는 토큰을 검증하는 것만으로 충분합니다.어디에token
의 값입니다.Bearer
를 입력해 주세요.
try {
final Claims claims = Jwts.parser().setSigningKey("secretkey")
.parseClaimsJws(token).getBody();
request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
2. 토큰을 훔치는 것은 쉽지 않지만, 제 경험상 로그인 성공 시마다 스프링 세션을 수동으로 생성함으로써 스스로를 보호할 수 있습니다.또한 세션 고유 ID와 베어러 값(토큰)을 맵에 매핑합니다(API 스코프를 사용한 Bean 작성 등).
@Component
public class SessionMapBean {
private Map<String, String> jwtSessionMap;
private Map<String, Boolean> sessionsForInvalidation;
public SessionMapBean() {
this.jwtSessionMap = new HashMap<String, String>();
this.sessionsForInvalidation = new HashMap<String, Boolean>();
}
public Map<String, String> getJwtSessionMap() {
return jwtSessionMap;
}
public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
this.jwtSessionMap = jwtSessionMap;
}
public Map<String, Boolean> getSessionsForInvalidation() {
return sessionsForInvalidation;
}
public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
this.sessionsForInvalidation = sessionsForInvalidation;
}
}
이것.SessionMapBean
는 모든 세션에서 사용할 수 있습니다.이제 모든 요구에서 토큰을 확인할 뿐만 아니라 세션이 계산되는지 여부도 확인합니다(요청 세션 ID 체크는 에 저장된 세션 ID와 일치합니다).SessionMapBean
세션 ID 도용도 가능하므로 통신을 보호해야 합니다.세션 ID를 도용하는 가장 일반적인 방법은 세션 스니핑(또는 중간에 있는 남자)과 크로스 사이트 스크립트 공격입니다.그런 공격으로부터 자신을 보호하는 방법을 읽을 수 있도록 자세한 내용은 언급하지 않겠습니다.
3. 제가 링크한 프로젝트에서 볼 수 있습니다.가장 간단하게 필터가 모두 유효성을 검사합니다./api/*
로그인 할 수 있습니다./user/login
예를들면.
언급URL : https://stackoverflow.com/questions/41975045/how-to-design-a-good-jwt-authentication-filter
'programing' 카테고리의 다른 글
Ajax 및 키 프레스를 사용하여 검색 최적화 (0) | 2023.03.14 |
---|---|
IO를 변환하는 가장 효율적인 방법읽기바이트 배열에 근접 (0) | 2023.03.14 |
POSTMAN에 객체가 존재하는지 확인하는 방법 (0) | 2023.03.14 |
$scope에서 컨트롤러 이름 가져오기 (0) | 2023.03.14 |
항목이 다른 테이블에 없는지 확인하는 중 (0) | 2023.03.14 |