스프링 Security - Jwt Authentication 구현 흐름
- User Sign-Up:
- 사용자는 사용자 이름(username), 비밀번호(password) 등의 정보를 제공.
- 비밀번호는 안전하게 해시(예: BCrypt) 처리된 후 데이터베이스에 저장.
- 사용자 정보는 UserDetails 객체(또는 엔티티) 형태로 데이터베이스에 저장.
- User Sign-In (Authentication):
- 사용자가 로그인 요청(예: /login)을 보내면 UsernamePasswordAuthenticationFilter가 요청을 가로챕니다. 그리고 필터는추출된 username과 password를 기반으로 인증 전 상태의 UsernamePasswordAuthenticationToken 객체 (Authentication 객체)를 생성합니다.
- DaoAuthenticationProvider는 사용자가 입력한 자격 증명(사용자 이름, 비밀번호)이 데이터베이스의 저장된 사용자 정보와 비교. 인증이 성공하면, 사용자 정보(Principal)와 권한(Authorities) 등을 포함하는 인증된 객체로 변환되어 반환.
- JWT 생성은 인증이 성공한 후, Authentication 객체에서 사용자 정보를 추출하여 이루어집니다.서버는 커스텀 JWT 생성클래스를 통해 JWT 토큰을 생성하여 클라이언트에 반환. JWT에는 사용자 ID, 권한 등의 정보를 포함하며, 서명을 통해 무결성을 보장.
- JWT Token Interception & Validation:
- 권한설정이 부여된 API에 요청이 발송되었을때, Spring Security의 필터 체인이 요청을 가로챔.
- SecurityContextPersistenceFilter: 요청의 SecurityContext에 인증 객체가 있는지 확인. 만약 인증 객체가 없다면, JWT토큰을 추출하는 필터로 (e.g OncePerRequestFilter) 요청을 넘깁니다
- JWT 처리 필터 (예: OncePerRequestFilter): HTTP 요청 헤더에서 JWT를 추출. JWT의 서명(Signature) 및 만료 시간 검증. JWT가 유효하면 사용자 정보를 바탕으로 Authentication 객체 생성. 생성된 Authentication 객체를 SecurityContext에 저장.
- Token Expiry & Refresh:
- JWT는 만료 시간이 설정될 수 있으며, 만료된 토큰은 사용할 수 없음.
- 토큰이 만료되면 클라이언트는 Refresh Token을 사용해 새 Access Token을 발급받거나 재인증이 필요.
Security 구현하기 : Dependency
Jjwt, SpringSecurity 의존성 추가하기
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
스프링 SecuirtyConfiguration
Filter를 어떻게 적용할지에 대해서 세부조정을 하는 클래스. 우선, Custom구현한 JWT Authentification Filter를 Chain of Filter 내에서 몇번째 순서에 배치하는지 확정하고 더불어 해당 Filter를 발동시킬 Url조건과 예외처리를 할 Url 등을 지정한다. Signup page등은 Filter 발동에서 제외가 필요하다.
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
/*
* Jwt Statless 인증을 적용함.
* Url base로 권한을 제한함 ; auth : signin - signup 관련 url
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 로그인, 로그아웃
.authorizeHttpRequests(auth -> auth.requestMatchers("/css/**"
,"/js/**", "/images","/h2-console/**", "/fonts/**",
"/images/**", "/favicon.ico").permitAll())
.authorizeHttpRequests(auth -> auth.requestMatchers(
"/auth/**").permitAll())
// url 기반으로 partner 와 user 권한부여
.authorizeHttpRequests(auth->auth.requestMatchers(
"/*/partner/**").hasRole("PARTNER"))
.authorizeHttpRequests(auth->auth.requestMatchers(
"/*/user/**").hasRole("USER"))
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()))
.addFilterBefore(jwtAuthFilter,
UsernamePasswordAuthenticationFilter.class);
log.info("Security filter chain is being configured");
return http.build();
}
}
- @EnableWebSecurity : Spring Security의 자동 설정을 비활성화하고, 애플리케이션 보안 구성을 개발자가 직접 제어할 수 있도록 합니다. 보안 규칙, 인증 방식 등을 커스터마이징할 수 있습니다.
- SecurityFilterChain Bean : Spring의 의존성 주입(Dependency Injection)과 통합하기 위한 Bean으로 등록. SecurityFilterChain은 FilterChainProxy라는 스프링 내부코드 객체에서 사용한다. default 설정이 아닌, 개발자 설정을 원할때 자체적으로 빈으로 등록하여 커스터마이징하는 것.
- HttpSecurity : 보안 필터 체인의 동작 방식을 설정하는 객체. URL 기반 보안, 세션 관리, CSRF 설정 등 다양한 보안 규칙을 정의. 설정이 완료되면 HttpSecurity 객체를 사용해서 SecurityFilterChain을 생성.
- .csrf(csrf -> csrf.disable()) : Spring Security가 제공하는 기본 CSRF 방지 필터를 비활성화하는 것입니다. JWT 기반 인증처럼 Stateless 애플리케이션에서는 종종 비활성화됨. Configures the CSRF Filter
- sessionManagement (session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) : 세션 관리 정책을 Stateless로 설정. Spring Security가 세션을 생성하거나 사용하지 않도록 설정. JWT 기반 인증과 같은 Stateless 애플리케이션에 적합. Configures the SecurityContextPersistenceFilter.
- 세션 Policy : 세션을 생성하는 동작은 클라이언트와 서버 간의 상태를 유지하기 위해 서버가 데이터를 저장하고 이를 참조할 수 있는 고유 식별자(세션 ID)를 생성하는 과정입니다. 이를 통해 서버는 클라이언트를 식별하고, 이전 요청의 상태를 유지하거나 사용자 인증 정보를 저장합니다.
- authorizeHttpRequests: URL 기반으로 보안 규칙을 설정합니다. 요청 URL에 따라 접근 권한(인증 필요 여부)을 정의할 수 있습니다. Configures the FilterSecurityInterceptor.
- .requestMatchers("/**/signup", "/**/signin") : 특정 HTTP 요청 경로와 매칭되는 요청에 대한 규칙을 설정합니다.
- .permitAll(): 필터를 통과하고나서도 authenticated되지 않은 request도 통과할 수 있도록 허락함.
- * 는 wildcard이지만 a single path segment를 대신한다.
- ** 는 wildcard로써 뒤나 앞에 오는 모든 path segment을 대신한다. 단, **는 url내에서 한번만 쓰일 수 있다.
- .anyRequest() : 이전에 정의되지 않은 모든 HTTP 요청에 대한 규칙을 설정합니다.
- .authenticated(): 인증된 사용자만 접근할 수 있도록 설정합니다.
- .requestMatchers("/**/signup", "/**/signin") : 특정 HTTP 요청 경로와 매칭되는 요청에 대한 규칙을 설정합니다.
- addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class): UsernamePasswordAuthenticationFilter는 스프링 security에서 기본적으로 지원하는 로그인기능으로 userdetail의 정보와 대조를 통해서 이루어진다. 해당 필터보다 Custom필터를 앞에두어 SecurityContext를 채우면 보안필터는 스킵할 수 있어서 인증방식을 지정하는 역하을 하는 코드라고 할 수 있다.
Url Base Authority Control vs Method Base Authority Control
Url base의 경우 조금더 broad한 관점에서 제어를 할때 유리하다. 예를들어 특정 url주소는 user만 사용할 수 있다거나, 특정 url주소는 partner만 사용할 수 있다거나 할때 1차적으로 제어를 걸수 있다. 반면 Method base는 좀더 specific한 관점에서 제어할때 필수적이다. 특정 Id의 회원정보 수정에 접근할때 요청자의 Id가 특정 Id인 경우에만 접근가능하다든지 등의 접근 제어에 적합하다.
Method Base Authority Control
PreAuthorize and FilterSecurityInterceptor: 메서드 수준 보안에서 @PreAuthorize, @Secured와 같은 애노테이션은 현재 사용자가 필요한 권한을 가지고 있는지 **SecurityContext**를 확인하여 보안 검사를 수행합니다. 이러한 보안 검사는 **FilterSecurityInterceptor**에서 처리됩니다.
- SecurityConfig annotation : @EnableMethodSecurity(prePostEnabled = true)
- @EnableMethodSecurity : SpringSecurityConfig 클래스에 부착하여 메서드 기반 보안을 활성화합니다. 메서드 단위의 보안 제어를 적용할 수 있게 해줍니다.
- prePostEnabled (Default: false) : @PreAuthorize와 @PostAuthorize 애노테이션의 사용을 활성화합니다.
- Method annotation
- @PreAuthorize : 메서드 실행 전에 조건(주로 사용자 역할 또는 권한 기반)을 확인하여 보안 검사를 수행합니다.
- @PostAuthorize : 메서드 실행 후 반환 값을 기반으로 조건을 확인하고 보안 검사를 수행합니다. 반환값을 기반으로 보안 제약을 검증해야 할 때 유용합니다.
@PreAuthorize("#partnerId == authentication.principal.memberId")
@PreAuthorize( "hasRole('PARTNER')" )
#partnerId:
Refers to the method parameter named partnerId.
authentication:
SpEL 표현식에서 타입을 지정하지 않고도 바로 접근 가능하도록 Spring Security가 내부적으로 처리합니다. authentication은 현재 스레드의 SecurityContext에 저장된 Authentication 객체를 나타냅니다.
Security 구현하기 : JWT Authentication Filter
JWT Authentication을 구현할때 주로 사용되는 필터는 두가지로 아래와 같다. 우선 SecurityContextPersistenceFilter는 Thread가 SecurityContext를 보유하고 있는지 확인하고 미보유시 JwtAuthentificationFilter로 request를 넘긴다. JwtAuthentificationFilter는 해당 request가 1. jwt토큰을 보유하고 있는지 2. 토큰이 유효한지 두가지를 확인하여 맞다면 SecurityContext에 Authentication객체를 삽입한다.
스프링 Security에서 필터를 구현할때, OncePerRequestFilter 주로 사용하는데, 해당 필터는 request 1개당 1회만 실행되어 Security 구현에 적합하다
// token을 포함되어있는지 파싱하여 확인하는 작업을 구현
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public static final String TOKEN_HEADER = "Authorization"; // Http header
public static final String TOKEN_PREFIX = "Bearer "; // jwt Token에 처음붙음
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = resolveTokenFromRequest(request);
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getJwtAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String resolveTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(TOKEN_HEADER);
if(!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) {
return token.substring(TOKEN_PREFIX.length());
}
return null;
}
}
Security 구현하기 : UserDetails 객체
- Credential(ID,PW)를 보유하고 있는 UserDetail을 구현한다. UserDetail은 일반적으로 UserEntity를 Input으로 받아서 생성됨.
UserDetail이 구현되어야 Authentication 과정에서 User의 Credential을 검증하고 후에 Authority를 검증하는 과정이 가능함.
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled;
@Column(nullable = false)
private boolean accountNonExpired;
@Column(nullable = false)
private boolean credentialsNonExpired;
@Column(nullable = false)
private boolean accountNonLocked;
- UserDetails 인터페이스는 isexpired, islocked 등 여러가지 필드값과 Authority객체인 GrantedAuthority 인터페이스를 값으로 갖게한다. GrantedAuthority 인터페이스는 roles String을 -> Authority 구현체들("Role_"read/write... 형식)으로 변환되게끔하여 Spring 기능 (method annotation @hasRole() 등)에 활용될 수 있도록 함. (SimpleGrantedAuthoritie는 roles -> GrantedAuthority의 가장 기본적인 형태임)
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDetails implements UserDetails {
private Long id;
private String username;
private String password;
private List<String> roles;
public static MemberDetails fromEntity(MemberEntity memberEntity) {
return MemberDetails.builder()
.id(memberEntity.getId())
.username(memberEntity.getUsername())
.password(memberEntity.getPassword())
.roles(memberEntity.getRoles())
.build();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
- Authority Enum으로 저장
public enum Authority {
ROLE_READ,
ROLE_WRITE;
}
Security 구현하기 : UserDetailsService
UserDetail이 구현이 완료되었다면, Persistency계층과 Userdetail을 연결시키는 UserDetailService를 구현한다. UserDetailService는 1. UserDetail 저장(Signin) 2. UserDetail 대조하여 호출 3. UserDetail 호출
/*
유저 디테일을 불러오고 저장하는 클래스
*/
public class MemberAuthService implements UserDetailsService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
/*
유저로부터 Id와 Pw를 받아서 Id중복여부를 확인한 후
pw를 encoding하여 db에 저장함
*/
@Transactional
public void register(String memberId, String password){
if(memberRepository.existsByMemberId(memberId)){
throw new UsernameNotFoundException("Member already exists");
}
memberRepository.save(
MemberDetails.builder()
.memberId(memberId)
.password(passwordEncoder.encode(password))
.build());
}
public UserDetails authenticate(String memberId, String password ){
UserDetails userDetails = this.loadUserByUsername(memberId);
if(!passwordEncoder.matches(password, userDetails.getPassword())){
throw new BadCredentialsException("Wrong password");
}
return userDetails;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return memberRepository.findByMemberId(username).orElseThrow(() -> new UsernameNotFoundException("Member does not exist"));
}
}
- Appconfig에서 PasswordEncoder Bean을 등록한다. 이때, Spring에서 제공하는 BcrptPasswordEncoder을 사용하여 주입한다.
@Configuration
public class AppConfig {
@Bean
public Trie<String,String> tire(){
return new PatriciaTrie<>();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
passwordEncoder.encode(password)
passwordEncoder.matches(password, memberEntity.getPassword())
- The matches method takes the raw password and the encoded password (the one stored in the database).
- It hashes the raw password using the same algorithm and compares it to the stored hash.
Security 구현하기 : Sign Controller
- Sign-up에는 아이디 중복여부를 체크하고 Persistency에 저장
- Sign-in에는 DaoAuthenticate 후에 JWT를 return
public class AuthController {
private final MemberAuthService memberAuthService;
private final JwtHandler jwtHandler;
private final UserService userService;
private final PartnerService partnerService;
private final PasswordEncoder passwordEncoder;
/*
memberEntity 와 partnerEntity 를 최초 생성함.
단, memberEntity 는 관련기능은 후속추가되어 클래스 추가관리되었고 @Transactional 관리를 위해
controller 에서 서비스 호출하지 않고 partnerService 내에서 관련로직 호출함
*/
@PostMapping("/partners")
public ResponseEntity<CreatePartner.Response> createPartner(
@RequestBody @Valid CreatePartner.Request request) {
log.info("Creating partner : {}", request);
PartnerDto partnerDto = partnerService.createPartner
( request.getPartnerId(),
request.getPassword(),
request.getPartnerName(),
request.getBusinessId(),
request.getPhoneNumber()
);
return ResponseEntity.ok().body(
CreatePartner.Response.fromDto(partnerDto));
}
/*
memberEntity 와 userEntity 최초 생성함.
단, memberEntity 는 관련기능은 후속추가되어 클래스 추가관리되었고 @Transactional 관리를 위해
controller 에서 서비스 호출하지 않고 userService 내에서 관련로직 호출함
*/
@PostMapping("/users")
public ResponseEntity<CreateUser.Response> createUser(
@RequestBody @Valid CreateUser.Request request) {
log.info("Creating user request received : {}", request.getUserId());
UserDto userDto = userService.createUser(
request.getUserId(),
request.getPassword(),
request.getUserName(),
request.getPhoneNumber()
);
return ResponseEntity.ok().body(
CreateUser.Response.fromDto(userDto));
}
/*
Dao Authentication 을 행하고 결과값으로 JWT token 을 받음
*/
@PostMapping("/signIn")
public ResponseEntity<String> signIn(@RequestBody @Validated SignAuth.SignIn signIn) {
UserDetails userDetails = memberAuthService.authenticate(signIn.getId(), signIn.getPassword());
String token = jwtHandler.generateToken(userDetails.getUsername(), userDetails.getAuthorities());
return ResponseEntity.ok(token);
}
- Request 객체만들기
public class Auth {
@Data
public static class SignInRequest {
private String userName;
private String password;
}
@Data
public static class SignUpRequest {
private String userName;
private String password;
private List<String> roles;
}
}
Security 구현하기 : Jwts dependency 알아보기
JWT(Json Web Token)
사용자 인증 및 식별에 사용되는 JSON 기반의 토큰입니다. JWT는 Header, Payload,Signature 3부분으로 나누어진다. JWT는 서버에서 세션을 관리하지 않고, 클라이언트 측에 토큰을 전달하여 사용자를 인증하는 방식으로 작동합니다.
실무적으로 DAO Authentication을 통해서 인증이 완료되면 Dao로부터 꺼낸 Authority, Principal, Credential에 대한 정보를 JWT에 담아서 인증,권한 확인 등에 사용한다.
JWT encoding 방식
JWT는 Base64 Encoded된 것으로 엄밀히 말하면 암호화되지 않고 정보를 담은 것입니다. 암호화된 부분은 Signature부분으로 Header과 Payload의 값이 변경되면 Signature이 변경되어 컨텐츠의 유효성을 확인할 수 있다.
base64 : Base64 encoding takes three bytes (3*8 bits) of binary data and splits them into (4*6bits) groups. Each of these 6-bit groups is then represented by a single ASCII character.
JWT 구조
{
"alg":"HS512",
"typ":"JWT"
}
- Header : 1. 토큰의 타입(JWT) 2. 어떤 암호화 알고리즘이 적용되었는지, 두가지 정보가 저장
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"roles": ["ROLE_USER", "ROLE_ADMIN"]
}
- Payload :
- 1. 등록된 클레임(Registered Claims): 토큰의 만료 시간(exp), 발급자(iss), 수신자(aud), 발급 시간(iat) 등이 포함
- 2. 공개 클레임(Public Claims): 사용자 정의 클레임으로, 사용자 ID, 이메일, 사용자 이름 등의 정보를 담을 수 있습니다.
- 3. 비공개 클레임(Private Claims): 서버와 클라이언트 간에 동의된 비공개 데이터입니다.
- 서명 : JWT의 무결성을 보장하기 위해 사용됩니다. 서버는 헤더 + 페이로드+ secretkey + al를 조합하여, 지정된 암호화 알고리즘을 사용해 서명을 생성합니다.
Jwts코드 예시
- Jwts 코드예시 : Jwt token 생성을 위해서 Header, Payload, Sign 방식을 설정한다.
// Secret key in string format
String secretKeyString = "mySecretKey"; // This should be securely stored
// Convert the secret key string to a SecretKey object
SecretKey secretKey = new SecretKeySpec(secretKeyString.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
// Build and sign the JWT
String jwt = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject("user123")
.setIssuer("my-app")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour expiration
.claim("role", "admin") // Custom claim
.signWith(secretKey, Signature
대표 매소드 정리
Jwts.builder():
- setHeaderParam(String name, Object value) : Sets a single custom header parameter.
- setSubject(String subject) : Sets the subject (sub) claim, typically used to represent the user or entity the token is issued for.
- setClaims(Map<String, Object> claims) : Sets multiple claims(predefined) in the JWT. Claims are the payload data that include information like the subject, issuer, expiration time, etc.
- it replaces the entire set of claims that have been accumulated so far, including any claims you may have set earlier in the builder chain, such as setSubject, setExpiration, or setIssuedAt.
- claim(String name, Object value) : Sets a single claim(usualy customized) in the JWT.
- setIssuer(String issuer) : Sets the issuer (iss) claim, which identifies the principal that issued the JWT.
- setIssuedAt(Date issuedAt) : Sets the issued-at (iat) claim, which indicates the time at which the JWT was issued.
- setExpiration(Date expiration) : Sets the expiration (exp) claim, which indicates the time after which the JWT should no longer be accepted.
- signWith(Key key, SignatureAlgorithm alg) : to digitally sign a JWT using a specified cryptographic key and signature algorithm.
- compact() : Finalizes the construction of the JWT and returns it as a String. This method must be called at the end of the JWT building process
// Convert the secret key string to a SecretKey object
SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());
// Recommended method:
String jwt = Jwts.builder()
.setSubject("user123")
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
SecetKey
이전에는 .signWith(SignatureAlgorithm.HS512, String secretkey)와 같은 방식으로 secretkey가 String으로 입력되었다면 이제는 client 측에서 string을 byte[]로 변환한 후 Secretkey라는 객체를 별도로 만들고 signwith에 그 객체를 input 하도록 되어있다. 이 변화의 트렌드는 moving away from "magic" or automatic behavior that might lead to mistakes and instead encouraging explicit, well-understood actions that align with security best practices 라고 한다.
new SecretKeySpec(byte[], Algorithm Name:): Creates a new SecretKeySpec object, which implements the SecretKey interface. This object is used for cryptographic operations such as signing or verifying a JWT.
- Byte Array: The first parameter is the byte array of the secret key.
- Algorithm Name: The second parameter is the algorithm's name (e.g., "HmacSHA512"), which specifies how the key will be used.
secretKey.getBytes(StandardCharsets.UTF_8):
- Purpose: Converts the secretKey string into a byte array using the UTF-8 character encoding.
Claims claims = Jwts.claims().setSubject("user123");
claims.put("role", "admin");
String jwt = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
Jwts.claims():
This method creates a new, empty Claims object. Claims is essentially a Map<String, Object> that holds the key-value pairs representing the claims in the JWT's payload. This Claims object is where you define both standard and custom claims that will be included in the JWT.
- claimsObject.put(KEY_ROLES, role): The put method is used to add a custom claim to the Claims object.
- claimsObject.setSubject(username):
- It sets the sub claim within the Claims object, which you can then pass to JwtBuilder using setClaims() to include it in the JWT.
Jwts Parser : Jwts.parseClaimsJws
parsing하면서 sign의 validation을 같이 확인하는 기능이 포함되어 있음
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
- Jwts.parserBuilder(): This initializes a new JwtParserBuilder, which is used to create a JwtParser that can parse and validate JWTs.
- .setSigningKey(key): Specifies the cryptographic key that should be used to validate the signature of the JWT.
- .build() : Finalizes the construction of the JwtParser based on the configurations provided (like the signing key).
- .parseClaimsJws(token): Parses the JWT, which is expected to be in the JWS (JSON Web Signature) format. This method performs the following steps:
- .getBody(): Retrieves the claims (i.e., the payload) from the parsed and validated JWT.
Security 구현하기 : JwtTokenProvider(using Jwts)
- JWT를 생성할때는 jsonwebtoken jjwt library class를 사용하여 build와 parsing을 통해서 진행한다.
- userEntity로부터 DAO authentication이 진행되면, userdetail의 정보(username, roles)와 만든시간, 유효시간, secretkey 등을 조합하여 JWT을 만들어 낸다
- 그리고 Token이 Client로부터 발신되었을 때 주요 정보(claim, client)를 추출해내기위한 utility method를 생성한다.
- secretkey는 특별한 문자를 만들어내기위해서 CLI 'echo plaintext | base64'를 사용한다. 암호화키는 주로 base64 으로 표기하여 유출을 방지한다. JWT생성을 위해서 입력될때는 decoded되어서 원본형태로 입력된후 jwt 방식으로 다시 encode된다.
jwt:
secret: c2FuZ2h3YS1zcHJpbmdzZWN1cml0eS1kaXZpZGVuLXByYWN0aWNlLWp3dC1zZWNyZXRrZXkK
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private static final String KEY_ROLES = "roles";
private static final Long TIME_TOKEN_EXPIRE_TIME = 1000 * 60 * 60L;//1hour
private final MemberService memberService;
@Value("${spring.jwt.secret}")
private String secretKey;
// DAO Authentication을 진행한 후, entity의 username(principal)과 roles
// (authorities)를 JWT에 담아내어 반환한다.
public String generateToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put(KEY_ROLES, roles);
var now = new Date();
var expiredTimes = new Date(now.getTime() + TIME_TOKEN_EXPIRE_TIME);
SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS512.getJcaName());
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiredTimes)
.signWith(key, SignatureAlgorithm.HS512) // Using SecretKeySpec instead of a raw string
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = this.parseClaimsFromToken(token);
return claims.getSubject();
}
private Claims parseClaimsFromToken(String token) {
SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS512.getJcaName());
try {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
- 그 외 Client가 token을 발송했을때, filter layer에서시간적으로 여전히 유효한지 확인하고, 유효하다면 Token이 authentication을 생성하여 SecurityContext에 input하기 위한 함수들을 생성한다.
UsernamePasswordAuthenticationToken:
- This is a concrete implementation of the Authentication interface provided by Spring Security. It represents an authentication request that contains the user’s principal (userDetails), credentials (password), and authorities (roles/permissions).
'개발기술 > 보안' 카테고리의 다른 글
네트워크 접근제어(NAC) ; NAT, 방화벽 (0) | 2025.02.25 |
---|---|
보안의 개념과 기술 (0) | 2025.01.19 |
HTTPS 의 TLS 동작과정 (0) | 2025.01.18 |
Security JWT,세션, 쿠키방식 (0) | 2025.01.15 |
Spring Security 전체개념 (0) | 2024.09.09 |