Spring 버전 : 3.xx 이상
Security 버전 : 6.3

그림 출처
1. Filter chain : 다양한 필터로 이루어져있다. 하지만 우리가 이용할 것은 UsernamePasswordAuthenticationFilter로 사용자인증정보를 처리
2. UsernamePasswordAuthenticationToken :위에서 언급한 Filter에서 토큰을 만들어서 3번의 AuthenticationManager에 보내준다.
3. AuthenticationManager : 실제 인증을 실행, AuthenticationProvider들을 이용한다.
4. AuthenticationProvider : 각각의 Provider들은 특정 유형의 인증을 처리
5. PasswordEncoder : 인증과 인가에서 사용될 패스워드의 인코딩방식을 설정
6. UserDetailsService : 사용자의 정보를 가져온다. 사용자의 정보를 받아 loadByUsername을 호출하여 데이터베이스에서 관련 정보를 담은 UserDetail를 반환
7. UserDetails : 사용자의 아이디, 비밀번호 등의 정보를 가지고 있다.
8. SecurityContextHolder : Security Context에 접근권한 설정, 현재 실행 중인 스레드 세션에 접근
//사용자 정보를 활용
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(memberReq.getLogInId(), memberReq.getPassword());
# jwt
jwt.header = Authorization
// BASE64로 암호환 내용
jwt.secret= dyAeHubOOc8KaOfYB6XEQoEj1QzRlVgtjNL8PYs1A1tymZvvqkcEU7L1imkKHeDa
# unit is ms. 15 * 24 * 60 * 60 * 1000 = 15days
jwt.expiration=1296000000
//jwt
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
JwtUtil : JWT유효성 검증, JWT 토큰생성, JWT의 Claims 추출
@Component
@Slf4j
public class JwtUtil {
private final Key key;
private final long accessTokenExpTime;
// application.properties에서 secret 값 가져와서 key에 저장
public JwtUtil(@Value("${jwt.secret}") String secretKey,
@Value("${jwt.expiration}") long accessTokenExpTime)
{
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.accessTokenExpTime = accessTokenExpTime;
}
//Access Token 생성
public String createAccessToken(MemberReq member) {
return createToken(member, accessTokenExpTime);
}
public String getLoginId(String Token)
{
return parseClaims(Token).get("LogInId",String.class);
}
private String createToken(MemberReq member, long expireTime) {
Claims claims = Jwts.claims();
claims.put("LogInId", member.getLogInId());
claims.put("name", member.getName());
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime tokenValidity = now.plusSeconds(expireTime);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(Date.from(now.toInstant()))
.setExpiration(Date.from(tokenValidity.toInstant()))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
System.out.println("Invalid JWT Token" + e);
} catch (ExpiredJwtException e) {
System.out.println("Expired JWT Token"+ e);
} catch (UnsupportedJwtException e) {
System.out.println("Unsupported JWT Token" + e);
} catch (IllegalArgumentException e) {
System.out.println("JWT claims string is empty." + e);
}
return false;
}
public Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
로그인 시 사용자정보들을 검증한 뒤 로그인 성공 시 JWT토큰 반환
로그인을 강제로 시도를 하기 위하여 밑의 코드처럼 설정
@PostMapping("/signin")
public ResponseEntity<?> signIn(@RequestBody MemberReq memberReq)
{
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(memberReq.getLogInId(), memberReq.getPassword());
//현재 Request의 Security Context에 접근권한 설정
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//로그인 강제 시도
String token = this.memberService.login(memberReq);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization","Bearer "+token);
return new ResponseEntity<>(token,httpHeaders,HttpStatus.OK);
}
public String login(MemberReq memberReq){
String loginId = memberReq.getLogInId();
String password = memberReq.getPassword();
Optional<Member> findMember =memberRepository.findByLogInId(loginId);
//로그인 정보 확인
if(findMember.isEmpty()) {
throw new UsernameNotFoundException("로그인 정보가 존재하지 않습니다.");
}
Member member = findMember.get();
if(!passwordEncoder.matches(password, member.getPassword()))
{
throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
}
return jwtUtil.createAccessToken(memberReq);
}
@Getter
@Setter
@Entity
public class Member {
//기본키, 자동으로 1씩증가시키기위함
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long userId;
@Column(nullable = false, length = 30, updatable = false, unique = true)
String logInId;
@Column(nullable = false)
String name;
@Column(nullable = false)
String password;
//제품과 연결, 회원이 사라지면 해당 제품도 다같이 삭제
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE)
List<Product> productList;
}
@Getter
@NoArgsConstructor
public class UserDetailsImpl implements UserDetails{
@Autowired
Member member;
List<GrantedAuthority> roles = new ArrayList<>();
public UserDetailsImpl(Member member) {
super();
this.member = member;
}
public Member getUser() {
return this.member;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return member.getName();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
@Override //해당 User의 권한을 리턴하는 곳
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles;
}
public void setAuthorities(List<GrantedAuthority> roles) {
this.roles = roles;
}
}
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private MemberRepository memberRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
Member member = this.memberRepository.findByLogInId(loginId).orElseThrow(
()->new IllegalArgumentException("error"));
if(member == null) {
return null;
}
UserDetailsImpl userDetailsImpl = new UserDetailsImpl(member);
return userDetailsImpl;
}
}
loadUserByUsername : JWT에서 추출한 정보와 데이터베이스의 사용자 정보가 일치하는지 확인
존재 -> UserPasswordAuthenticationToken만들 때 필요한 UserDetails을 반환
미존재 -> 오류반환
@RequiredArgsConstructor
public class jwtAuthFilter extends OncePerRequestFilter { // OncePerRequestFilter ->한번 실행 보장
final private UserDetailsService userDetailsService;
final private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
//JWT토큰이 있는 경우
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer "))
{
String token = authorizationHeader.replace("Bearer ","");
String jwt = token.substring(7);
if(jwtUtil.validateToken(jwt))
{
String LogInId = jwtUtil.getLoginId(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(LogInId);
if(userDetails != null)
{
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//현재 Request의 Security Context에 접근권한 설정 *자동으로 설정해줌
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}
//다음 필터로 이동
filterChain.doFilter(request,response);
}
}
@Configuration
@EnableWebSecurity //모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션
@AllArgsConstructor
public class SecurityConfig {
final private UserDetailsService userDetailsService;
final private JwtUtil jwtUtil;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(HttpMethod.GET,"/api/product/list","/api/product/info/{productId:\\\\d+}")
.permitAll()
.requestMatchers("/api/member/**")
.permitAll()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable());
//세션 관리 상태 없음으로 구성, Spring Security가 세션 생성 or 사용 X
http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS));
//JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
http.addFilterBefore(new jwtAuthFilter(userDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter.class);
//FormLogin, BasicHttp 비활성화
http.formLogin((form) -> form.disable());
http.httpBasic(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean//해당 메서드의 리턴되는 오브젝트를 IoC로 등록해줌
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
postman을 이용해서 테스트를 했다.

그림과 같이 실제 로그인이 성공하여 jwt토큰정보를 출력해주는 걸 볼 수 있다.
해당 JWT가 내가 보낸 정보와 일치하는 지를 다시 확인
