JWT, 스프링 시큐리티 사용하여 게시판에 로그인, 회원가입 기능 구현하기
회원을 구분할 수 있는 정보가 담기는 곳이 JWT의 payload부분이며, 이곳에 담기는 정보의 한 조각을 Claim이라고 한다.
Claim은 name / value 한 쌍으로 이루어져 있다.
스프링 시큐리티와 jwt에 사용되는 의존성 추가 후 gradle 새로고침하기
//security
implementation 'org.springframework.boot:spring-boot-starter-security'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
스프링 시큐리티 설정을 해주는 클래스
스프링 시큐리티를 사용하기 위해 Spring Security Filter Chain을 사용한다는 것을 명시해줘야하는데,
이것은 WebSecurityConfigurerAdapter를 상속받은 클래스에 @EnableWebSecurity annotation을 붙여주면 된다.
import com.sparta.BoardAPI2.jwt.JwtAuthenticationFilter;
import com.sparta.BoardAPI2.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
// 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// authenticationManager를 Bean 등록합니다.
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다.
.csrf().disable() // csrf 보안 토큰 disable처리.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다.
.and()
.authorizeRequests() // 요청에 대한 사용권한 체크
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);
// JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
}
}
User 정보를 담을 Entity 객체 역할
import com.sparta.BoardAPI2.dto.UserRequestDto;
import lombok.*;
import javax.persistence.*;
@Entity //db테이블과 일대일로 매핑
@Table(name = "`user`") // 테이블명 지정
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@Column(name = "user_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@Column(length = 50, unique = true)
private String username;
@Column(length = 100)
private String password;
public User(UserRequestDto requestDto) {
this.username = requestDto.getUsername();
this.password = requestDto.getPassword();
}
}
유저 정보를 DB에 저장하거나 찾는 역할
import com.sparta.BoardAPI2.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
회원가입에 필요한 정보 요청 클래스
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserRequestDto {
private String username;
private String password;
private String passwordCheck;
}
회원가입 메소드
1. userRepository의 findByUsername으로 같은 유저네임이 있는지 찾기
2. BCrypt엔코더로 비밀번호 암호화
3. requestDto의 password를 암호화된 비밀번호로 set해주기
4. requestDto정보로 새 user객체 만들기
5. 만든 객체 userRepository에 저장하기
import com.sparta.BoardAPI2.dto.UserRequestDto;
import com.sparta.BoardAPI2.entity.User;
import com.sparta.BoardAPI2.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 회원가입 조건 체크
public boolean checkSignupValueCondition(UserRequestDto requestDto){
String username = requestDto.getUsername();
String password = requestDto.getPassword();
String passwordCheck = requestDto.getPasswordCheck();
boolean checkValueCondition = true;
String pattern = "^[a-zA-Z0-9]*$";
if( !(Pattern.matches(pattern,username) && username.length()>=4 && username.length()<=12) ){
System.out.println("닉네임이 잘못되었습니다");
checkValueCondition=false;
}
else if( !(Pattern.matches(pattern,password) && password.length()>=4 && password.length()<=32) ){
System.out.println("비밀번호가 잘못되었습니다");
checkValueCondition=false;
}
else if( !password.equals(passwordCheck) ){
System.out.println("비밀번호 확인과 일치하지 않습니다");
checkValueCondition=false;
}
return checkValueCondition;
}
// 회원가입
public UserRequestDto register(UserRequestDto requestDto) {
// 요구조건 확인
if(!checkSignupValueCondition(requestDto)){
throw new IllegalArgumentException("회원가입 정보가 정확하지 않습니다.");
};
//회원 닉네임 중복 확인
Optional<User> found = userRepository.findByUsername(requestDto.getUsername());
if(found.isPresent()){
throw new IllegalArgumentException("중복된 사용자 id가 존재합니다.");
}
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
requestDto.setPassword(passwordEncoder.encode(requestDto.getPassword())); ;
User user = new User(requestDto);
userRepository.save(user);
return requestDto;
}
}
import com.sparta.BoardAPI2.dto.UserRequestDto;
import com.sparta.BoardAPI2.service.UserService;
import lombok.RequiredArgsConstructor;
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 UserController {
private final UserService userService;
// 회원가입
@PostMapping("/signup")
public UserRequestDto register (@RequestBody UserRequestDto requestDto) {
userService.register(requestDto);
return requestDto;
}
}
csrf 설정을 disable해준다.
(SecurityConfig.java)
http
// token을 사용하는 방식이기 때문에 csrf를 disable하게 설정
.csrf().disable()
문제상황 : postman에서 signup api가 실행이 안됨
해결 : 매개변수에 @RequestBody 추가해서 데이터를 파싱시켜줘야함
@PostMapping("/signup")
public UserRequestDto register (@RequestBody UserRequestDto requestDto) {
userService.register(requestDto);
return requestDto;
}
JWT 생성 및 유효성을 검증함
Claim정보에는 토큰에 부가적으로 실어 보낼 정보를 담을 수 있음
package com.sparta.BoardAPI2.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.List;
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private String secretKey = "asdfasdfasdfasdfasdfasdfasdfqwerqwerqwerqwerqwerqwer";
// 토큰 유효시간 30분
private long tokenValidTime = 30 * 60 * 1000L;
private final UserDetailsService userDetailsService;
// 객체 초기화, secretKey를 Base64로 인코딩한다.
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
// JWT 토큰 생성
public String createToken(String userPk) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
// claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
package com.sparta.BoardAPI2.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 헤더에서 JWT 를 받아옵니다.
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 유효한 토큰인지 확인합니다.
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext 에 Authentication 객체를 저장합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
login시 token반환하는 메소드 추가
// 수정 : service로 로직 옮기기
@PostMapping("/login")
public String login(@RequestBody LoginRequestDto requestDto) {
User member = userRepository.findByUsername(requestDto.getUsername())
.orElseThrow(() -> new IllegalArgumentException("가입되지 않은 회원입니다."));
// 비밀번호 복호화 (passwordEncoder사용)
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
if (passwordEncoder.matches(member.getPassword(), requestDto.getPassword())) {
throw new IllegalArgumentException("잘못된 비밀번호입니다.");
}
return jwtTokenProvider.createToken(member.getUsername());
}
import com.sparta.BoardAPI2.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class UserDetailsImpl implements UserDetails {
private final User user;
public UserDetailsImpl(User user) {
this.user = user;
}
public User getUser(){
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
import com.sparta.BoardAPI2.entity.User;
import com.sparta.BoardAPI2.repository.UserRepository;
import lombok.AllArgsConstructor;
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.Service;
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(()->new UsernameNotFoundException("아이디가 존재하지 않습니다"));
return new UserDetailsImpl(user);
}
}
로그인 후 토큰이 생성되면,
그 토큰을 이용해 유저네임을 출력하는 코드
@GetMapping("/api/userinfo")
@ResponseBody
public String getUserInfo(@AuthenticationPrincipal UserDetailsImpl userDetails){
if(userDetails!=null){
System.out.println("로그인 된 상태입니다.");
return userDetails.getUser().getUsername();
}
return "확인 불가";
}

Headers에 가서,
Key : X-AUTH-TOKEN
Value : /login API 실행 시 나오는 토큰 값
를 입력해 준 뒤 send
유저이름(1234)가 정상적으로 출력된다