서버로 들어오는 접근을 처리하여 접근에 대한 권한을 보고 나누는 역할
@EnableWebSecurity // 0
public class Security Config extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf.disable() // 1
.and()
.authroizeRequests() // 2
.antMatchers("/").permitAll() // 3
.antMatchers("api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated() // 4
.and()
.logout.logoutSuccessUrl("/") // 5
[JWT 로그인 스크랩 블로그]
출처 : https://velog.io/@jkijki12/Spirng-Security-Jwt-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
<인증과정>
1. Http Request가 서버로 넘어온다.
2. 가장먼저 AuthenticationFilter가 요청을 낚아챈다.
3. AuthenticationFilter에서 Request의 Username, password를 이용하여 UsernamePasswordAuthenticationToken을 생성한다.
4. 토큰을 AuthenticationManager가 받는다.
5. AuthenticationManager는 토큰을 AuthenticationProvider에게 토큰을 넘겨준다.
6. AuthenticationProvider는 UserDetailsService로 토큰의 사용자 아이디(username)을 전달하여 DB에 존재하는지 확인한다. 이 때, UserDetailsService는 DB의 회원정보를 UserDetails 라는 객체로 반환한다.
7. AuthenticationProvider는 반환받은 UserDetails 객체와 실제 사용자의 입력정보를 비교한다.
8. 비교가 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를 실행한다.)
Security, Filter 관련 설정
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class
WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
//https://velog.io/@cminmins/Spring-Status-401-error
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/jat/users/test").authenticated()
.anyRequest().permitAll()
.and()
.addFilterBefore(new JwtAuthenticateFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
config 파일에서 설정한 필터 클래스, 요청을 받아서 토큰이 유효한지 처리한다.
@RequiredArgsConstructor
public class JwtAuthenticateFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//헤더에서 JWT를 받아옴
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 기간이 유효한 토큰인지 확인
if (token != null && jwtTokenProvider.validateToken(token)){
//토큰이 유효하면 토큰으로부터 유저정보 받아옴.
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext에 Authentication 객체를 저장함.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
요청에 들어오는 유저의 PK (Long) 타입을 이용하여 JWT 생성 및 확인, 발급
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private final UserService userService;
// 23.06.27 나중에 토큰 유효시간 바꿀것
private long tokenValidTime = 1*(1000*60*60*24*365);
/*
//https://superbono-2020.tistory.com/186
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}*/
// 23.06.27 나중에 구매자, 판매자에 따른 Role 설정 해줘야함
public String createJwt(int userIdx){
Date now = new Date();
return Jwts.builder()
.setHeaderParam("type", "jwt")
.claim("userIdx", userIdx) // jwt 토큰 내의 payload 정보 (key, value)
.setIssuedAt(now) //토큰 발행 시간 정보
.setExpiration(new Date(System.currentTimeMillis()+ 1*tokenValidTime)) // 토큰 유효기간
.signWith(SignatureAlgorithm.HS256, Secret.JWT_SECRET_KEY) // 사용할 암호화 알고리즘, signature에 들어갈 secret값 세팅
.compact();
}
// JWT 토큰에서 인증정보 조회
public Authentication getAuthentication(String token){
UserDetails userDetails = userService.loadUserByUserIdx(this.getUserId(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
//토큰에서 회원정보 추출
public Long getUserId(String token){
return new Long(Jwts.parser().setSigningKey(Secret.JWT_SECRET_KEY).parseClaimsJws(token).getBody().get("userIdx", Integer.class));
}
// Request의 Header에서 token 값을 가져옴 "X-ACCESS-TOKEN" : "TOKEN 값"
public String resolveToken(HttpServletRequest request){
return request.getHeader("X-ACCESS-TOKEN");
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(Secret.JWT_SECRET_KEY).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
UserDetails 인터페이스 상속
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserAuthentication implements UserDetails {
private Long sellerIdx;
private String uid;
private String password;
@ElementCollection(fetch = FetchType.EAGER)
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 getPassword() {
return password;
}
@Override
public String getUsername() {
return uid;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
컨트롤러에서 의도한데로 jwt토큰이 발행되는것을 확인할 수 있다.
WebSecurityConfig에서 설정한 "/jat/users/tests" URI는 인가된 접근이 필요
올바르게 요청에 대한 응답이 오는것을 확인할 수 있다.
헤더에 유효하지않은 JWT를 넣는다면??
(예외처리를 하지 않아서 코드가 500으로 뜨지만, 오류가 나는것을 확인)
https://jwt.io/ 서버에서 응답으로 온 JWT를 복호화하였더니 올바른 Idx를 확인 가능.
ROLE 적용하여 인가된 접근 시도 (23.07.03 추가)
Merchandisers 유저에게 ROLE_SELLER라는 권한을 부여하였음.
DB 테이블에도 role 칼럼을 만들어서 회원가입시 권한을 저장하게됨.
WebSecurityConfig 클래스
DB User테이블에는 role 칼럼에 값이 "ROLE_SELLER"로 들어가 있음
JwtTokenProvider 클래스
SellerService
SellerDao
SellerController
2-1) 올바른 권한부여를 하지않고 API 요청
2-2) 올바른 권한 부여 후 API 요청 (잘 동작하는 모습)