{"id":8737,"date":"2024-03-16T20:16:21","date_gmt":"2024-03-16T11:16:21","guid":{"rendered":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=8737"},"modified":"2024-03-24T14:58:05","modified_gmt":"2024-03-24T05:58:05","slug":"spring-security-jwt","status":"publish","type":"post","link":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=8737","title":{"rendered":"Spring Security + JWT"},"content":{"rendered":"<h1>Spring Security + JWT<\/h1>\n<p>Spring Security \uc640 JWT \ub97c \uc774\uc6a9\ud574 API \uc811\uadfc\uc81c\uc5b4\ub97c \uad6c\ud604\ud574 \ubd05\ub2c8\ub2e4.<\/p>\n<p>\ud30c\uc77c\uc758 \uc218\ub294 \ub9ce\uc9c0\ub9cc \uc2e4\uc81c\ub85c\ub294,<br \/>\nJwtAuthenticationFilter \ub97c \ud638\ucd9c\ud558\ub294 \ubd80\ubd84\uacfc JwtAuthenticationFilter \uc758 \ub0b4\ubd80 \uc791\ub3d9\ub9cc \ud655\uc778\ud558\uba74 Security \uac00 \uc791\ub3d9\ud558\ub294 \ubc29\uc2dd\uc744 \ub300\ubd80\ubd84 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n<p>\uadf8\ubc16\uc5d0 \ud1a0\ud070\uc744 \uc0dd\uc131\ud558\uace0 \uac31\uc2e0\ud558\ub294 \ubd80\ubd84\uc740 RestController \uc5d0\uc11c \uc9c1\uc811 \ud638\ucd9c\ud558\ubbc0\ub85c \uc774\ud574\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4.<\/p>\n<blockquote>\n<p>\uc8fc\uc758!!!!<br \/>\nsecret \ub9cc \uc788\uc73c\uba74 \ub204\uad6c\ub098 \ud1a0\ud070\uc744 \ub9cc\ub4e4\uc5b4\ub0bc \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br \/>\n\uc18c\uc2a4\ucf54\ub4dc\ub97c \uce74\ud53c \ud398\uc774\uc2a4\ud2b8 \ud558\ub294\uac74 \uc88b\uc740\ub370 secret \ub294 \ubc18\ub4dc\uc2dc \ubcc0\uacbd\ud558\uc138\uc694.<\/p>\n<p>\ub610\ud55c, Brute Force \ub300\uc751\ucf54\ub4dc\ub3c4 \uc5c6\uc2b5\ub2c8\ub2e4.<br \/>\n(github \uc5d0\ub294 \ub300\uc751 \ucf54\ub4dc \ub123\uc5c8\uc2b5\ub2c8\ub2e4.)<\/p>\n<\/blockquote>\n<h2>github<\/h2>\n<p><a href=\"https:\/\/github.com\/skyer9\/spring-boot-rest-api-example\">https:\/\/github.com\/skyer9\/spring-boot-rest-api-example<\/a><\/p>\n<h2>\uc758\uc874\uc131 \ucd94\uac00<\/h2>\n<p>Spring Security, JWT, H2 \ub97c \uc124\uc815\ud574 \uc90d\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-grrvy\">dependencies {\n    implementation &#039;org.springframework.boot:spring-boot-starter-security&#039;\n    implementation &#039;org.springframework.boot:spring-boot-starter-validation&#039;\n    testImplementation &#039;org.springframework.security:spring-security-test&#039;\n    implementation group: &#039;io.jsonwebtoken&#039;, name: &#039;jjwt-api&#039;, version: &#039;0.11.5&#039;\n    runtimeOnly group: &#039;io.jsonwebtoken&#039;, name: &#039;jjwt-impl&#039;, version: &#039;0.11.5&#039;\n    runtimeOnly group: &#039;io.jsonwebtoken&#039;, name: &#039;jjwt-jackson&#039;, version: &#039;0.11.5&#039;\n}<\/code><\/pre>\n<pre><code class=\"language-yaml\">spring:\n  h2:\n    console:\n      enabled: true\n  datasource:\n    url: jdbc:h2:mem:testdb;NON_KEYWORDS=USER\n    driver-class-name: org.h2.Driver\n    username: sa\n    password:\n  jpa:\n    open-in-view: false\n    hibernate:\n      ddl-auto: create-drop\n    properties:\n      hibernate:\n        format_sql: true\n        show_sql: ture\n    defer-datasource-initialization: true\n\njwt:\n  secret: a2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbXRva2FyaW10b2thcmltdG9rYXJpbQ==\n  access-token-validity-in-milliseconds: 1800000\n  refresh-token-validity-in-milliseconds: 86400000<\/code><\/pre>\n<pre><code class=\"language-sql\">-- insert into user (USERNAME, PASSWORD, NICKNAME, ACTIVATED)\n-- values (&#039;admin&#039;, &#039;$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO\/Vxd5DOi&#039;, &#039;admin&#039;, 1);\n\ninsert into AUTHORITY (AUTHORITY_NAME) values (&#039;USER&#039;);\ninsert into AUTHORITY (AUTHORITY_NAME) values (&#039;ADMIN&#039;);\n\n-- insert into USER_AUTHORITY (USERNAME, AUTHORITY_NAME) values (&#039;admin&#039;, &#039;USER&#039;);\n-- insert into USER_AUTHORITY (USERNAME, AUTHORITY_NAME) values (&#039;admin&#039;, &#039;ADMIN&#039;);<\/code><\/pre>\n<h2>Util<\/h2>\n<p>ResponseDto.java<\/p>\n<pre><code class=\"language-java\">@Getter\n@Setter\n@Builder\npublic class ResponseDto&lt;T&gt; {\n    private HttpStatus status;\n    private String message;\n    private T data;\n\n    public static&lt;T&gt; ResponseDto&lt;T&gt; res(final HttpStatus status, final String message) {\n        return res(status, message, null);\n    }\n\n    public static&lt;T&gt; ResponseDto&lt;T&gt; res(final HttpStatus status, final String message, final T data) {\n        return ResponseDto.&lt;T&gt;builder()\n                .status(status)\n                .message(message)\n                .data(data)\n                .build();\n    }\n}<\/code><\/pre>\n<p>SecurityUtil.java<\/p>\n<pre><code class=\"language-java\">public class SecurityUtil {\n\n    private SecurityUtil() {}\n\n    public static Optional&lt;String&gt; getCurrentUsername() {\n\n        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n\n        if (authentication == null) {\n            return Optional.empty();\n        }\n\n        String username = null;\n        if (authentication.getPrincipal() instanceof UserDetails springSecurityUser) {\n            username = springSecurityUser.getUsername();\n        } else if (authentication.getPrincipal() instanceof String) {\n            username = (String) authentication.getPrincipal();\n        }\n\n        return Optional.ofNullable(username);\n    }\n\n    public static void setResponse(HttpServletResponse response, Exception ex) throws RuntimeException, IOException {\n        ObjectMapper mapper = new ObjectMapper();\n        response.setStatus(HttpServletResponse.SC_OK);\n        response.setContentType(MediaType.APPLICATION_JSON_VALUE);\n        response.getWriter().print(mapper.writeValueAsString(ResponseDto.res(HttpStatus.BAD_REQUEST, ex.getMessage())));\n    }\n\n    public static String getClientIpAddress(HttpServletRequest request) {\n        String ipAddress = request.getHeader(&quot;X-FORWARDED-FOR&quot;);\n        if (ipAddress == null) {\n            return request.getRemoteAddr();\n        }\n        return ipAddress.contains(&quot;,&quot;) ? ipAddress.split(&quot;,&quot;)[0] : ipAddress;\n    }\n}<\/code><\/pre>\n<h2>Config \ub808\uc774\uc5b4<\/h2>\n<p>SecurityConfig.java \uc640 JwtTokenProvider.java \ub97c \uc911\uc810\uc801\uc73c\ub85c \ud655\uc778\ud574 \uc90d\ub2c8\ub2e4.<\/p>\n<p>generateAccessToken(), generateRefreshToken() \uac00 \uc2e4\uc81c\ub85c JWT \ud1a0\ud070\uc744 \uc0dd\uc131\ud558\ub294 \ub85c\uc9c1\uc785\ub2c8\ub2e4.<\/p>\n<p>JwtAccessDeniedHandler.java<\/p>\n<pre><code class=\"language-java\">@Component\npublic class JwtAccessDeniedHandler implements AccessDeniedHandler {\n    @Override\n    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {\n        SecurityUtil.setResponse(response, accessDeniedException);\n    }\n}<\/code><\/pre>\n<p>JwtAuthenticationEntryPoint.java<\/p>\n<pre><code class=\"language-java\">@Component\npublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {\n    @Override\n    public void commence(HttpServletRequest request,\n                         HttpServletResponse response,\n                         AuthenticationException authException) throws IOException {\n        SecurityUtil.setResponse(response, authException);\n    }\n}<\/code><\/pre>\n<p>JwtAuthenticationFilter.java<\/p>\n<pre><code class=\"language-java\">@RequiredArgsConstructor\npublic class JwtAuthenticationFilter extends GenericFilterBean {\n    private final JwtTokenProvider jwtTokenProvider;\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        String token = resolveToken((HttpServletRequest) request);\n        if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {\n            Authentication authentication = jwtTokenProvider.getAuthentication(token);\n            SecurityContextHolder.getContext().setAuthentication(authentication);\n        }\n        chain.doFilter(request, response);\n    }\n\n    private String resolveToken(HttpServletRequest request) {\n        String bearerToken = request.getHeader(JwtTokenProvider.AUTHORIZATION_HEADER);\n        if (StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(&quot;Bearer&quot;)) {\n            return bearerToken.substring(7);\n        }\n        return null;\n    }\n}<\/code><\/pre>\n<p>JwtExceptionFilter.java<\/p>\n<pre><code class=\"language-java\">@Slf4j\n@RequiredArgsConstructor\n@Component\npublic class JwtExceptionFilter extends OncePerRequestFilter {\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {\n        try {\n            chain.doFilter(request, response);\n        } catch (JwtException ex) {\n            SecurityUtil.setResponse(response, ex);\n        }\n    }\n}<\/code><\/pre>\n<p>\uc18c\uc2a4\ucf54\ub4dc \ub77c\uc778\uc218\ub294 \ub9ce\uc740\ub370 \uc758\uc678\ub85c \ub0b4\uc6a9\uc740 \uac04\ub2e8\ud569\ub2c8\ub2e4.<\/p>\n<p>JwtTokenProvider.java<\/p>\n<pre><code class=\"language-java\">@Slf4j\n@Component\n@RequiredArgsConstructor\npublic class JwtTokenProvider implements InitializingBean {\n    private Key key;\n\n    public static final String AUTHORITIES_KEY = &quot;auth&quot;;\n    public static final String AUTHORIZATION_HEADER = &quot;Authorization&quot;;\n\n    @Value(&quot;${jwt.secret}&quot;)\n    private String secret;\n    @Value(&quot;${jwt.access-token-validity-in-milliseconds}&quot;)\n    private long accessTokenValidityInMilliseconds;\n    @Value(&quot;${jwt.refresh-token-validity-in-milliseconds}&quot;)\n    private long refreshTokenValidityInMilliseconds;\n\n    private final RefreshTokenRepository refreshTokenRepository;\n\n    @Transactional\n    public TokenDto generateToken(MyUser myUser) {\n        long now = (new Date()).getTime();\n        String authorities = getAuthorities(myUser);\n        String accessToken = generateAccessToken(myUser.getUsername(), authorities, now);\n        String refreshToken = generateRefreshToken(myUser.getUsername(), now);\n\n        return TokenDto.builder()\n                .grantType(&quot;Bearer&quot;)\n                .accessToken(accessToken)\n                .refreshToken(refreshToken)\n                .build();\n    }\n\n    public TokenDto reissueToken(MyUser myUser, RefreshToken refreshToken) {\n        validateToken(refreshToken.getToken());\n\n        long now = (new Date()).getTime();\n        String authorities = getAuthorities(myUser);\n        String accessToken = generateAccessToken(refreshToken.getUsername(), authorities, now);\n\n        return TokenDto.builder()\n                .grantType(&quot;Bearer&quot;)\n                .accessToken(accessToken)\n                .refreshToken(refreshToken.getToken())\n                .build();\n    }\n\n    public Authentication getAuthentication(String accessToken) {\n        Claims claims = parseClaims(accessToken);\n\n        if (claims.get(AUTHORITIES_KEY) == null) {\n            throw new RuntimeException(&quot;\uad8c\ud55c \uc815\ubcf4\uac00 \uc5c6\ub294 \ud1a0\ud070\uc785\ub2c8\ub2e4.&quot;);\n        }\n\n        Collection&lt;? extends GrantedAuthority&gt; authorities =\n                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(&quot;,&quot;))\n                        .map(SimpleGrantedAuthority::new)\n                        .collect(Collectors.toList());\n\n        UserDetails principal = new User(claims.getSubject(), &quot;&quot;, authorities);\n        return new UsernamePasswordAuthenticationToken(principal, &quot;&quot;, authorities);\n    }\n\n    public boolean validateToken(String token) {\n        try {\n            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);\n        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {\n            throw new JwtException(&quot;Invalid JWT Token&quot;);\n        } catch (ExpiredJwtException e) {\n            throw new JwtException(&quot;Expired JWT Token&quot;);\n        } catch (UnsupportedJwtException e) {\n            throw new JwtException(&quot;Unsupported JWT Token&quot;);\n        } catch (IllegalArgumentException e) {\n            throw new JwtException(&quot;JWT claims string is empty&quot;);\n        } catch (Exception e) {\n            throw new JwtException(&quot;Unknown exception occurred&quot;, e);\n        }\n        return true;\n    }\n\n    private Claims parseClaims(String accessToken) {\n        try {\n            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();\n        } catch (ExpiredJwtException e) {\n            return e.getClaims();\n        }\n    }\n\n    private String generateAccessToken(String username, String authorities, long now) {\n        Date accessTokenExpiresIn = new Date(now + accessTokenValidityInMilliseconds);\n        return Jwts.builder()\n                .setSubject(username)\n                .claim(AUTHORITIES_KEY, authorities)\n                .setExpiration(accessTokenExpiresIn)\n                .signWith(key, SignatureAlgorithm.HS512)\n                .compact();\n    }\n\n    private String generateRefreshToken(String username, long now) {\n        String refreshToken = Jwts.builder()\n                .setSubject(username)\n                .setExpiration(new Date(now + refreshTokenValidityInMilliseconds))\n                .signWith(key, SignatureAlgorithm.HS512)\n                .compact();\n\n        Optional&lt;RefreshToken&gt; saved = refreshTokenRepository.findByToken(refreshToken);\n        if (saved.isEmpty()) {\n            refreshTokenRepository.save(RefreshToken\n                    .builder()\n                    .token(refreshToken)\n                    .expiryDate(now + refreshTokenValidityInMilliseconds)\n                    .username(username)\n                    .build());\n        }\n\n        return refreshToken;\n    }\n\n    private String getAuthorities(MyUser myUser) {\n        return myUser\n                .getAuthorities()\n                .stream()\n                .map(Authority::addPrefix)\n                .collect(Collectors.joining(&quot;,&quot;));\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        byte[] keyBytes = Decoders.BASE64.decode(secret);\n        this.key = Keys.hmacShaKeyFor(keyBytes);\n    }\n}<\/code><\/pre>\n<p>SecurityConfig.java<\/p>\n<pre><code class=\"language-java\">@Configuration\n@EnableWebSecurity\n@RequiredArgsConstructor\npublic class SecurityConfig {\n    private final JwtTokenProvider jwtTokenProvider;\n    private final JwtExceptionFilter jwtExceptionFilter;\n    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;\n    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;\n\n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n        http\n                .csrf(AbstractHttpConfigurer::disable)\n                .headers((headerConfig) -&gt;\n                        headerConfig.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)\n                )\n                .authorizeHttpRequests((authorizeRequests) -&gt;\n                        authorizeRequests\n                                .requestMatchers(PathRequest.toH2Console()).permitAll()\n                                .requestMatchers(&quot;\/api\/createAdminUser&quot;).permitAll()\n                                .requestMatchers(&quot;\/api\/signin&quot;).permitAll()\n                                .requestMatchers(&quot;\/api\/signup&quot;).permitAll()\n                                .requestMatchers(&quot;\/api\/reissue&quot;).permitAll()\n                                .requestMatchers(&quot;\/favicon.ico&quot;).permitAll()\n                                .requestMatchers(&quot;\/swagger-ui\/**&quot;).permitAll()\n                                .requestMatchers(&quot;\/v3\/api-docs\/**&quot;).permitAll()\n                                .requestMatchers(&quot;\/api\/user&quot;).hasAnyRole(&quot;ADMIN&quot;, &quot;USER&quot;)\n                                .requestMatchers(&quot;\/api\/user\/**&quot;).hasRole(&quot;ADMIN&quot;)\n                                .anyRequest().authenticated()\n                )\n                .exceptionHandling((exceptionConfig) -&gt;\n                        exceptionConfig\n                                .authenticationEntryPoint(jwtAuthenticationEntryPoint) \/\/ handle 401 Error\n                                .accessDeniedHandler(jwtAccessDeniedHandler)           \/\/ handle 403 Error\n                )\n                .sessionManagement(configurer -&gt; configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))\n                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)\n                .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);\n        return http.build();\n    }\n\n    @Bean\n    public PasswordEncoder passwordEncoder() {\n        return new BCryptPasswordEncoder();\n    }\n}<\/code><\/pre>\n<h2>Domain \ub808\uc774\uc5b4<\/h2>\n<p>RefreshToken \uc744 \ub370\uc774\ud0c0\ubca0\uc774\uc2a4\uc5d0 \uc800\uc7a5\ud558\uace0 \uac31\uc2e0\uc694\uccad\uc774 \ub4e4\uc5b4\uc62c\ub54c RefreshToken \uc758 \uc720\ud6a8\uc131\uc744 \uac80\uc99d\ud569\ub2c8\ub2e4.<br \/>\n\ub530\ub77c\uc11c, AccessToken \uc774 \ud0c8\ucde8\ub2f9\ud574\ub3c4 \ucd5c\ub300\ud55c \uc811\uadfc\uc744 \uc81c\ud55c\ud560 \uc218\ub2e8\uc774 \uc0dd\uae41\ub2c8\ub2e4.<br \/>\n(RefreshToken \uc0ad\uc81c \ub4f1)<\/p>\n<pre><code class=\"language-java\">@Entity\n@Getter\n@Setter\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\n@Table(name = &quot;authority&quot;)\npublic class Authority {\n    @Id\n    @Column(name = &quot;authority_name&quot;, length = 50)\n    private String authorityName;\n\n    public String addPrefix() {\n        return this.authorityName.startsWith(&quot;ROLE_&quot;) ? authorityName : &quot;ROLE_&quot; + authorityName;\n    }\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Entity\n@Getter\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\n@Table(name= &quot;user&quot;)\npublic class MyUser {\n\n    @Id\n    @Column(name = &quot;username&quot;, length = 128, unique = true)\n    private String username;\n\n    @JsonIgnore\n    @Column(name = &quot;password&quot;, length = 256)\n    private String password;\n\n    @Column(name = &quot;nickname&quot;, length = 128)\n    private String nickname;\n\n    @JsonIgnore\n    @Column(name = &quot;activated&quot;)\n    private boolean activated;\n\n    @ManyToMany\n    @Fetch(FetchMode.JOIN)\n    @JoinTable(\n            name = &quot;user_authority&quot;,\n            joinColumns = {@JoinColumn(name = &quot;username&quot;, referencedColumnName = &quot;username&quot;)},\n            inverseJoinColumns = {@JoinColumn(name = &quot;authority_name&quot;, referencedColumnName = &quot;authority_name&quot;)})\n    private Set&lt;Authority&gt; authorities;\n}<\/code><\/pre>\n<pre><code class=\"language-java\">public interface MyUserRepository extends JpaRepository&lt;MyUser, String&gt; {\n    Optional&lt;MyUser&gt; findByUsername(String username);\n\n    Optional&lt;MyUser&gt; findOneWithAuthoritiesByUsername(String username);\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Entity\n@Getter\n@Setter\n@Builder\n@AllArgsConstructor\n@NoArgsConstructor\n@Table(name= &quot;refresh_token&quot;)\npublic class RefreshToken {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n\n    @Column(unique = true)\n    private String token;\n\n    private Long expiryDate;\n    private String username;\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Repository\npublic interface RefreshTokenRepository extends CrudRepository&lt;RefreshToken, Long&gt; {\n    Optional&lt;RefreshToken&gt; findByToken(String token);\n}<\/code><\/pre>\n<h2>Service \ub808\uc774\uc5b4<\/h2>\n<pre><code class=\"language-java\">@Getter\n@Setter\n@NoArgsConstructor\npublic class LoginDto {\n    @NotNull\n    @Size(min = 3, max = 128)\n    private String username;\n\n    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)\n    @NotNull\n    @Size(min = 3, max = 256)\n    private String password;\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Getter\n@Setter\n@Builder\n@NoArgsConstructor\n@AllArgsConstructor\npublic class MyUserDto {\n    @NotNull\n    @Size(min = 3, max = 128)\n    private String username;\n\n    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)\n    @NotNull\n    @Size(min = 3, max = 256)\n    private String password;\n\n    @NotNull\n    @Size(min = 3, max = 128)\n    private String nickname;\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Getter\n@Setter\n@AllArgsConstructor\n@NoArgsConstructor\npublic class RefreshTokenRequestDTO {\n    private String token;\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Getter\n@Setter\n@Builder\n@AllArgsConstructor\npublic class TokenDto {\n    private String grantType;\n    private String accessToken;\n    private String refreshToken;\n}<\/code><\/pre>\n<p>\uc544\ub798 \ud074\ub798\uc2a4\ub97c \uad6c\ud604\ud574 \uc8fc\uc9c0 \uc54a\uc73c\uba74 \ub0b4\ubd80\uc801\uc73c\ub85c \ubd88\ud544\uc694\ud55c \ub85c\uc9c1\uc774 \ucd94\uac00\ub85c \uc791\ub3d9\ud558\ubbc0\ub85c \uc0dd\uc131\ud574 \uc90d\ub2c8\ub2e4.<\/p>\n<pre><code class=\"language-java\">@Service\n@RequiredArgsConstructor\npublic class CustomUserDetailsService implements UserDetailsService {\n    @Override\n    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n        return null;\n    }\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Service\n@RequiredArgsConstructor\npublic class MyUserService {\n    private final MyUserRepository myUserRepository;\n    private final JwtTokenProvider jwtTokenProvider;\n    private final PasswordEncoder passwordEncoder;\n\n    @Transactional(noRollbackFor = UserNotFoundException.class)\n    public TokenDto login(HttpServletRequest request, String username, String password) {\n        MyUser myUser = getUserWithAuthorities(username);\n        if (!passwordEncoder.matches(password, myUser.getPassword())) {\n            throw new UserNotFoundException(&quot;username or password is incorrect&quot;);\n        }\n\n        return jwtTokenProvider.generateToken(myUser);\n    }\n\n    @Transactional\n    public TokenDto reissue(RefreshToken refreshToken) {\n        MyUser myUser = getUserWithAuthorities(refreshToken.getUsername());\n        return jwtTokenProvider.reissueToken(myUser, refreshToken);\n    }\n\n    @Transactional\n    public MyUser createAdminUser(MyUserDto myUserDto) {\n        if (myUserRepository.count() &gt; 0) {\n            throw new RuntimeException(&quot;User account must be zero&quot;);\n        }\n\n        Set&lt;Authority&gt; authorities = new HashSet&lt;&gt;();\n        authorities.add(Authority.builder().authorityName(&quot;ADMIN&quot;).build());\n        authorities.add(Authority.builder().authorityName(&quot;USER&quot;).build());\n\n        MyUser myUser = MyUser.builder()\n                .username(myUserDto.getUsername())\n                .password(passwordEncoder.encode(myUserDto.getPassword()))\n                .nickname(myUserDto.getNickname())\n                .authorities(authorities)\n                .activated(true)\n                .build();\n\n        return myUserRepository.save(myUser);\n    }\n\n    @Transactional\n    public MyUser signup(MyUserDto myUserDto) {\n        if (myUserRepository.findOneWithAuthoritiesByUsername(myUserDto.getUsername()).orElse(null) != null) {\n            throw new RuntimeException(&quot;Unavailable username&quot;);\n        }\n\n        Authority authority = Authority.builder()\n                .authorityName(&quot;USER&quot;)\n                .build();\n\n        MyUser myUser = MyUser.builder()\n                .username(myUserDto.getUsername())\n                .password(passwordEncoder.encode(myUserDto.getPassword()))\n                .nickname(myUserDto.getNickname())\n                .authorities(Collections.singleton(authority))\n                .activated(true)\n                .build();\n\n        return myUserRepository.save(myUser);\n    }\n\n    @Transactional(readOnly = true)\n    public MyUser getUserWithAuthorities(String username) {\n        Optional&lt;MyUser&gt; myUser = myUserRepository.findOneWithAuthoritiesByUsername(username);\n        if (myUser.isEmpty()) {\n            throw new UserNotFoundException(&quot;User not found&quot;);\n        }\n\n        return myUser.get();\n    }\n\n    @Transactional(readOnly = true)\n    public MyUser getMyUserWithAuthorities() {\n        Optional&lt;MyUser&gt; myUser = SecurityUtil\n                .getCurrentUsername()\n                .flatMap(myUserRepository::findOneWithAuthoritiesByUsername);\n        if (myUser.isEmpty()) {\n            throw new UserNotFoundException(&quot;User not found&quot;);\n        }\n\n        return myUser.get();\n    }\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Service\n@RequiredArgsConstructor\npublic class RefreshTokenService {\n    private final RefreshTokenRepository refreshTokenRepository;\n\n    public Optional&lt;RefreshToken&gt; findByToken(String token){\n        return refreshTokenRepository.findByToken(token);\n    }\n}<\/code><\/pre>\n<h2>Controller \ub808\uc774\uc5b4<\/h2>\n<pre><code class=\"language-java\">@RestControllerAdvice(&quot;com.example.api.web&quot;)\npublic class RestExceptionHandler {\n    @ExceptionHandler(RuntimeException.class)\n    public ResponseEntity&lt;?&gt; handleRuntimeException(RuntimeException e) {\n        return ResponseEntity.ok(ResponseDto.res(HttpStatus.BAD_REQUEST, e.getMessage()));\n    }\n\n    @ExceptionHandler(UserNotFoundException.class)\n    public ResponseEntity&lt;?&gt; handleUserNotFoundException(UserNotFoundException e) {\n        return ResponseEntity.ok(ResponseDto.res(HttpStatus.BAD_REQUEST, e.getMessage()));\n    }\n}<\/code><\/pre>\n<pre><code class=\"language-java\">public class UserNotFoundException extends RuntimeException {\n    public UserNotFoundException(String msg) {\n        super(msg);\n    }\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@RestController\n@RequestMapping(&quot;\/api&quot;)\n@RequiredArgsConstructor\npublic class AuthController {\n    private final MyUserService myUserService;\n    private final RefreshTokenService refreshTokenService;\n\n    @PostMapping(&quot;\/createAdminUser&quot;)\n    public ResponseEntity&lt;MyUser&gt; createAdminUser(\n            @Valid @RequestBody MyUserDto userDto\n    ) {\n        return ResponseEntity.ok(myUserService.createAdminUser(userDto));\n    }\n\n    @PostMapping(&quot;\/signin&quot;)\n    public ResponseEntity&lt;TokenDto&gt; signin(@RequestBody LoginDto loginDto) {\n        String username = loginDto.getUsername();\n        String password = loginDto.getPassword();\n\n        TokenDto tokenDto = myUserService.login(username, password);\n        HttpHeaders httpHeaders = new HttpHeaders();\n        httpHeaders.add(JwtTokenProvider.AUTHORIZATION_HEADER, &quot;Bearer &quot; + tokenDto.getAccessToken());\n\n        return new ResponseEntity&lt;&gt;(tokenDto, httpHeaders, HttpStatus.OK);\n    }\n\n    @PostMapping(&quot;\/reissue&quot;)\n    public ResponseEntity&lt;TokenDto&gt; reissue(@RequestBody RefreshTokenRequestDTO requestDTO) {\n        Optional&lt;RefreshToken&gt; refreshToken = refreshTokenService.findByToken(requestDTO.getToken());\n        if (refreshToken.isEmpty()) {\n            throw new RuntimeException(&quot;Token not found&quot;);\n        }\n        TokenDto tokenDto = myUserService.reissue(refreshToken.get());\n        HttpHeaders httpHeaders = new HttpHeaders();\n        httpHeaders.add(JwtTokenProvider.AUTHORIZATION_HEADER, &quot;Bearer &quot; + tokenDto.getAccessToken());\n\n        return new ResponseEntity&lt;&gt;(tokenDto, httpHeaders, HttpStatus.OK);\n    }\n}<\/code><\/pre>\n<pre><code class=\"language-java\">@Slf4j\n@RestController\n@RequiredArgsConstructor\n@RequestMapping(&quot;\/api&quot;)\npublic class MyUserController {\n    private final MyUserService myUserService;\n\n    @PostMapping(&quot;\/signup&quot;)\n    public ResponseEntity&lt;MyUser&gt; signup(\n            @Valid @RequestBody MyUserDto userDto\n    ) {\n        return ResponseEntity.ok(myUserService.signup(userDto));\n    }\n\n    @GetMapping(&quot;\/user&quot;)\n    @PreAuthorize(&quot;hasAnyRole(&#039;USER&#039;,&#039;ADMIN&#039;)&quot;)\n    public ResponseEntity&lt;MyUser&gt; getMyUserInfo() {\n        return ResponseEntity.ok(myUserService.getMyUserWithAuthorities());\n    }\n\n    @GetMapping(&quot;\/user\/{username}&quot;)\n    @PreAuthorize(&quot;hasAnyRole(&#039;ADMIN&#039;)&quot;)\n    public ResponseEntity&lt;MyUser&gt; getUserInfo(@PathVariable String username) {\n        return ResponseEntity.ok(myUserService.getUserWithAuthorities(username));\n    }\n}<\/code><\/pre>\n<h2>\ud14c\uc2a4\ud2b8<\/h2>\n<pre><code class=\"language-bash\">POST http:\/\/localhost:8080\/api\/createAdminUser\n{\n  &quot;username&quot;: &quot;skyer9&quot;,\n  &quot;password&quot;: &quot;abcd1234&quot;,\n  &quot;nickname&quot;: &quot;skyer9&quot;\n}\n\n# POST http:\/\/localhost:8080\/api\/signin\n# {\n#   &quot;username&quot;: &quot;admin&quot;,\n#   &quot;password&quot;: &quot;admin&quot;\n# }\n\nPOST http:\/\/localhost:8080\/api\/signin\n{\n  &quot;username&quot;: &quot;skyer9&quot;,\n  &quot;password&quot;: &quot;abcd1234&quot;\n}\n\nPOST http:\/\/localhost:8080\/api\/signup\n{\n  &quot;username&quot;: &quot;skyer9&quot;,\n  &quot;password&quot;: &quot;abcd1234&quot;,\n  &quot;nickname&quot;: &quot;skyer9&quot;\n}\n\nPOST http:\/\/localhost:8080\/api\/reissue\n{\n  &quot;token&quot;: &quot;refreshToken&quot;\n}\n\nGET http:\/\/localhost:8080\/api\/user\nBearer accessToken\n\nGET http:\/\/localhost:8080\/api\/user\/{username}\nBearer accessToken<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Spring Security + JWT Spring Security \uc640 JWT \ub97c \uc774\uc6a9\ud574 API \uc811\uadfc\uc81c\uc5b4\ub97c \uad6c\ud604\ud574 \ubd05\ub2c8\ub2e4. \ud30c\uc77c\uc758 \uc218\ub294 \ub9ce\uc9c0\ub9cc \uc2e4\uc81c\ub85c\ub294, JwtAuthenticationFilter \ub97c \ud638\ucd9c\ud558\ub294 \ubd80\ubd84\uacfc JwtAuthenticationFilter \uc758 \ub0b4\ubd80 \uc791\ub3d9\ub9cc \ud655\uc778\ud558\uba74 Security \uac00 \uc791\ub3d9\ud558\ub294 \ubc29\uc2dd\uc744 \ub300\ubd80\ubd84 \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ubc16\uc5d0 \ud1a0\ud070\uc744 \uc0dd\uc131\ud558\uace0 \uac31\uc2e0\ud558\ub294 \ubd80\ubd84\uc740 RestController \uc5d0\uc11c \uc9c1\uc811 \ud638\ucd9c\ud558\ubbc0\ub85c \uc774\ud574\ud558\uae30 \uc27d\uc2b5\ub2c8\ub2e4. \uc8fc\uc758!!!! secret \ub9cc \uc788\uc73c\uba74 \ub204\uad6c\ub098 \ud1a0\ud070\uc744 \ub9cc\ub4e4\uc5b4\ub0bc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=8737\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-8737","post","type-post","status-publish","format-standard","hentry","category-spring-boot"],"_links":{"self":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/8737","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=8737"}],"version-history":[{"count":24,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/8737\/revisions"}],"predecessor-version":[{"id":8845,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/8737\/revisions\/8845"}],"wp:attachment":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=8737"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=8737"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=8737"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}