5 Commits

Author SHA1 Message Date
Daeil Choi
7cab42f9b6 Modify h2console ignoring 방식 수정 2023-02-14 12:30:42 +09:00
Daeil Choi
35be1538f6 Modify login success alwaysUse false 2023-02-13 17:25:27 +09:00
Daeil Choi
e8d3336225 Add jjwt 라이브러리 테스트 2023-02-09 16:11:06 +09:00
Daeil Choi
eb7f9974a2 Modify 비밀키 환경변수로 숨김 처리 2023-02-08 12:20:14 +09:00
Daeil Choi
09f1e3d07f Add JWT 토큰 인증 방식으로 변경, JWT 커스텀 필터 추가 2023-02-07 17:18:41 +09:00
10 changed files with 399 additions and 12 deletions

View File

@@ -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'

View File

@@ -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()
);
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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";
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}
}