JWT의 Spring Security
다른 한편, REST API를 하나만 구축하면 외부 서비스나 SPA/모바일 애플리케이션에서 사용되므로 전체 세션이 필요하지 않을 수 있습니다.다음은 JWT(JSON Web 토큰) - 작은 디지털 서명 토큰입니다.필요한 모든 정보를 영패에 저장할 수 있어 서버가 세션을 줄일 수 있다.
JWT는 서버가 사용자에게 권한을 부여할 수 있도록 각 HTTP 요청에 연결해야 합니다.영패를 어떻게 보내는지에 관해서는 몇 가지 옵션이 있다.예를 들어, URL 매개 변수로 또는 호스팅 모드를 사용하는 HTTP 라이센스 헤더:
Authorization: Bearer <token string>
JSON 웹 토큰에는예제 토큰
라이센스 헤드의 JWT 토큰은 다음과 같습니다.
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA
보시다시피, 제목, 성명, 서명 세 부분을 쉼표로 구분합니다.Header 및 payload는 Base64 인코딩된 JSON 객체입니다.제목:
{
"typ": "JWT",
"alg": "HS512"
}
클레임/페이로드:
{
"iss": "secure-api",
"aud": "secure-app",
"sub": "user",
"exp": 1548242589,
"rol": [
"ROLE_USER"
]
}
예제 응용 프로그램
다음 예제에서는 두 개의 라우트를 포함하는 간단한 API를 만듭니다. 하나는 공개적이고 다른 하나는 승인된 사용자만 사용할 수 있습니다.
페이지start.spring.io를 사용하여 응용 프로그램 프레임워크를 만들고 보안과 웹 의존 항목을 선택할 것입니다.나머지 옵션은 취향에 따라 달라집니다.

JWT의 자바에 대한 지원은 라이브러리JJWT에서 제공하기 때문에pom에 다음과 같은 의존항을 추가해야 합니다.xml 파일:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.5</version>
<scope>runtime</scope>
</dependency>
컨트롤러
프로그램의 컨트롤러는 가능한 한 간단할 것입니다.사용자가 승인되지 않은 경우 메시지나 HTTP 403 오류 코드만 반환됩니다.
@RestController
@RequestMapping("/api/public")
public class PublicController {
@GetMapping
public String getMessage() {
return "Hello from public API controller";
}
}
@RestController
@RequestMapping("/api/private")
public class PrivateController {
@GetMapping
public String getMessage() {
return "Hello from private API controller";
}
}
필터
우선 JWT의 생성과 검증을 위해 다시 사용할 상수와 기본값을 정의합니다.
주의: 프로그램 코드에 JWT 서명 키를 하드코어로 인코딩해서는 안 됩니다. (이 예에서는 이 점을 무시합니다.)환경 변수 또는 를 사용해야 합니다.속성 파일.이 밖에 열쇠는 적당한 길이가 필요하다.예를 들어 HS512 알고리즘에는 최소 512바이트 크기의 키가 필요합니다.
public final class SecurityConstants {
public static final String AUTH_LOGIN_URL = "/api/authenticate";
// Signing key for HS512 algorithm
// You can use the page http://www.allkeysgenerator.com/ to generate all kinds of keys
public static final String JWT_SECRET = "n2r5u8x/A%D*G-KaPdSgVkYp3s6v9y$B&E(H+MbQeThWmZq4t7w!z%C*F-J@NcRf";
// JWT token defaults
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String TOKEN_TYPE = "JWT";
public static final String TOKEN_ISSUER = "secure-api";
public static final String TOKEN_AUDIENCE = "secure-app";
private SecurityConstants() {
throw new IllegalStateException("Cannot create instance of static util class");
}
}
첫 번째 필터는 사용자 인증에 직접 사용됩니다.URL에서 사용자 이름과 암호 매개 변수를 확인하고 Spring 인증 관리자를 호출하여 인증합니다.사용자 이름과 암호가 올바르면 필터가 JWT 토큰을 만들고 HTTP 라이센스 헤더에서 반환합니다.
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
var username = request.getParameter("username");
var password = request.getParameter("password");
var authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain, Authentication authentication) {
var user = ((User) authentication.getPrincipal());
var roles = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
var signingKey = SecurityConstants.JWT_SECRET.getBytes();
var token = Jwts.builder()
.signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512)
.setHeaderParam("typ", SecurityConstants.TOKEN_TYPE)
.setIssuer(SecurityConstants.TOKEN_ISSUER)
.setAudience(SecurityConstants.TOKEN_AUDIENCE)
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 864000000))
.claim("rol", roles)
.compact();
response.addHeader(SecurityConstants.TOKEN_HEADER, SecurityConstants.TOKEN_PREFIX + token);
}
}
두 번째 필터는 모든 HTTP 요청을 처리하고 올바른 토큰이 있는 헤더가 있는지 확인합니다.예를 들어 영패가 만료되지 않았거나 서명 키가 정확하면토큰이 유효하면 필터는 Spring의 보안 컨텍스트에 인증 데이터를 추가합니다.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthorizationFilter.class);
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
var authentication = getAuthentication(request);
if (authentication == null) {
filterChain.doFilter(request, response);
return;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
var token = request.getHeader(SecurityConstants.TOKEN_HEADER);
if (StringUtils.isNotEmpty(token) && token.startsWith(SecurityConstants.TOKEN_PREFIX)) {
try {
var signingKey = SecurityConstants.JWT_SECRET.getBytes();
var parsedToken = Jwts.parser()
.setSigningKey(signingKey)
.parseClaimsJws(token.replace("Bearer ", ""));
var username = parsedToken
.getBody()
.getSubject();
var authorities = ((List<?>) parsedToken.getBody()
.get("rol")).stream()
.map(authority -> new SimpleGrantedAuthority((String) authority))
.collect(Collectors.toList());
if (StringUtils.isNotEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
} catch (ExpiredJwtException exception) {
log.warn("Request to parse expired JWT : {} failed : {}", token, exception.getMessage());
} catch (UnsupportedJwtException exception) {
log.warn("Request to parse unsupported JWT : {} failed : {}", token, exception.getMessage());
} catch (MalformedJwtException exception) {
log.warn("Request to parse invalid JWT : {} failed : {}", token, exception.getMessage());
} catch (SignatureException exception) {
log.warn("Request to parse JWT with invalid signature : {} failed : {}", token, exception.getMessage());
} catch (IllegalArgumentException exception) {
log.warn("Request to parse empty or null JWT : {} failed : {}", token, exception.getMessage());
}
}
return null;
}
}
보안 구성
구성의 마지막 부분은 Spring 보안 자체입니다.구성은 간단합니다. 세부사항만 설정하면 됩니다.
CORS 구성
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("password"))
.authorities("ROLE_USER");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
테스트
API 공개 요청
GET http://localhost:8080/api/public
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 32
Date: Sun, 13 Jan 2019 12:22:14 GMT
Hello from public API controller
Response code: 200; Time: 18ms; Content length: 32 bytes
사용자 인증
POST http://localhost:8080/api/authenticate?username=user&password=password
HTTP/1.1 200
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDYwNzUsInJvbCI6WyJST0xFX1VTRVIiXX0.yhskhWyi-PgIluYY21rL0saAG92TfTVVVgVT1afWd_NnmOMg__2kK5lcna3lXzYI4-0qi9uGpI6Ul33-b9KTnA
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Sun, 13 Jan 2019 12:21:15 GMT
<Response body is empty>
Response code: 200; Time: 167ms; Content length: 0 bytes
토큰을 사용하여 개인 API 요청
GET http://localhost:8080/api/private
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Sun, 13 Jan 2019 12:22:48 GMT
Hello from private API controller
Response code: 200; Time: 12ms; Content length: 33 bytes
토큰이 없는 개인 API 요청
유효한 JWT 없이 보안 엔드포인트를 호출하면 HTTP 403 메시지가 표시됩니다.
GET http://localhost:8080/api/private
HTTP/1.1 403
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 13 Jan 2019 12:27:25 GMT
{
"timestamp": "2019-01-13T12:27:25.020+0000",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/private"
}
Response code: 403; Time: 28ms; Content length: 125 bytes
결론
이 문서의 목적은 Spring Security에서 JWTs를 사용하는 정확한 방법을 보여주는 것이 아닙니다.이것은 어떻게 실제 응용 프로그램에서 실현되는지의 예이다.또한 영패 갱신, 실효 등 내용이 매우 적기 때문에 나는 이 화제를 깊이 토론하고 싶지 않다. 그러나 나는 장래에 이런 화제를 토론할 것이다.
tl;dr 이 예시 API의 전체 소스 코드를 my GitHub repository 에서 찾을 수 있습니다.
Reference
이 문제에 관하여(JWT의 Spring Security), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/kubadlo/spring-security-with-jwt-3j76텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)