본문 바로가기
웹개발/스프링부트

Spring boot jwt 로그인 구현

by 어컴띵 2021. 3. 15.

Springboot 로그인을 구현해보자. 여기서는 jwt를 이용한 로그인을 구현한다

 

다음 순서로 진행한다.

1. spring security, jwt gradle 설정

2. property에 시크릿키 설정

3. Jwt util 클래스 작성

4. UserDetailService 작성

5. Jwt 인증 컨트롤러 작성

6. jwt request, response 작성

7. jwt request 필터 작성

8. jwt 인증 엔트리 포인트 작성

9. 스프링 시큐리티 설정

10. 유저 관련 클래스

11. json web token 생성

12. json web tocker 검증하기

 

1. spring security, jwt gradle 설정

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.2'

2. property에 시크릿키 설정

spring:
  h2:
    console:
      enabled: true
      path: /h2-console
    port: 9082
  profiles:
    active: local
  datasource:
    hikari:
      jdbc-url: jdbc:h2:./data/testdb
      driver-class-name: org.h2.Driver
      username: sa
      password:
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
jwt:
  secret: jwtsecret

logging:
  level:
    root: debug
    org.hibernate.SQL: debug

3. Jwt util 클래스 작성

@Service
public class JwtTokenUtil implements Serializable {
    private static final long serialVersionUID = -798416586417070603L;
    private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;

    @Value("${jwt.secret}")
    private String secret;

    /**
     * jwt 토큰에서 username 검색
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token){
        try{
            return getClaimFromToken(token, Claims::getSubject);
        }catch(Exception ex){
            throw new UsernameFromTokenException("username from token exception");
        }
    }

    /**
     * jwt 토큰에서 날짜 만료 검색
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token){
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    /**
     * secret 키를 가지고 토큰에서 정보 검색
     * @param token
     * @return
     */
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    /**
     * 토큰 만료 체크
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token){
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * username으로 토큰생성
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails){
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    /**
     * 토큰을 생성하는 동안
     * 1. 토큰, Issuer, Expiration, Subject, ID로 claims를 정의한다.
     * 2. HS512알고리즘과 secret key를 가지고 JWT를 서명한다.
     * 3. JWS Compact Serialization (https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)에
     * 따라 JWT를 URL 안전 문자열로 압축
     * @param claims
     * @param username
     * @return
     */
    private String doGenerateToken(Map<String, Object> claims, String username) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 토큰 검증
     * @param token
     * @param userDetails
     * @return
     */
    public Boolean validateToken(String token, UserDetails userDetails){
        final String username = getUsernameFromToken(token);
        return(username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
    
}

4. UserDetailService 작성

@Service
public class JwtUserDetailsService implements UserDetailsService {

    @Autowired
    UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<User> userOptional = userService.findUserByEmail(username);

        User user = userOptional.orElseThrow(()->new UsernameNotFoundException("user name not found!"));
        return new org.springframework.security.core.userdetails.User(user.getEmail(),user.getPassword(),new ArrayList<>());

    }
}

5. Jwt 인증 컨트롤러 작성

@RestController
@CrossOrigin
public class JwtAuthenticationController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private JwtUserDetailsService userDetailService;

    @RequestMapping(value = "/api/v1/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception{
        authenticate(authenticationRequest.getUsername(),authenticationRequest.getPassword());
        final UserDetails userDetails = userDetailService
                .loadUserByUsername(authenticationRequest.getUsername());
        final String token = jwtTokenUtil.generateToken(userDetails);
        ApiResponse response = new ApiResponse();
        response.setData(token);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    private void authenticate(String username, String password){
        try{
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password));
        }catch (DisabledException ex){
            throw new DisabledException("USER_DISABLED",ex);
        }catch (BadCredentialsException ex){
            throw new BadCredentialsException("INVALID_CREDENTIALS",ex);
        }
    }
}

6. jwt request, response 작성

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class JwtRequest implements Serializable {
    private static final long serialVersionUID = 263011851808996064L;
    private String username;
    private String password;
}
@Getter
@AllArgsConstructor
public class JwtResponse implements Serializable {
    private static final long serialVersionUID = -868765630229614668L;
    private final String jwttoken;
}

7. jwt request 필터 작성

Bearer에 토큰이 있는지 체크하고 있으면 유효한지 확인후에 해당 url을 호출한다.

@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken =null;

        // JWT 토큰은 "Beare token"에 있다. Bearer단어를 제거하고 토큰만 받는다.
        if(requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")){
            jwtToken = requestTokenHeader.substring(7);
            try{
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException ex){
                log.error("Unable to get JWT token", ex);
            } catch (ExpiredJwtException ex){
                log.error("JWT Token has expired", ex);
                throw new ExpiredJwtException("JWT Token has expired");
            } catch (UsernameFromTokenException ex) {
                log.error("token valid error:" + ex.getMessage() ,ex);
                throw new UsernameFromTokenException("Username from token error");
            } catch (Exception ex) {
                log.error("token valid error:" + ex.getMessage() ,ex);
                throw new RuntimeException("11 Username from token error");
            }
        }else{
            log.warn("JWT token does not begin with Bearer String");
        }

        // 토큰을 가져오면 검증을 한다.
        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);

            // 토큰이 유효한 경우 수동으로 인증을 설정하도록 스프링 시큐리티를 구성한다.
            if(jwtTokenUtil.validateToken(jwtToken,userDetails)){
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities()
                        );
                usernamePasswordAuthenticationToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );

                // 컨텍스트에 인증을 설정 한 후 현재 사용자가 인증되도록 지정한다.
                // 그래서 Spring Security 설정이 성공적으로 넘어간다.
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request,response);
    }
}

8. jwt 인증 엔트리 포인트 작성

@Component
public class JwtAuthenticationEntryPoint  implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = 4858836244833364014L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

9. 스프링 시큐리티 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception{
        return super.authenticationManagerBean();
    }

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private UserDetailsService jwtUserDetailsService;

    @Autowired
    private ExceptionHandlerFilter exceptionHandlerFilter;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configGlobal(AuthenticationManagerBuilder auth) throws Exception{
        // 일치하는 자격증명을 위해 사용자를 로드할 위치를 알수 있도록
        // AuthenticationManager를 구성한다.
        // BCryptPasswordEncoder를 이용
        auth.userDetailsService(jwtUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http
                .cors().disable()
                .csrf().disable()
                // 이 요청은 인증을 하지 않는다.
                .authorizeRequests().antMatchers(
                        "/authenticate",
                "/api/v1/user",
                "/api/v1/authenticate")
                .permitAll()
                // 다른 모든 요청은 인증을 한다.
                .anyRequest().authenticated().and()
                // 상태없는 세션을 이용하며, 세션에 사용자의 상태를 저장하지 않는다.
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .formLogin().disable()
                .headers().frameOptions().disable();
        // 모든 요청에 토큰을 검증하는 필터를 추가한다.
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(exceptionHandlerFilter, JwtRequestFilter.class);
    }
}

10. 유저 관련 클래스

User.java

@Setter
@Getter
@Entity(name = "user")
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    @NotEmpty @Email @Size(min = 5, max = 255)
    private String email;
    @NotEmpty @Size(min = 10, max = 100)
    private String password;
    @NotEmpty @Size(min = 2, max = 100)
    private String name;

    @Builder
    public User(Long id, String email, String password,String name){
        this.id = id;
        this.email = email;
        this.password = password;
        this.name = name;
    }
}

SignVo.java

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SignVo {
    String email;
    String password;
    String name;
}

 

UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User,Long> {
    public Optional<User> findByEmail(String email);
}

UserService.java

@Service
public class UserService {

    UserRepository repository;
    PasswordEncoder passwordEncoder;

    @Autowired
    public UserService(UserRepository repository, PasswordEncoder passwordEncoder){
        this.repository = repository;
        this.passwordEncoder = passwordEncoder;
    }

    public void save(User user){
        Optional<User> aleadyUser = repository.findByEmail(user.getEmail());
        if( aleadyUser.isPresent()){
            throw new EmailDuplicateException("email duplicated",ErrorCode.EMAIL_DUPLICATION);
        }
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        repository.save(user);
    }

    public Optional<User> findUserByEmail(String email){
        Optional<User> aleadyUser = repository.findByEmail(email);
        return aleadyUser;
    }
}

UserController.java

@RestController
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping("/api/v1/user")
    @ResponseBody
    public ResponseEntity<ApiResponse> saveUser(@RequestBody SignVo signVo){
        User user = User.builder()
                .email(signVo.getEmail())
                .password(signVo.getPassword())
                .name(signVo.getName())
                .build();
        userService.save(user);
        return new ResponseEntity<>(new ApiResponse(), HttpStatus.OK);
    }

    @GetMapping("/api/v1/exist_user/{email}")
    public boolean findUserByEmail(@PathVariable String email){
        Optional<User> user = userService.findUserByEmail(email);
        return user.isPresent() ? true : false;
    }
}

ApiResponse.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse {
    private int status = 200;
    private String message = "OK";
    private String code = "200";
    private Object data = "no data";
}

유저 등록하기

11. json web token 생성

12. json web tocken 검증하기

참조: www.javainuse.com/spring/boot-jwt

 

Spring Boot Security + JWT Hello World Example | JavaInUse

package com.javainuse; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringBootHelloWorldApplication { public static void main(String[] args) { Sp

www.javainuse.com