티스토리 뷰

spring(boot)

[security]jwt (+react)

수학소년 2022. 11. 16. 22:18

pom.xml

		...
        <!-- spring security -->
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
        ...
        <dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
        ...
        <!-- jwt -->
        <dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>

 

securityConfiguration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	private final JwtTokenProvider jwtTokenProvider;
	
	@Autowired
	public SecurityConfiguration(JwtTokenProvider jwtTokenProvider) {
		this.jwtTokenProvider = jwtTokenProvider;
	}
	
	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {
		
		// REST API는 UI를 사용하지 않으므로 기본설정을 비활성화
		httpSecurity.httpBasic().disable()

		// REST API는 csrf 보안이 필요 없으므로 비활성화
		.csrf().disable()
        
		.cors()

		// JWT Token 인증방식으로 세션은 필요 없으므로 비활성화
		.and()
		.sessionManagement()
		.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

		// 인증없이 허용할 매핑. ex)로그인, 회원가입, error페이지,,,
		.and()
		.authorizeRequests()
		.antMatchers("/v1/api/....", "/v1/api/....",
			"/v1/api/....").permitAll()
		// .antMatchers(HttpMethod.GET, "/product/**").permitAll()
		// .antMatchers("**exception**").permitAll()

		// 권한체크
		.anyRequest().hasAnyRole("ADMIN", "USER")
		//.anyRequest().hasRole("ADMIN")

		// JWT Token 필터를 id/password 인증 필터 이전에 추가
		.and()
		.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
			UsernamePasswordAuthenticationFilter.class);
	}
}

이제 요청마다 JWT Filter를 통하게 된다.

JWT 과정을 살펴보자.

 

JWT 인증 과정

1. 로그인 하면서 jwt 생성

	public SignDto signIn(String email, String password) throws RuntimeException {
		...
		SignDto signDto = SignDto.builder()
        	.token(jwtTokenProvider.createToken(String.valueOf(user.getEmail()), user.getRoles()))
			.build();
		...
	return signDto;
    public String createToken(String email, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(email);
        claims.put("roles", roles);

        Date now = new Date();
        String token = Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + tokenValidMillisecond))
            .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, secret 값 세팅
            .compact();
        return token;
    }

이렇게 만든 token을 dto에 담아서 화면까지 가져가서 sessionStorage에 담는다.

    submit({
      url: "/v1/api/signin",
      body: values,
      success: (res) => {
        if (res.success) {
          sessionStorage.setItem("jwt", res.token);
          router.push('/')
        }
      }
    });

(위 코드는 해당 프로젝트 환경에 맞게 구현한것이에요. 실제로 어디서나 돌아가는 소스는 아니에요.)

이제 token이 필요할때마다 sessionStorage에서 가져와서 쓸수있음.

 

2. http요청

      submit({
        url: "/v1/api/get",
        headers: {
            ...
            "X-AUTH-TOKEN": sessionStorage.getItem("jwt"),
            ...
        }
        body: {
          ...
        },
        success: (res) => {
          // 콜백
        }
      });

headers에 "X-AUTH-TOKEN"라는 key의 값에 token을 담아서 요청 보냄

3. JWT Filter

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest servletRequest,
                                    HttpServletResponse servletResponse,
                                    FilterChain filterChain)
                             throws ServletException,
                                    IOException {
        String token = jwtTokenProvider.resolveToken(servletRequest);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

유효한 token을 받았으면, token으로 부터 사용자 정보를 알아내고 spring security에 담아둔다.

(SecurityContextHolder.getContext().setAuthentication(authentication);)

 

String token = jwtTokenProvider.resolveToken(servletRequest);

	public String resolveToken(HttpServletRequest request) {
		return request.getHeader("X-AUTH-TOKEN");
	}

http요청(HttpServletRequest)에서 header에서  "X-AUTH-TOKEN"라는 key의 값을 가져옴

=> UI에서 http요청시, header에 "X-AUTH-TOKEN"키에 token을 넣어서 보내야 함.

 

jwtTokenProvider.validateToken(token)

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
...
	public boolean validateToken(String token) {
		try {
			Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
			return !claims.getBody().getExpiration().before(new Date());
		} catch (Exception e) {
			return false;
		}
	}

token : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbDAxQGRvby5jb20iLCJyb2xlcyI6WyJST0xFX1VTRVIiXSwiaWF0IjoxNjY4NjA0NjE1LCJleHAiOjE2Njg2MDgyMTV9.6g4Y5_MM8WHvZQL54AL7kDvidP92H954CMBg7ndEc3c" 일경우,

claims : header={alg=HS256},body={sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215},signature=6g4Y5_MM8WHvZQL54AL7kDvidP92H954CMBg7ndEc3c

claims.getBody() : {sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215}

claims.getBody().getExpiration() : Wed Nov 16 23:16:55 KST 2022

 

token을 decode하려면 jwt.io에 접속해서 복붙하면 알 수 있다.

jwtTokenProvider.getAuthentication(token)

	public Authentication getAuthentication(String token) {
		UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
		return new UsernamePasswordAuthenticationToken(userDetails, "",
			userDetails.getAuthorities());
	}
...
	public String getUsername(String token) {
		return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
	}

 

Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) : header={alg=HS256},body={sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215},signature=6g4Y5_MM8WHvZQL54AL7kDvidP92H954CMBg7ndEc3c
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody() : {sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215}
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject() : email01@doo.com

=> 결론은 token으로부터 email(로그인할때 id로 쓴 값)을 가져옴.

 

4. UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    ...

    @Override
    public UserDetails loadUserByUsername(String email) {
        return userRepository.findByEmail(email);
    }
}

그 email로 사용자DB에서 해당 사용자를 조회.

 

5. User

User entity를 만들면서 org.springframework.security.core.userdetails.UserDetails의 구현체로 만듬.

...
public class User implements UserDetails {

	@Id private String email;
	private String password;
	
	...
	
	@ElementCollection(fetch = FetchType.EAGER)
    @Builder.Default
    private List<String> roles = new ArrayList<>();
	
	...

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
	}
	@Override public String getUsername() { return this.email; }
	@Overridepublic boolean isAccountNonExpired() { return true; }
	@Override public boolean isAccountNonLocked() { return true; }
	@Override public boolean isCredentialsNonExpired() { return true; }
	@Override public boolean isEnabled() { return true; }

회원가입 하면서 role값을 받아서 jpa로 save하면 DB에 "ROLE_"이라는 prefix가 붙어서 저장됌.

이후에 SecurityConfiguration에서는 "ROLE_"을 제외한 부분의 role로 권한체크를 함.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함