Compare commits
5 Commits
custom-fil
...
jwt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cab42f9b6 | ||
|
|
35be1538f6 | ||
|
|
e8d3336225 | ||
|
|
eb7f9974a2 | ||
|
|
09f1e3d07f |
@@ -29,6 +29,10 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
// Thymeleaf에서 SpringSecurity를 Integration
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
|
||||
// jjwt
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
|
||||
// lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.example.springsecuritystudy.config;
|
||||
|
||||
import com.example.springsecuritystudy.filter.StopwatchFilter;
|
||||
import com.example.springsecuritystudy.filter.TesterAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -11,11 +8,22 @@ import org.springframework.security.config.annotation.authentication.configurati
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import com.example.springsecuritystudy.filter.StopwatchFilter;
|
||||
import com.example.springsecuritystudy.jwt.JwtAuthenticationFilter;
|
||||
import com.example.springsecuritystudy.jwt.JwtAuthorizationFilter;
|
||||
import com.example.springsecuritystudy.jwt.JwtProperties;
|
||||
import com.example.springsecuritystudy.jwt.JwtUtils;
|
||||
import com.example.springsecuritystudy.user.UserRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Security 설정 Config
|
||||
*/
|
||||
@@ -23,6 +31,9 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
@@ -36,16 +47,21 @@ public class SecurityConfig {
|
||||
new StopwatchFilter(),
|
||||
WebAsyncManagerIntegrationFilter.class
|
||||
);
|
||||
// tester authentication filter
|
||||
// JWT filter
|
||||
http.addFilterBefore(
|
||||
new TesterAuthenticationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))),
|
||||
new JwtAuthenticationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))
|
||||
, jwtUtils),
|
||||
UsernamePasswordAuthenticationFilter.class
|
||||
).addFilterBefore(
|
||||
new JwtAuthorizationFilter(userRepository, jwtUtils),
|
||||
BasicAuthenticationFilter.class
|
||||
);
|
||||
http
|
||||
.httpBasic().disable()
|
||||
.csrf();
|
||||
.csrf().disable();
|
||||
http
|
||||
.rememberMe();
|
||||
.rememberMe().disable()
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.antMatchers("/", "/home", "/signup").permitAll()
|
||||
@@ -57,14 +73,14 @@ public class SecurityConfig {
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.defaultSuccessUrl("/")
|
||||
.defaultSuccessUrl("/", false)
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
// .logoutUrl("/logout") // post 방식으로만 동작
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // get 방식으로도 동작
|
||||
.logoutSuccessUrl("/")
|
||||
.deleteCookies("JSESSIONID")
|
||||
.deleteCookies(JwtProperties.COOKIE_NAME)
|
||||
.invalidateHttpSession(true)
|
||||
);
|
||||
|
||||
@@ -75,9 +91,10 @@ public class SecurityConfig {
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
// 정적 리소스 spring security 대상에서 제외
|
||||
return (web) -> web.ignoring()
|
||||
.antMatchers("/h2-console/**")
|
||||
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
|
||||
;
|
||||
.requestMatchers(
|
||||
PathRequest.toStaticResources().atCommonLocations(),
|
||||
PathRequest.toH2Console()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import com.example.springsecuritystudy.user.User;
|
||||
|
||||
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
|
||||
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
public JwtAuthenticationFilter(
|
||||
AuthenticationManager authenticationManager, JwtUtils jwtUtils) {
|
||||
super(authenticationManager);
|
||||
this.jwtUtils = jwtUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 인증 시도
|
||||
*/
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws
|
||||
AuthenticationException {
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
request.getParameter("username"),
|
||||
request.getParameter("password"),
|
||||
new ArrayList<>()
|
||||
);
|
||||
return getAuthenticationManager().authenticate(authenticationToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증에 성공했을 때 사용
|
||||
* JWT Token을 생성해서 쿠키에 넣는다.
|
||||
*/
|
||||
@Override
|
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
||||
Authentication authResult) throws IOException, ServletException {
|
||||
User user = (User)authResult.getPrincipal();
|
||||
String token = jwtUtils.createToken(user);
|
||||
// 쿠키 생성
|
||||
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, token);
|
||||
cookie.setMaxAge(JwtProperties.EXPIRATION_TIME); // 쿠키 만료 시간
|
||||
cookie.setPath("/");
|
||||
response.addCookie(cookie);
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException failed) throws IOException, ServletException {
|
||||
response.sendRedirect("/login");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import com.example.springsecuritystudy.common.UserNotFoundException;
|
||||
import com.example.springsecuritystudy.user.User;
|
||||
import com.example.springsecuritystudy.user.UserRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT를 이용한 인증
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthorizationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = null;
|
||||
try {
|
||||
// cookie에서 JWT token을 가져온다.
|
||||
token = Arrays.stream(request.getCookies())
|
||||
.filter(cookie -> cookie.getName().equals(JwtProperties.COOKIE_NAME)).findFirst()
|
||||
.map(Cookie::getValue)
|
||||
.orElse(null);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (token != null) {
|
||||
try {
|
||||
Authentication authentication = getUsernamePasswordAuthenticationToken(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (Exception e) {
|
||||
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, null);
|
||||
cookie.setMaxAge(0);
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private Authentication getUsernamePasswordAuthenticationToken(String token) {
|
||||
String username = jwtUtils.getUsername(token);
|
||||
if (username != null) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow(UserNotFoundException::new);
|
||||
return new UsernamePasswordAuthenticationToken(
|
||||
user,
|
||||
null,
|
||||
user.getAuthorities()
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import javafx.util.Pair;
|
||||
|
||||
/**
|
||||
* JWT Key를 제공하고 조회한다.
|
||||
* Key Rolling을 지원한다.
|
||||
*/
|
||||
@Component
|
||||
public class JwtKey {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public JwtKey(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
private Map<String, String> SECRET_KEY_SET;
|
||||
private String[] KID_SET;
|
||||
private Random randomIndex;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
SECRET_KEY_SET = new HashMap<String, String>() {
|
||||
{
|
||||
put("key1", env.getProperty("jwt.secret-key1"));
|
||||
put("key2", env.getProperty("jwt.secret-key2"));
|
||||
put("key3", env.getProperty("jwt.secret-key3"));
|
||||
}
|
||||
};
|
||||
KID_SET = SECRET_KEY_SET.keySet().toArray(new String[0]);
|
||||
randomIndex = new Random();
|
||||
}
|
||||
|
||||
/**
|
||||
* SECRET_KEY_SET 에서 랜덤한 KEY 가져오기
|
||||
*
|
||||
* @return kid와 key Pair
|
||||
*/
|
||||
public Pair<String, Key> getRandomKey() {
|
||||
String kid = KID_SET[randomIndex.nextInt(KID_SET.length)];
|
||||
String secretKey = SECRET_KEY_SET.get(kid);
|
||||
return new Pair<>(kid, Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
/**
|
||||
* kid로 Key찾기
|
||||
*
|
||||
* @param kid kid
|
||||
* @return Key
|
||||
*/
|
||||
public Key getKey(String kid) {
|
||||
String key = SECRET_KEY_SET.getOrDefault(kid, null);
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
/**
|
||||
* JWT 기본 설정값
|
||||
*/
|
||||
public class JwtProperties {
|
||||
public static final int EXPIRATION_TIME = 600000; // 10분
|
||||
public static final String COOKIE_NAME = "JWT";
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.example.springsecuritystudy.user.User;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import javafx.util.Pair;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtUtils {
|
||||
private final JwtKey jwtKey;
|
||||
|
||||
/**
|
||||
* 토큰에서 username 찾기
|
||||
*
|
||||
* @param token 토큰
|
||||
* @return username
|
||||
*/
|
||||
public String getUsername(String token) {
|
||||
// jwtToken에서 username을 찾는다.
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKeyResolver(new SigningKeyResolver(jwtKey))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody()
|
||||
.getSubject(); // 토큰에 담긴 정보에서 username을 가져온다.
|
||||
}
|
||||
|
||||
/**
|
||||
* user로 토큰 생성
|
||||
* HEADER : alg, kid
|
||||
* PAYLOAD : sub, iat, exp
|
||||
* SIGNATURE : JwtKey.getRandomKey로 구한 Secret Key로 HS512 해시
|
||||
*
|
||||
* @param user 유저
|
||||
* @return jwt token
|
||||
*/
|
||||
public String createToken(User user) {
|
||||
Claims claims = Jwts.claims().setSubject(user.getUsername());
|
||||
Date now = new Date();
|
||||
Pair<String, Key> key = jwtKey.getRandomKey();
|
||||
return Jwts.builder()
|
||||
.setClaims(claims) // 토큰에 담을 정보 설정
|
||||
.setIssuedAt(now) // 토큰 발행 시간 설정
|
||||
.setExpiration(new Date(now.getTime() + JwtProperties.EXPIRATION_TIME)) // 토큰 만료 시간 설정
|
||||
.setHeaderParam(JwsHeader.KEY_ID, key.getKey()) // 토큰에 kid 설정
|
||||
.signWith(key.getValue()) // signature 생성
|
||||
.compact();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.SigningKeyResolverAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* JwsHeader를 통해 Signature 검증에 필요한 Key를 가져오는 코드를 구현합니다.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class SigningKeyResolver extends SigningKeyResolverAdapter {
|
||||
private final JwtKey jwtKey;
|
||||
|
||||
@Override
|
||||
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
|
||||
String kid = jwsHeader.getKeyId();
|
||||
if (kid == null)
|
||||
return null;
|
||||
return jwtKey.getKey(kid);
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,10 @@ spring:
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
show-sql: true
|
||||
|
||||
jwt:
|
||||
secret-key1: SpringSecurityJWTPracticeProjectIsSoGoodAndThisProjectIsSoFunSpringSecurityJWTPracticeProjectIsSoGoodAndThisProjectIsSoFun
|
||||
secret-key2: GoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurity
|
||||
secret-key3: HelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurity
|
||||
expiration-time: 86400 #60 * 60 * 24
|
||||
remember-me-expiration-time: 2592000 #60 * 60 * 24 * 30
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
|
||||
class JwtUtilsTest {
|
||||
|
||||
String secretKey = "SecretKeyToGenJWTsSecretKeyToGenJWTsSecretKeyToGenJWTs";
|
||||
|
||||
private void printToken(String token) {
|
||||
System.out.println("token: " + token);
|
||||
System.out.println("header: " + decodeToken(token.split("\\.")[0]));
|
||||
System.out.println("payload: " + decodeToken(token.split("\\.")[1]));
|
||||
}
|
||||
|
||||
private String decodeToken(String token) {
|
||||
return new String(Base64Utils.decodeFromString(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_okta_token() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("sub", "test");
|
||||
claims.put("name", "dalichoi");
|
||||
claims.put("admin", "true");
|
||||
claims.put("exp", Instant.now().plusSeconds(60*60*24).getEpochSecond());
|
||||
claims.put("iat", Instant.now().getEpochSecond());
|
||||
|
||||
String oktaToken = Jwts.builder()
|
||||
// .setHeaderParam("typ", "JWT")
|
||||
// .setHeaderParam("alg", "HS256")
|
||||
// .setHeaderParam("kid", "key1")
|
||||
// .setSubject("test")
|
||||
// .setIssuedAt(java.util.Date.from(Instant.now()))
|
||||
// .setExpiration(java.util.Date.from(Instant.now().plusSeconds(60*60*24)))
|
||||
.addClaims(claims)
|
||||
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
printToken(oktaToken);
|
||||
|
||||
Jws<Claims> claimsJws = Jwts.parserBuilder()
|
||||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
|
||||
.build()
|
||||
.parseClaimsJws(oktaToken);
|
||||
System.out.println("claimsJws: " + claimsJws);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user