
Spring Security for REST API with Spring Boot3.x 프로젝트 생성 시 원리 몰라도 일단 시작하기위해 코드만 작성해보겠습니다.

package com.ward.ward_server.controller;
import com.ward.ward_server.model.LoginRequest;
import com.ward.ward_server.model.LoginResponse;
import com.ward.ward_server.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/auth/login")
public LoginResponse login(@RequestBody @Validated LoginRequest request){
return authService.attemtLogion(request.getEmail(), request.getPassword());
}
}
package com.ward.ward_server.controller;
import com.ward.ward_server.security.UserPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class HelloController {
@GetMapping("/")
public String greeting(){
return "Hello, World";
}
@GetMapping("/secured")
public String secured(@AuthenticationPrincipal UserPrincipal principal) {
return "If you see this, then you're logged in as user " + principal.getEmail()
+ " User ID: " + principal.getUserId();
}
@GetMapping("/admin")
public String admin(@AuthenticationPrincipal UserPrincipal principal) {
return "If you see this, then you are an Admin. User ID: " + principal.getUserId();
}
}
package com.ward.ward_server.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserEntity {
private long id;
private String email;
@JsonIgnore
private String password;
private String role;
private String extraInfo;
}
package com.ward.ward_server.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
private String email;
private String password;
}
package com.ward.ward_server.model;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class LoginResponse {
private final String accessToken;
}
package com.ward.ward_server.security;
import com.ward.ward_server.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
private final UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
var user = userService.findByEmail(username).orElseThrow();
return UserPrincipal.builder()
.userId(user.getId())
.email(user.getEmail())
.authorities(List.of(new SimpleGrantedAuthority(user.getRole())))
.password(user.getPassword())
.build();
}
}
package com.ward.ward_server.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Optional;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtDecoder jwtDecoder;
private final JwtToPrincipalConverter jwtToPrincipalConverter;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
extractTokenFromRequest(request)
.map(jwtDecoder::decode)
.map(jwtToPrincipalConverter::convert)
.map(UserPrincipalAuthenticationToken::new)
.ifPresent(authentication -> SecurityContextHolder.getContext().setAuthentication(authentication));
filterChain.doFilter(request, response);
}
// Authorization: Bearer ey74823y58734.y34t897y34.y8934t8934 이런식이라서 substring 7 해서 ey 부분만 가져오기위해서
private Optional<String> extractTokenFromRequest(HttpServletRequest request) {
var token = request.getHeader("Authorization");
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
return Optional.of(token.substring(7));
}
return Optional.empty();
}
}
package com.ward.ward_server.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor // @Bean 주입할거기때문에 @RequiredArgsConstructor 어노테이션 사용
public class JwtDecoder {
private final JwtProperties properties;
public DecodedJWT decode(String token) {
return JWT.require(Algorithm.HMAC256(properties.getSecretKey()))
.build()
.verify(token);
}
}
package com.ward.ward_server.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
@Component
@RequiredArgsConstructor
public class JwtIssuer {
private final JwtProperties properties;
public String issue(long userId, String email, List<String> roles) {
return JWT.create()
.withSubject(String.valueOf(userId))
.withExpiresAt(Instant.now().plus(Duration.of(1, ChronoUnit.DAYS))) // 보통 duration 짧게 하는데 튜토리얼이니까 1day
.withClaim("e", email)
.withClaim("a", roles)
.sign(Algorithm.HMAC256(properties.getSecretKey()));
}
}
package com.ward.ward_server.security;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "security.jwt")
public class JwtProperties {
/**
* Secret key used for issuing JWT
*/
private String secretKey;
}
package com.ward.ward_server.security;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class JwtToPrincipalConverter {
public UserPrincipal convert(DecodedJWT jwt) {
return UserPrincipal.builder()
.userId(Long.valueOf(jwt.getSubject()))
.email(jwt.getClaim("e").asString())
.authorities(extractAuthoritiesFromClaim(jwt))
.build();
}
private List<SimpleGrantedAuthority> extractAuthoritiesFromClaim(DecodedJWT jwt) {
var claim = jwt.getClaim("a");
if (claim.isNull() || claim.isMissing()) return List.of();
return claim.asList(SimpleGrantedAuthority.class);
}
}
package com.ward.ward_server.security;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
package com.ward.ward_server.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Builder;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Getter
@Builder
public class UserPrincipal implements UserDetails {
private final Long userId;
private final String email;
@JsonIgnore
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
// 사용자에게 부여된 권한 목록을 반환한다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// 사용자의 비밀번호를 반환한다.
@Override
public String getPassword() {
return password;
}
// 사용자의 이름(아이디)를 반환한다.
@Override
public String getUsername() {
return email;
}
// 계정이 만료되지 않았는지?
@Override
public boolean isAccountNonExpired() {
return true; // true: 만료되자 않았다.
}
// 계정이 잠겨있지 않은지?
@Override
public boolean isAccountNonLocked() {
return true; // true: 잠겨있지 않다.
}
// 자격 증명이 만료되지 않았는지?
@Override
public boolean isCredentialsNonExpired() {
return true; // 만료되지 않았다.
}
// 활성화되어 있는지?
@Override
public boolean isEnabled() {
return true; // 활성화 되어있다
}
}
package com.ward.ward_server.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
public class UserPrincipalAuthenticationToken extends AbstractAuthenticationToken {
private final UserPrincipal principal;
public UserPrincipalAuthenticationToken(UserPrincipal principal) {
super(principal.getAuthorities());
this.principal = principal;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public UserPrincipal getPrincipal() {
return principal;
}
}
package com.ward.ward_server.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailService customUserDetailService;
private final UnauthorizedHandler unauthorizedHandler;
@Bean
public SecurityFilterChain applicationSecurity(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.securityMatcher("/**") // map current config to given resource path
.sessionManagement(sessionManagementConfigurer
-> sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable)
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.authorizeHttpRequests(registry -> registry // 요청에 대한 권한 설정 메서드
.requestMatchers("/").permitAll() // / 경로 요청에 대한 권한을 설정. permitAll() 모든 사용자, 인증되지않은 사용자에게 허용
.requestMatchers("/auth/login").permitAll() // 모든 사용자에게 허용
.requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN 에게만 허용
.anyRequest().authenticated() // 다른 나머지 모든 요청에 대한 권한 설정, authenticated()는 인증된 사용자에게만 허용, 로그인해야만 접근 가능
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(customUserDetailService)
.passwordEncoder(passwordEncoder())
.and().build();
}
}
package com.ward.ward_server.service;
import com.ward.ward_server.model.LoginResponse;
import com.ward.ward_server.security.JwtIssuer;
import com.ward.ward_server.security.UserPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthService {
private final JwtIssuer jwtIssuer;
private final AuthenticationManager authenticationManager;
public LoginResponse attemtLogion(String email, String password) {
var authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(email, password)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
var principal = (UserPrincipal) authentication.getPrincipal();
var roles = principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
var token = jwtIssuer.issue(principal.getUserId(), principal.getEmail(), roles);
return LoginResponse.builder()
.accessToken(token)
.build();
}
}
package com.ward.ward_server.service;
import com.ward.ward_server.entity.UserEntity;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserService {
private static final String EXISTING_EMAIL = "test@test.com";
private static final String ANOTHER_EMAIL = "next@test.com";
public Optional<UserEntity> findByEmail(String email) {
// TODO : Move this to a database
if (EXISTING_EMAIL.equalsIgnoreCase(email)) {
var user = new UserEntity();
user.setId(1L);
user.setEmail(EXISTING_EMAIL);
user.setPassword("$2a$12$kivpiZrLUW9.44c4P4KpgOrgvH.Y6UkWNP9/nxV5sZW2K5ztE78e6"); // test
user.setRole("ROLE_ADMIN");
user.setExtraInfo("My nice admin");
return Optional.of(user);
} else if (ANOTHER_EMAIL.equalsIgnoreCase(email)) {
var user = new UserEntity();
user.setId(99L);
user.setEmail(ANOTHER_EMAIL);
user.setPassword("$2a$12$kivpiZrLUW9.44c4P4KpgOrgvH.Y6UkWNP9/nxV5sZW2K5ztE78e6"); // test
user.setRole("ROLE_USER");
user.setExtraInfo("My nice user");
return Optional.of(user);
}
return Optional.empty();
}
}
security:
jwt:
secret-key: common-secret-key // 길게 바꿔서 넣기
정말 멋진 자료네요~!! pom.xml 내용이 궁금합니다.