끄적끄적 코딩일지

[Spring] Spring Security + JWT 본문

Spring

[Spring] Spring Security + JWT

BaekGyuHyeon 2022. 6. 8. 11:00

JWT란?

Json Web Token의 약자로 Json 포맷을 사용하여 사용자의 속성을 저장하는 Claim기반의 Web Token이다. 

JWT는 암호화 방식과 type등에 대한 정보가 있는 Header, 사용자에 대한 정보(Claim)와 Token에 대한 정보를 담고있는 PayLoad, 

Token을 인코딩하거나 유효성을 검증할 때 쓰는 암호화 코드인 Signatured으로 구성되어 있다.


Spring Security에서 JWT 사용하기

Spring security에서 JWT를 사용하려면 기존의 인증방식대신 Filter을 써서 Jwt  발급, 해석을 해야한다.

전체적인 흐름을 정리하자면

로그인 -> 입력한 Id, PW으로 사용자 조회(database) -> 해당 사용자 정보로 Jwt 발급 -> Jwt를 Cookie에 저장 

 

모든 요청에 대해 -> Cookie 의 Jwt 요청 -> Jwt가 Cookie에 있을경우 해독을 통해 사용자 정보 추출 -> 추출한 정보로 Authentication(인증 객체) 생성 및 SecurityContextHolder에 저장

 

SecurityContextHolder에 인증객체를 저장하면 @AuthenticationPrincipal이나 Thymeleaf의 sec등을 사용할 수 있다.

 

이번 글에서 사용할 사용자 Entity는 다음과 같다.

@Getter
@Setter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column
    private String nickName;

    @Column
    private String name;

    @Column(unique = true)
    private String loginId;

    @Column
    private String loginPw;
}

쿠키를 만들고 조회하는 class

public class CookieService {

    public static Cookie createCookie(String cookieName, String value){
        Cookie token = new Cookie(cookieName,value);
        token.setHttpOnly(true);
        token.setMaxAge((int) 2 * 360 * 1000);
        token.setPath("/");
        return token;
    }
    public static Cookie deleteCookie(String cookieName){
        Cookie token = new Cookie(cookieName,"");
        token.setHttpOnly(true);
        token.setMaxAge(0);
        token.setPath("/");
        return token;
    }

    public static Cookie getCookie(HttpServletRequest req, String cookieName){
        final Cookie[] cookies = req.getCookies();
        if(cookies==null) return null;
        for(Cookie cookie : cookies){
            if(cookie.getName().equals(cookieName))
                return cookie;
        }
        return null;
    }
        // 로그아웃시 해당 cookie를 삭제하도록 하자
    public static void resetToken(HttpServletResponse response){
        Cookie c = deleteCookie("myJwtToken");
        response.addCookie(c);
    }

}

Step 1. 라이브러리 추가하기

Jwt 관련 기능을 사용하기 위해 라이브러리부터 추가해야한다

Gradle 사용자라면 build.gradle에 추가

dependencies {
    ....
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    ....
}

maven 사용자라면 pom.xml에 추가

<dependencies>
    ....
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    ....
</dependencies>

Step 2. CustomUserDetail 만들기

Jwt를 해독해서 Authentication(인증객체)을 만들때 사용할 UserDetail을 정의하면 된다.

@Getter
public class MemberDetail implements UserDetails {
    private final Member member;
    public MemberDetail(Member member){
        this.member = member;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.EMPTY_LIST;
    }

    @Override
    public String getPassword() {
        return member.getLoginPw();
    }

    @Override
    public String getUsername() {
        return member.getLoginId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

 각 항목에 대한 자세한 설명은 이전글을 참고하자

2022.06.04 - [Spring] - [Spring 기초] Spring Security 사용하기

 

[Spring 기초] Spring Security 사용하기

Spring Security란? Spring Web에서 사용자에 대한 인증과 권한등을 관리하고 주요 공격으로부터 어플리케이션을 보호하는 프레임워크이다. 명령형과 반응형 어플리케이션 모두에서 작동 가능하기때

blablacoding.tistory.com


Step 3. JwtProvider 만들기

사용자 정보로 Jwt를 발급하고, Jwt를 해독해서 사용자 정보를 추출하거나 유효성을 검증하고, Authentication을 만드는등 Jwt관련 기능을 모아서 하나의 class으로 만들어서 Bean으로 등록하도록 하자.

 

@Component
public class JwtTokenProvider {
    private final String JWT_SECRET = Base64.getEncoder().encodeToString("비밀 키값".getBytes()); //Jwt암호화와 해독에 필요한 키값 정의
    private final long ValidTime = 1000L * 60 * 60; // 토큰의 유효시간 정의(millisec)
    private MemberRepo repo; // 사용자 정보 repository
    
    @Autowired
    public JwtTokenProvider(MemberRepo repo){
        this.repo = repo;
    }

    // 사용자 정보를 Payload에 담기 + 토큰 발급
    public String generateToken(Member m) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("loginId",m.getLoginId());
        claims.put("loginPw",m.getLoginPw());
        claims.put("id",m.getId());
        return doGenerateToken(claims, "id");
    }
    // 토큰 발급 로직
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setHeaderParam("typ","JWT")
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + ValidTime))
                .signWith(SignatureAlgorithm.HS256, JWT_SECRET)
                .compact();
    }
    
    // UserDetail으로 Autentication(인증객체) 만들기
    public Authentication getAuthentication(MemberDetail userDetails) {
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }
    
    // 토큰을 해독하여 사용자 정보 찾기 및 UserDetail 생성
    public MemberDetail getMemberDetail(String token){
        return new MemberDetail(repo.findById(Long.parseLong(this.getUserPk(token))));
    }
   
    // 토큰에서 회원 정보 PK 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody().get("id").toString();
    }

    // 토큰의 유효성 + 만료일자 확인
    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

Step 4-1. 로그인 Filter 만들기

Jwt를 사용하기 위해 spring으로 오는 모든 요청에 대해 검사를 해야할 필요가 있다. 이때 사용하는것이 fiilter이다.

 

먼저 로그인에 대해 필터를 만들도록 하자

@Component
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    private final MemberRepo repo;
    private final JwtTokenProvider provider;

    // 로그인 시도시 해당 attemptAuthentication 실행
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String id = request.getParameter("id");
        String pw = request.getParameter("pw");
        Member member = repo.findByLoginIdAndLoginPw(id,pw); //Id와 Pw으로 사용자 정보 조회
         if(member != null) {
            // 사용자 정보가 조회되면 해당 정보로 jwt 생성 및 쿠키 저장 및 인증 객체 재공
            String token = provider.generateToken(member);
            Cookie tokenCookie = CookieService.createCookie("myJwtToken",token);
            response.addCookie(tokenCookie);
            return provider.getAuthentication(new MemberDetail(member));
        }
        return super.attemptAuthentication(request, response);
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        response.sendRedirect("로그인 성공시 이동할 페이지 uri");
    }
}

Step 4-2. Jwt 인증 Filter 만들기

@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    private JwtTokenProvider provider;

    @Autowired
    public JwtAuthFilter(JwtTokenProvider provider){
        this.provider=provider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // jwt 쿠키 조회
        Cookie cookie = CookieService.getCookie(request, "myJwtToken");

        // Jwt cookie가 있는지 확인
        if (cookie != null) {
            String jwtToken = cookie.getValue();
            // 해당 토큰의 유효성 검사
            if (provider.validateToken(jwtToken)) {
                // 토큰을 해독하여 유저 정보 조회
                MemberDetail detail = provider.getMemberDetail(jwtToken);
                if(detail.getMember() != null) {
                    Authentication authentication = provider.getAuthentication(detail);
                    // SecurityContext 에 Authentication 객체를 저장
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        chain.doFilter(request, response);
    }
}

Step 5. Spring Security Configuration 만들기

여기까지 진행되었으면 마지막으로 spring security에서 해당 filter을 사용하도록 조작만 하면 된다.

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{


    private final LoginFilter loginFilter;
    private final JwtAuthFilter jwtfilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        loginFilter.setFilterProcessesUrl("/login"); // 로그인 process url 경로 설정
        http.csrf().disable();
        // spring security는 인증정보를 sessionstorage에 저장한다.
        // jwt를 사용할 경우 client가 인증정보를 저장하고 있으므로 sessionstorage를 비활성화 한다.
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); 
        // 필터 추가
        http.addFilterBefore(jwtfilter, UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(loginfilter, UsernamePasswordAuthenticationFilter.class);
        // 나머지 설정(sample)
        // 로그인 process는 인증없이도 요청할 수 있도록 허용해야한다.
        http
                .authorizeRequests()
                .antMatchers("/loginpage").permitAll()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutUrl("/logout")
                .deleteCookies("myJwtToken").permitAll()
    }

}

요즘은 jwt 토큰을 header에 저장하고 있다던데 따로 공부를 해야할것 같다.