JWT (Json Web Token) + Spring Security ์ธ์ฆ

GEONNYยท2024๋…„ 9์›” 9์ผ
0

Building-API

๋ชฉ๋ก ๋ณด๊ธฐ
24/28
post-thumbnail

๐Ÿ“ŒJWT ?

JWT (Json Web Token) ์€ RFC 7519 ์›น ํ‘œ์ค€์œผ๋กœ ์ง€์ •๋œย JSON ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ† ํฐ ์ž์ฒด์— ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” Web Token ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ธ์ฆ ๋ฐฉ์‹๋“ค์— ๋น„ํ•ด ๊ฐ€๋ณ๊ณ  ๊ฐ„ํŽธํ•ด์„œ ์œ ์šฉํ•œ ์ธ์ฆ๋ฐฉ์‹ ์ž…๋‹ˆ๋‹ค.

๐Ÿ“ŒJWT ๊ตฌ์กฐ

JWT๋Š” Header, Payload, Signature ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Header : Signature๋ฅผ ํ•ด์‹ฑํ•˜๊ธฐ์œ„ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •๋ณด.
Payload : ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฃผ๊ณ ๋ฐ›๋Š” ์‹œ์Šคํ…œ์—์„œ ์‹ค์ œ ์‚ฌ์šฉ๋  ๋ฐ์ดํ„ฐ ์ •๋ณด.
Signature : ํ† ํฐ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์œ„ํ•œ ๋ฌธ์ž์—ด. ํ—ค๋”์™€ ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋น„๋ฐ€ํ‚ค๋กœ ์„œ๋ช…ํ•œ ๊ฐ’. ์ด ์„œ๋ช…์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ† ํฐ์„ ๋ณ€์กฐํ•  ์ˆ˜ ์—†๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ŒJWT์˜ ์žฅ์ 

์ž์ฒด ํฌํ•จ(Self-contained)
JWT๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด์™€ ๊ฐ™์€ ํด๋ ˆ์ž„์„ ์ž์ฒด์ ์œผ๋กœ ํฌํ•จํ•˜๋ฏ€๋กœ ๋ณ„๋„์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์กฐํšŒ ์—†์ด ์ •๋ณด ํ™•์ธ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

ํ™•์žฅ์„ฑ
JWT๋Š” ๋‹ค์–‘ํ•œ ํด๋ ˆ์ž„์„ ๋‹ด์„ ์ˆ˜ ์žˆ์–ด ์œ ์—ฐํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ ์ˆ˜ํ‰ ํ™•์žฅ์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.

ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ
์„œ๋ฒ„์— ์ƒํƒœ ์ •๋ณด๋ฅผ ์ €์žฅํ•  ํ•„์š” ์—†์ด, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ† ํฐ์„ ๋ณด์œ ํ•ด ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.(Stateless)

์ธ์ฝ”๋”ฉ
Base64 URL Safe Encoding์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— URL, Cookie, Header ๋ชจ๋‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ŒJWT์˜ ๋‹จ์ 

ํ† ํฐ ํฌ๊ธฐ
ํŽ˜์ด๋กœ๋“œ์— ํฌํ•จ๋œ ์ •๋ณด๊ฐ€ ๋งŽ์•„์ง€๋ฉด ํ† ํฐ ํฌ๊ธฐ๊ฐ€ ์ปค์ง‘๋‹ˆ๋‹ค.

๋ฌดํšจํ™” ์–ด๋ ค์›€
JWT๋Š” ๋ฐœ๊ธ‰ ํ›„ ๋งŒ๋ฃŒ ์ „๊นŒ์ง€๋Š” ๋ฌดํšจํ™”๊ฐ€ ์–ด๋ ค์›Œ, ์ฆ‰์‹œ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ๋‚˜ ํ† ํฐ ํ•ด์ง€ ๋“ฑ์˜ ์ž‘์—…์ด ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ๋‹จ์  ๋ณด์™„

ย JWT์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ์„ธ์…˜๊ณผ์˜ ์ฐจ์ด์—์„œ ์•Œ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์„ธ์…˜๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ ‘๊ทผ์— ๋Œ€ํ•œ ์ƒํƒœ์ •๋ณด๋ฅผ ์„œ๋ฒ„์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๋ฅผ ์ƒ๋Œ€์ ์œผ๋กœ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ Stateless ํ•˜๋‹ค๊ณ  ํ‘œํ˜„ ํ•ฉ๋‹ˆ๋‹ค. ย ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ์ƒํƒœ์ •๋ณด๋ฅผ ์„œ๋ฒ„์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Access Token์ด ํƒˆ์ทจ ๋˜์—ˆ์„ ๋•Œ Token์ด ๋งŒ๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€๋Š” ํƒˆ์ทจ๋œ Token์— ๋Œ€ํ•ด์„œ ์–ด๋– ํ•œ ์กฐ์น˜๋ฅผ ์ทจํ•  ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ Access Token์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์งง๊ฒŒ ์„ค์ •์„ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ“Refresh Token

Access Token์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์งง๊ฒŒ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉด ์‚ฌ์šฉ์ž๋Š” ๊ทธ๋งŒํผ ์ž์ฃผ ๋กœ๊ทธ์ธ์ด๋ผ๋Š” ๊ณผ์ •์„ ๊ฑฐ์ณ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์ดํŠธ์ธ๋ฐ ๊ณ„์† ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ๊ฐ€ ๋œ๋‹ค๋ฉด ์‚ฌ์šฉ์ž๋Š” ์—„์ฒญ ๋ถˆํŽธํ•ด ํ•˜๊ฒ ์ฃ . ๊ทธ๋ž˜์„œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ๊ธด Refresh Token์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Access Token์ด ๋งŒ๋ฃŒ ๋˜์—ˆ์„ ๋•Œ ๋กœ๊ทธ์ธ ์‹œ ๋ฐ›์€ Refresh Token์œผ๋กœ Token์˜ ๊ฐฑ์‹ ์„ ์š”์ฒญํ•˜๊ณ , ๊ฐฑ์‹ ๋œ ํ† ํฐ์œผ๋กœ ์ด์ „์— ์š”์ฒญํ•œ API๋ฅผ ๋‹ค์‹œ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์ด์ฃ .

๐Ÿ“Access & Refresh Token ๋งŒ๋ฃŒ ๊ธฐํ•œ

์ผ๋ฐ˜์ ์œผ๋กœ Access Token์€ 1๋ถ„ ~ 30๋ถ„ ์ •๋„, Refresh Token์€ 1์ฃผ or ๊ทธ ์ด์ƒ ์ •๋„์˜ ๋งŒ๋ฃŒ์‹œ๊ฐ„์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋ณด์•ˆ ์ •์ฑ…์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Refresh Token ํƒˆ์ทจ

๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ๊ธด Refresh Token ์ด ํƒˆ์ทจ๋˜๋ฉด ๋งŒ๋ฃŒ ์ „ ๊นŒ์ง€๋Š” ๋ณด์•ˆ์ƒ์˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ ํ•  ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ์•ž์—์„œ ๋งํ•œ๊ฒƒ๊ณผ ๊ฐ™์ด Refresh Token์˜ ๊ฒฝ์šฐ์—๋„ Stateless ํ•˜๊ธฐ ๋•Œ๋ฌธ์— Access Token๊ณผ ๋งˆ์ฐฎ๊ฐ€์ง€๋กœ ํƒˆ์ทจ ๋˜์—ˆ์„ ๋•Œ ์„œ๋ฒ„์—์„œ๋Š” ์–ด๋– ํ•œ ์กฐ์น˜๋ฅผ ์ทจํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
๋กœ๊ทธ์ธ ์‹œ ๋ฐœํ–‰ ๋œ Refresh Token ์„ ์ €์žฅ๋งค์ฒด(Database)์— ์ €์žฅํ•˜์—ฌ ํƒˆ์ทจ ํ™•์ธ ์‹œ ์ €์žฅ๋œ Refresh Token์„ ๋ณ€๊ฒฝํ•˜๋ฏ€๋กœ์จ ๊ฐฑ์‹ ์„ ๋ง‰์„ ์ˆœ ์žˆ๊ฒ ์ง€๋งŒ, ์ด๋„ ํƒˆ์ทจ๋ฅผ ํ™•์ธํ–ˆ์„ ๋•Œ์— ํ•œํ•˜์—ฌ ๊ฐ€๋Šฅํ•œ ๋Œ€์ฑ…์ž…๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์— RTR (Refresh Token Rotation) ์„ ์ ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. Refresh Token์„ ํ™œ์šฉํ•˜์—ฌ ๊ฐฑ์‹ ์„ ํ•œ ๋ฒˆ๋งŒ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์„ค์ •ํ•˜๋Š” ๋ฐฉ์‹์ธ๋ฐ์š”. Refresh Token์œผ๋กœ Access Token์„ ๊ฐฑ์‹ ํ•  ๋•Œ Refresh Token๋„ ๊ฐฑ์‹ ํ•˜์—ฌ ์ €์žฅ๋งค์ฒด(Database)์— ์ €์žฅ, ์ด์ „ Refresh Token์œผ๋กœ ๊ฐฑ์‹  ์š”์ฒญ ์‹œ ํƒˆ์ทจ๋œ ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ•˜์—ฌ ์ €์žฅ ๋งค์ฒด์˜ Refresh Token ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

๐Ÿ“Token ํƒˆ์ทจ์— ๋Œ€ํ•œ ๋ณด์•ˆ ๋Œ€์ฑ…

ํ†ต์‹  ์ค‘์— Token์ด ํƒˆ์ทจ๋˜๊ฑฐ๋‚˜ ์กฐ์ž‘๋˜์ง€ ์•Š๋„๋ก HTTPS ์‚ฌ์šฉ.
Token์˜ ๋งŒ๋ฃŒ๊ธฐ๊ฐ„์„ ์งง๊ฒŒ ์„ค์ •.
CSRF(Cross-Site Request Forgery) ๋ฐฉ์ง€, security ์ ์šฉ.

๐Ÿ“ŒExample

SpringBoot 3.3.1, gradle Procject ์˜ˆ์‹œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.
Spring Security ์™€ JWT ๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์˜์กด์„ฑ ์ฃผ์ž…์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“๋กœ๊ทธ์ธ ๋กœ์ง

๐Ÿ“๋กœ๊ทธ์ธ ์ดํ›„ ์„œ๋น„์Šค ๋กœ์ง

๐Ÿ“์˜์กด์„ฑ ์ถ”๊ฐ€

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    
    //์ƒ๋žต..
    
    //JWT
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.6'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.6'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.12.6'

๐Ÿ“Application ์„ค์ •

application.yml

jwt:
  auth-key: ${jwt-key}
  cookie-token-name: "GN_AUT"

๋ณด์•ˆ์„ ์œ„ํ•ด jwt-key ๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋“ฑ๋ก์„ ํ•ฉ๋‹ˆ๋‹ค.
IntelliJ ์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋“ฑ๋กํ•˜๋Š” ๋ฐฉ๋ฒ•์€ shift ๋‘๋ฒˆ ๋ˆŒ๋Ÿฌ edit configurations ์„ ๊ฒ€์ƒ‰ํ•ด ๋‚˜์˜จ ํŒ์—… ์ฐฝ์—์„œ Environment variables ์— ๊ฐ’์„ ๋“ฑ๋กํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. Environment variables ๊ฐ€ ์—†๋‹ค๋ฉด Modify opetions๋ฅผ ๋ˆŒ๋Ÿฌ ์ถ”๊ฐ€ํ•ด ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
jwt-key ๋Š” ์‚ฌ์šฉํ•  ์•Œ๊ณ ๋ฆฌ์ฆ˜์— ๋”ฐ๋ผ ๊ธธ์ด๋ฅผ ๋งž์ถ”์–ด Base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ๊ฐ’์„ ์„ค์ •ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
(Base64 ์ธ์ฝ”๋”ฉ์€ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๋‚˜์˜ค๋Š” ์˜จ๋ผ์ธ ์ธ/๋””์ฝ”๋”๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”.)

๐Ÿ“TokenProvider ์ƒ์„ฑ

Token์˜ ์ƒ์„ฑ, Cookie ์— Token ์ถ”๊ฐ€ ๋ฐ ์ถ”์ถœ, Claim ๋ฐ์ดํ„ฐ ์ถ”์ถœ, Token ์œ ํšจ์„ฑ ๊ฒ€์ฆ, XSS ๊ณต๊ฒฉ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ TokenProvider ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
config.jwt.TokenProvider

@Component
@Slf4j
public class TokenProvider implements InitializingBean {

    @Value("${server.servlet.context-path}")
    public String contextPath;
    @Value("${jwt.auth-key}")
    private String jwtAuthKey;
    @Value("${jwt.cookie-token-name}")
    private String tokenNameInCookie;
    private SecretKey signingKey;
    public final String authClaimName = "AUTH";


    @Override
    public void afterPropertiesSet() {
        this.signingKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(this.jwtAuthKey));
    }

    public TokenResponse createToken(Authentication authentication, Member member) {
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));
        Date now = new Date();
        long dateTime = (now).getTime();
        //1์‹œ๊ฐ„
        long tokenValidityInMilliseconds = 1000 * 3600;
        Date accessValidity = new Date(dateTime + tokenValidityInMilliseconds);
        //24์‹œ๊ฐ„
        long refreshTokenValidityInMilliseconds = 24 * (1000 * 3600);
        Date refreshValidity = new Date(dateTime + refreshTokenValidityInMilliseconds);
        return TokenResponse.builder()
                .accessToken(
                        Jwts.builder()
                                .subject(authentication.getName())
                                .claim(this.authClaimName, authorities)
                                .issuedAt(now)
                                .expiration(accessValidity)
                                .signWith(this.signingKey)
                                .compact())
                .refreshToken(
                        Jwts.builder()
                                .subject(authentication.getName())
                                .claim(this.authClaimName, authorities)
                                .issuedAt(now)
                                .expiration(refreshValidity)
                                .signWith(this.signingKey)
                                .compact())
                .build();
    }

    public String getTokenFromCookie(HttpServletRequest httpServletRequest) {
        Cookie[] cookies = httpServletRequest.getCookies();
        String requestURI = httpServletRequest.getRequestURI().replace(this.contextPath, "");
        if (cookies == null) {
            log.error("No cookies found in request. Request URI: {}", requestURI);
            return null;
        }
        return Arrays.stream(cookies)
                .filter(cookie -> this.tokenNameInCookie.equals(cookie.getName()))
                .map(Cookie::getValue)
                .findFirst()
                .map(this::doXssFilter)
                .orElseGet(() -> {
                    log.error("Access token in cookie does not exist. Request URI: {}", requestURI);
                    return null;
                });
    }

    public void setTokenToCookie(HttpServletResponse httpServletResponse, String accessToken) {
        log.info("Add Token in Cookie.");
        Cookie cookie = new Cookie(this.tokenNameInCookie, accessToken);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        httpServletResponse.addCookie(cookie);
    }

    public String getUserId(String token) {
        return Jwts.parser().verifyWith(this.signingKey)
        	.build().parseSignedClaims(token).getPayload().getSubject();
    }

    public String getClaim(String token, String claimName) {
        return (String) Jwts.parser().verifyWith(this.signingKey)
        	.build().parseSignedClaims(token).getPayload().get(claimName);
    }

    public boolean validateToken(String token) throws ExpiredJwtException {
        try {
            Jwts.parser().verifyWith(this.signingKey).build().parseSignedClaims(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.error("Invalid jwt signature.", e);
        } catch (ExpiredJwtException e) {
            log.error("This token is expired.", e);
            throw e;
        } catch (UnsupportedJwtException e) {
            log.error("This jwt token is not supported.", e);
        } catch (IllegalArgumentException e) {
            log.error("Invalid jwt token.", e);
        } catch (DecodingException e) {
            log.error("JWT token decoding failed", e);
        }
        return false;
    }

    /**
     * XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํŠน์ˆ˜๋ฌธ์ž๋ฅผ HTML ๋กœ ๋ณ€ํ™˜
     */
    private String doXssFilter(String origin) {
        if (StringUtils.isEmpty(origin)) {
            return null;
        }
        return origin.replace("'", "'")
                .replace("\"", """)
                .replace("(", "(")
                .replace(")", ")")
                .replace("/", "/")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("&", "&amp;");
    }

InitializingBean ์„ ๊ตฌํ˜„ํ•˜์—ฌ Bean ๋“ฑ๋ก ํ›„ jwtAuthKey๋ฅผ ์ด์šฉํ•ด SecretKey๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. Access Token์€ 1์‹œ๊ฐ„, Refresh Token์˜ ๋งŒ๋ฃŒ์‹œ๊ฐ„์€ 24์‹œ๊ฐ„์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“JWT filter ์ถ”๊ฐ€

GenericFilterBean์„ ์ƒ์†๋ฐ›์•„ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Spring Security ๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ์ธ์ฆ๊ณผ์ • ์ „์— ์ฒ˜๋ฆฌํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ๋งŒ๋ฃŒ๋œ Token ์— ๋Œ€ํ•ด์„œ๋Š” ๋“ฑ๋ก๋œ ์‚ฌ์šฉ์ž์ธ์ง€, ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ์ฒดํฌํ•  ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
๋กœ๊ทธ์ธ, Swagger ๊ด€๋ จ ์ฃผ์†Œ๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด์„œ Token ๊ฒ€์ฆ์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. Cookie ์—์„œ Access Token์„ ์ถ”์ถœํ•ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ํ•˜๊ณ  ์œ ํšจํ•˜๋‹ค๋ฉด Spring Security์— ์‚ฌ์šฉ์ž ์ธ์ฆ์ •๋ณด๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. Token์ด ์œ ํšจ์‚ฌ์ง€ ์•Š์„ ๊ฒฝ์šฐ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ”๋กœ ErrorResponse๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
config.jwt.JwtFilter

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends GenericFilterBean {

    private final TokenProvider tokenProvider;

    private final List<String> ignoreUris = List.of(SecurityConfig.ignoreUris);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
    		throws ServletException, IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String requestURI = httpServletRequest.getRequestURI().replace(this.tokenProvider.contextPath, "");
        if (!this.ignoreUris.contains(requestURI)
        		&& !requestURI.startsWith("/swagger-") && !requestURI.startsWith("/api-docs")) {
            log.info("Request URI : " + requestURI);
            //1. SessionsCookie ์—์„œ Access Token ์ถ”์ถœ
            String accessToken = tokenProvider.getTokenFromCookie(httpServletRequest);
            TokenValidDto tokenValidDto = new TokenValidDto(false, null, accessToken);

            //2. Token ์œ ํšจ์„ฑ ๊ฒ€์ฆ
            if (StringUtils.isNotEmpty(accessToken)) {
                try {
                    checkTokenValidity(tokenValidDto);
                } catch (ExpiredJwtException e) {
                    // Token ๋งŒ๋ฃŒ ์‹œ ๋ฐ”๋กœ Token ๋งŒ๋ฃŒ ๋ฉ”์‹œ์ง€ ์ „์†ก
                    ResponseUtils.sendResponse(httpServletResponse, ErrorCode.EXPIRED_TOKEN);
                    return;
                }
            }

            //3. Spring Security Authentication token ์ถ”๊ฐ€
            if (tokenValidDto.isValid()) {
                log.info(tokenValidDto.getUserId() + "'s token is valid.");
                Collection<SimpleGrantedAuthority> authorities =
                        Arrays.stream(this.tokenProvider.getClaim(tokenValidDto.getAccessToken()
                        			, this.tokenProvider.authClaimName).split(","))
                              .map(SimpleGrantedAuthority::new).toList();
                Authentication authentication = new UsernamePasswordAuthenticationToken(
                        new User(this.tokenProvider.getUserId(
                        	tokenValidDto.getAccessToken()), "", authorities),
                        tokenValidDto.getAccessToken(),
                        authorities
                );
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        chain.doFilter(request, response);
    }

    private void checkTokenValidity(TokenValidDto tokenValidDto) {
        if (StringUtils.isNotEmpty(tokenValidDto.getAccessToken())
                && tokenProvider.validateToken(tokenValidDto.getAccessToken())) {
            String userId = tokenProvider.getUserId(tokenValidDto.getAccessToken());
            tokenValidDto.setValid(true);
            tokenValidDto.setUserId(userId);
        }
    }
}

common.response.util.ResponseUtils

@Slf4j
public class ResponseUtils {

    public static void sendResponse(ServletResponse response, ResponseCode errorCode) {
        MessageConfig messageConfig = ApplicationContextHolder.getContext()
        	.getBean(MessageConfig.class);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        try (PrintWriter writer = response.getWriter()) {
            writer.write(objectMapper.writeValueAsString(ErrorResponse.builder()
                    .status(errorCode.code())
                    .message(messageConfig.getMessage(errorCode))
                    .build()));
        } catch (IOException e) {
            log.error("Fail to send response.");
        }
    }
}

config.jwt.dto.TokenValidDto

@Getter
@Setter
@AllArgsConstructor
public class TokenValidDto {
    private boolean valid;
    private String userId;
    private String accessToken;
}

common.response.ErrorResponse

@Builder
public record ErrorResponse(
        String status,
        String message,
        @JsonInclude(JsonInclude.Include.NON_NULL)
        String detailMessage
) {
}

๐Ÿ“Spring Security Config ์„ค์ •

JWT ์™€ Spring Security ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋ณด์•ˆ์„ฑ๊ณผ ํ™•์žฅ์„ฑ ์ธก๋ฉด์—์„œ ์žฅ์ ์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ์ ‘๊ทผ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ 401 UNAUTHORIZED, 403 FORBIDDEN ์„ค์ •์„ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๊ณ  ํ•„ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์„œ๋ธ”๋ฆฟ ์ด์ „์— Token์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
Spring Security filter ์ด์ „์— JwtFilter๊ฐ€ ๋™์ž‘ํ•˜๋„๋ก ์ถ”๊ฐ€ํ•˜๊ณ , 401, 403 ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ class ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ exceptionHandling ์„ค์ •์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๐ŸŽˆJwtAuthenticationEntryPoint

401 UNAUTHORIZED ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด AuthenticationEntryPoint์˜ ๊ตฌํ˜„์ฒด JwtAuthenticationEntryPoint ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
config.jwt.JwtAuthenticationEntryPoint

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) {
       log.error("401 UNAUTHORIZED");
       ResponseUtils.sendResponse(response, ErrorCode.NOT_AUTHENTICATION);
   }
}

๐ŸŽˆJwtAccessDeniedHandler

403 FORBIDDEN ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด AccessDeniedHandler์˜ ๊ตฌํ˜„์ฒด JwtAccessDeniedHandler ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

@Component
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) {
        log.error("403 FORBIDDEN");
        ResponseUtils.sendResponse(response, ErrorCode.FORBIDDEN);
    }
}

๐Ÿ“SecurityConfig

PasswordEncoder ์„ค์ •์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์„ค์ •ํŒŒ์ผ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. Bcrypt ์•”ํ˜ธํ™” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ์•„์ง SHA-256 + salt key ์— ๋Œ€ํ•œ ์ˆ˜์š”๋„ ๋งŽ๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๊ธฐ์„œ๋Š” SHA-256 + salt key ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
application.yml

pdkdf2:
  key: geonny.log
  salt-length: 16
  iterations: 256
  algorithm: PBKDF2WithHmacSHA256

config.SecurityConfig

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    public static final String[] ignoreUris = {"/v1/login"};
    private final TokenProvider tokenProvider;
    private final MessageConfig messageConfig;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    private final String[] swaggerUris = {"swagger-ui.html", "/swagger-ui/**", "/api-docs/**"};
    @Value("${pdkdf2.key}")
    private String pdkdf2Key;

    @Value("${pdkdf2.salt-length}")
    private Integer saltLength;

    @Value("${pdkdf2.iterations}")
    private Integer iterations;

    @Value("${pdkdf2.algorithm}")
    private String algorithm;


    @Bean
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("SHA-256", new Pbkdf2PasswordEncoder(
        		  this.pdkdf2Key, this.saltLength, this.iterations, 
                  Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.valueOf(this.algorithm)));
        return new DelegatingPasswordEncoder("SHA-256", encoders);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
                	.disable())
                .authorizeHttpRequests(auth -> {
                    auth
                            .requestMatchers(ignoreUris).permitAll()
                            .requestMatchers(this.swaggerUris).permitAll()
                            .anyRequest().authenticated();
                })
                .exceptionHandling(c ->
                        c.authenticationEntryPoint(this.jwtAuthenticationEntryPoint)
                                .accessDeniedHandler(this.jwtAccessDeniedHandler))
                .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new JwtFilter(tokenProvider), 
                				 UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

๐Ÿ“Login

๐ŸŽˆLoginController

domain.login.LoginController

@RestController
@RequiredArgsConstructor
@Tag(name = "๋กœ๊ทธ์ธ", description = "๋กœ๊ทธ์ธ ์š”์ฒญ")
@RequestMapping("v1")
public class LoginController {

    private final LoginService loginService;
    private final MessageConfig messageConfig;

    @Operation(summary = "๋กœ๊ทธ์ธ ์š”์ฒญ", description = """
            """, operationId = "API-000-01")
    @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE, 
    							   produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ItemResponse<TokenResponse>> getMemberById(
    		@RequestBody @Valid LoginRequest parameter,
            HttpServletResponse httpServletResponse) {
        return ResponseEntity.ok()
                .body(ItemResponse.<TokenResponse>builder()
                        .status(messageConfig.getCode(NormalCode.SEARCH_SUCCESS))
                        .message(messageConfig.getMessage(NormalCode.SEARCH_SUCCESS))
                        .item(loginService.login(httpServletResponse, parameter))
                        .build());
    }
}

๐ŸŽˆLoginService

domain.login.LoginService

@Service
@RequiredArgsConstructor
public class LoginService {

    private final MemberQueryMethodRepository memberQueryMethodRepository;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final TokenProvider tokenProvider;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public TokenResponse login(
    		HttpServletResponse httpServletResponse, LoginRequest parameter) {
        Member member = this.memberQueryMethodRepository.findById(parameter.memberId())
                .orElseThrow(() -> new ServiceException(ErrorCode.NOT_AUTHENTICATION));
        if (!this.passwordEncoder.matches(parameter.password(), member.getPassword())) {
            throw new ServiceException(ErrorCode.NOT_AUTHENTICATION);
        }
        UsernamePasswordAuthenticationToken authenticationToken
        	= new UsernamePasswordAuthenticationToken(parameter.memberId(), 
            										  parameter.password());
        try {
            Authentication authentication = 
            	this.authenticationManagerBuilder.getObject()
                	.authenticate(authenticationToken);
            SecurityContextHolder.getContext()
            	.setAuthentication(authentication);
            TokenResponse tokenResponse = this.tokenProvider
            	.createToken(authentication, member);
            this.tokenProvider.setTokenToCookie(httpServletResponse, t
            									tokenResponse.accessToken());
            return tokenResponse;
        } catch (BadCredentialsException e) {
            throw new ServiceException(ErrorCode.SERVICE_ERROR);
        }
    }
}

๐ŸŽˆCustomUserDetailService

domain.login.CustomUserDetailService
CustomUserDetailService ๋Š” LoginService ์—์„œ AuthenticationManagerBuilder ์— ์˜ํ•ด ํ˜ธ์ถœ๋˜๋ฉฐ ์ธ์ฆ์ •๋ณด๋ฅผ ํฌํ•จํ•œ Security UserDetail ์ •๋ณด๋ฅผ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomUserDetailService implements UserDetailsService {

    private final MemberQueryMethodRepository memberQueryMethodRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberQueryMethodRepository.findById(username)
        		.map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> not found."));
    }

    private User createUser(String username, Member member) {
        if (member.getAuthority() == null) {
            throw new ServiceException(ErrorCode.FORBIDDEN);
        }
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        grantedAuthorities.add(new SimpleGrantedAuthority(member.getAuthority()
        														.getAuthorityCode()));
        log.info(username + "'s Authority : {}", grantedAuthorities);
        return new User(
                member.getMemberId(),
                member.getPassword(),
                grantedAuthorities
        );
    }
}

๐ŸŽˆ์ถ”๊ฐ€ ๋ณ€๊ฒฝ ์‚ฌํ•ญ

common.code.ErrorCode ์ฝ”๋“œ ์ถ”๊ฐ€

EXISTS_DATA("ERR_DT_02"),
SQL_ERROR("ERR_SQ_01"),
NOT_AUTHENTICATION("ERR_AT_01"),
FORBIDDEN("ERR_AT_02"),
EXPIRED_TOKEN("ERR_AT_03");

resources/messages/message.properties ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€

EXISTS_DATA=์ด๋ฏธ ์กด์žฌ๋Š” ๋ฐ์ดํ„ฐ ์ž…๋‹ˆ๋‹ค.
SQL_ERROR=๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.
NOT_AUTHENTICATION=์ž๊ฒฉ์ฆ๋ช…์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.
FORBIDDEN=์ ‘๊ทผ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
EXPIRED_TOKEN=ํ† ๊ทผ์ •๋ณด๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“๊ฒฐ๊ณผ ํ™•์ธ

์ด์ œ Swagger ๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธ์„ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. http://localhost:13713/my-api/swagger-ui/index.html ์— ์ ‘์†ํ•˜์—ฌ ๋กœ๊ทธ์ธ Operation ์„ Try it out ํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽˆ์ž˜๋ชป๋œ Member ์ •๋ณด (Id or Password)

๐ŸŽˆ์˜ฌ๋ฐ”๋ฅธ ์ •๋ณด

๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ Cookie ํ™•์ธ (๊ฐœ๋ฐœ์ž๋ชจ๋“œ > Application > Cookies > domain์ฃผ์†Œ)

๐ŸŽˆ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ ์ •๋ณด

console

This jwt token is not supported.
401 UNAUTHORIZED

๐ŸŽˆ๊ถŒํ•œ ์—†์Œ

Controller method ์— @PreAuthorize("hasRole('ADMIN')") ๊ณผ ๊ฐ™์ด ์ถ”๊ฐ€ํ•˜๋ฉด ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

console

403 FORBIDDEN

๐ŸŽˆToken ๋งŒ๋ฃŒ

TokenProvider ์— tokenValidityInMilliseconds ๋ฅผ ์งง๊ฒŒ ์„ค์ •ํ•˜๋ฉด ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

console

This token is expired.
JWT expired 1159987 milliseconds ago at 2024-09-09T06:01:31.000Z. 
Current time: 2024-09-09T06:20:50.987Z. Allowed clock skew: 0 milliseconds.

๐Ÿ“Œ๊ฐœ์„  ์‚ฌํ•ญ

๐Ÿ“RSA

Example Code ์—์„œ๋Š” Front๋กœ ๋ถ€ํ„ฐ password ๋ฅผ ๋ฐ›์„ ๋•Œ ์•”ํ˜ธํ™”์— ๋Œ€ํ•ด์„œ๋Š” ์ƒ๋žต๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. RSA ๋ฅผ ํ™œ์šฉํ•˜์—ฌ Front ๋กœ RSA Public key ๋ฅผ ์ „๋‹ฌํ•˜๊ณ , Front๋Š” password ๋ฅผ RSA Public key๋กœ ์•”ํ˜ธํ™” ํ•˜์—ฌ ์ „์†ก, API ์„œ๋ฒ„๋Š” Private Key๋กœ ๋ณตํ˜ธํ™” ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ณด์•ˆ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Refresh Token

Refresh Token์„ ํ™œ์šฉํ•˜์—ฌ Access Token ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ Front๋กœ ๋งŒ๋ฃŒ์ฝ”๋“œ๋ฅผ ์ „์†กํ•˜๊ณ , Front ๊ฐ€ ๋งŒ๋ฃŒ ์ฝ”๋“œ๋ฅผ ๋ฐ›์œผ๋ฉด Refresh Token ์„ ์„œ๋ฒ„๋กœ ๋‹ค์‹œ ์ „์†กํ•˜์—ฌ Token ์„ ๊ฐฑ์‹ ํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ๊ณผ ๋ณด์•ˆ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“ŒConclusion

ย ์„ธ์…˜ ๋ฐฉ์‹๋ณด๋‹ค Token ๋ฐฉ์‹์„ ์„ ํ˜ธํ•˜๋Š” ์ด์œ ๋Š” ํ™•์žฅ์„ฑ๊ณผ ๊ฒฝ์ œ์„ฑ ์ž…๋‹ˆ๋‹ค. ์„œ๋น„์Šค๊ฐ€ ํ™•์žฅ๋˜์–ด๋„ ํ† ํฐ์˜ ์œ ํšจ์„ฑ๋งŒ ๊ฒ€์ฆํ•˜๋ฉด ๋˜๊ณ , ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๊ฐ€ ์ƒ๋Œ€์ ์œผ๋กœ ์ ๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋“œ์›จ์–ด์— ๋“œ๋Š” ๋น„์šฉ๋„ ์ค„์ผ ์ˆ˜ ์žˆ์„ ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ์ตœ๋Œ€ํ•œ ๋ณด์•ˆ์ ์ธ ์กฐ์น˜๋“ค์„ ์ทจํ•จ์œผ๋กœ์จ ์•ˆ์ „ํ•˜๊ฒŒ JWT ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“š์ฐธ๊ณ 

๐Ÿ“•GenericFilterBean

GenericFilterBean์€ ํ‘œ์ค€ Filter Interface ์™€ ๋‹ฌ๋ฆฌ Spring Bean ์œผ๋กœ ๊ด€๋ฆฌ๋˜๋ฏ€๋กœ, ํ•„ํ„ฐ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๊ฐ€ Spring Context์— ์˜ํ•ด ๊ด€๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด Spring Container์˜ ์‹œ์ž‘๊ณผ ์ข…๋ฃŒ์— ๋”ฐ๋ผ ํ•„ํ„ฐ๋„ ์ž๋™์œผ๋กœ ์ดˆ๊ธฐํ™” ๋ฐ ์†Œ๋ฉธ๋˜๋ฉฐ, ์ถ”๊ฐ€์ ์ธ ์„ค์ • ์—†์ด Spring bean ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (Spring Bean DI ๊ฐ€๋Šฅ)

profile
Back-end developer

6๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2024๋…„ 9์›” 25์ผ

doXssFilter์˜ origin ๋‘๋ฒˆ์งธ replace("\"", """) ์—์„œ ๋ณ€ํ™˜ํ•˜๋ ค๋Š” ๋ฌธ์ž๋Š” \ ์ธ๊ฐ€์š”, " ์ธ๊ฐ€์š” ๐Ÿค“โ“

1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2024๋…„ 9์›” 26์ผ

์•„๋ž˜ sendResponse ํ•จ์ˆ˜์—์„œ ApplicationContext์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ApplicationContextHolder๋ฅผ ๋ณ„๋„๋กœ ๊ตฌํ˜„ํ•˜์—ฌ Context๋ฅผ ๋ฐ›์•„์˜ค๋Š” ์ด์œ ๊ฐ€ ๋ฌด์—‡์ธ๊ฐ€์š”?

public static void sendResponse(ServletResponse response, ResponseCode errorCode) {
        MessageConfig messageConfig = ApplicationContextHolder.getContext()
        	.getBean(MessageConfig.class);
        // omission...
    }
1๊ฐœ์˜ ๋‹ต๊ธ€
comment-user-thumbnail
2024๋…„ 9์›” 26์ผ

domain.login.LoginService ์—์„œ
ErrorCode๋ฅผ throwํ•  ๋•Œ
throw new ServiceException(ErrorCode.NOT_AUTHENTICATION); ๋ผ๊ณ  ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค -> ์—๋Ÿฌ๋œธ
throw new ServiceException(ErrorCode.NOT_AUTHENTICATION.messageCode()); ์•„๋‹Œ๊ฐ€์šง!!?!?!?

1๊ฐœ์˜ ๋‹ต๊ธ€