1. JWT 로그인 환경 설정
- 이전에 작성한 token 프로젝트에 로그인 관련 코드를 추가한다
1) 로그인
- oauth 패키지를 만들고 PrincipalDetails 클래스를 생성한다
- UserDetails 인터페이스를 implements 한다
- Member 객체와 생성자를 추가한다
package com.json.token.oauth;
import com.json.token.model.Member;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
@Data
public class PrincipalDetails implements UserDetails {
private Member member;
public PrincipalDetails(Member member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
member.getRoleList().forEach(n -> {
authorities.add(() -> n);
});
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- repository 패키지를 만들고 인터페이스 MemberRepository 클래스를 생성한다
- username을 기준으로 검색할 수 있도록 findByUsername()메서드를 생성한다
package com.json.token.repository;
import com.json.token.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
public Member findByUsername(String member);
}
- oauth 패키지에 PrincipalDetailsService 클래스를 생성한다
package com.json.token.oauth;
import com.json.token.model.Member;
import com.json.token.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member memberEntity = memberRepository.findByUsername(username);
return new PrincipalDetails(memberEntity);
}
}
- Jwt로 로그인 처리를 하기 위해 filter 패키지에 JwtAuthenticationFilter 클래스를 생성한다
package com.json.token.filter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("login 시도");
return super.attemptAuthentication(request, response);
}
}
- JwtAuthenticationFilter 클래스를 실행하기 위해 SecurityConfig.java를 수정한다
- 이전에는 .addFilter(new JwtAuthenticationFilter(authenticationManager())) 메서드를 통해 쉽게 처리할 수 있었다
- 하지만 WebSecurityConfigureAdapter가 deprecated되면서 내부에 클래스를 만들어주거나 별도의 처리가 필요하다
- CustomDsl 내부 클래스를 만들어 .addFilter(new JwtAuthenticationFilter(authenticationManager())) 처리를 통해 해당 필터를 적용한다
//주석처리 http.addFilterBefore(new FirstFilter(), BasicAuthenticationFilter.class); // 추가
......
.and()
.formLogin().disable()
.httpBasic().disable()
.apply(new CustomDsl()) // 추가
.and() //추가
.authorizeRequests()
......
return http.build();
}
//추가 코드
public class CustomDsl extends AbstractHttpConfigurer<CustomDsl, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
builder
.addFilter(corsFilter)
.addFilter(new JwtAuthenticationFilter(authenticationManager));
}
}
http.addFilterBefore(new FirstFilter(), BasicAuthenticationFilter.class);
필히 주석처리 한다
안할 경우 postman에서 결과가 나오지 않는다
2) 회원가입
- 회원가입 로직 처리를 위해 RestApiController 클래스에 코드를 추가한다
@RestController
@RequiredArgsConstructor // 추가
public class RestApiController {
// 추가
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
...
// 추가
@PostMapping("/join")
public String join(@RequestBody Member member) {
member.setPassword(bCryptPasswordEncoder.encode(member.getPassword()));
member.setRoles("ROLE_USER");
memberRepository.save(member);
return "회원 가입 완료";
}
}
- BCryptPasswordEncoder 빈 등록을 TokenApplication 클래스에 한다
- 코드를 작성하면 자동으로 두개의 클래스로 분리되어진다
- BCryptPasswordEncoder 순환 참조를 막기 위해 JwtApplication에 @Bean으로 등록한다
package com.json.token;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootApplication
public class JwtApplication {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(JwtApplication.class, args);
}
}
- 로그인을 진행하기 위하여 postman에서 POST를 통해 회원가입 요청을 보낸다
3) 회원가입 후 로그인 시도
- 실행 후 postman으로 body에 username, password를 포함하여 /login post 요청을 합니다.
- 지금은 username과 password 처리를 하지 않았기 때문에 오류가 발생한다
- intelliJ 로그를 확인해보면 login 시도(jwAuthenticationFilter가 정상적으로 적용) 후에 오류가 발생하는 것을 확인할 수 있다
- 이를 해결하기 위해 jwtAuthenticationFilter에서 정상적인 로그인 시도를 하고 JWT 토큰을 만들어서 응답해주는 작업을 진행을 해 본다
4) 사용자 정보 확인
- request 객체에 사용자 정보가 담겨져 있는데 확인해 보도록 한다
- JwtAuthenticationFilter 클래스의 코드를 추가한다
- request에 담긴 정보를 가져와서 출력하는 코드를 추가한다
try {
BufferedReader br = request.getReader();
String input = null;
while((input = br.readLine()) != null) {
System.out.println(input);
}
} catch (IOException e) {
e.printStackTrace();;
}
- 재실행 후 postman으로 /login POST 요청을 하면 login 시도 문구 다음에 입력된 값을 확인할 수 있다
- 아래에 오류가 뜨는 것은 정상이다
- postman으로 /login POST 요청을 할 때 Body에 사용자 정보를 raw, JSON 형태로 보내본다
- 콘솔에 출력되는형태가 변경되었음을 확인할 수 있다
5) 로그인 처리
- JwtAuthenticationFilter 클래스의 attemptAuthentication 메서드 코드를 수정한다
- 기존의 코드를 아래의 코드로 대체한다
- PrincipalDetailsService의 loadUserByUsername() 메서드가 실행된 후 정상 작동된다면 authentication이 return된다
- 로그인이 정상적으로 된다면 authentication 객체를 session에 저장한다
- attemptAuthentication 메서드가 정상적으로 작동하게 되면 successfulAuthentication 메서드를 실행한다
: 해당 메서드에서 JWT 토큰을 만들어서 요청한 사용자에게 JWT 토큰을 응답으로 돌려준다
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
ObjectMapper om = new ObjectMapper();
Member member = om.readValue(request.getInputStream(), Member.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(member.getUsername(), member.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
return authentication;
} catch (IOException e) {
e.printStackTrace();;
}
return null;
}
- 재실행 후 postman에서 login을 요청해도 정상 작동하지 않는다
6) JWT 토큰 생성 후 전달
- JwtAuthenticationFilter 클래스의 successfulAuthentication 메서드를 오버라이드 한다
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication");
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
String jwtToken = JWT.create()
.withSubject("cos jwt token")
.withExpiresAt(new Date(System.currentTimeMillis() + (60 * 1000 * 10)))
.withClaim("id", principalDetails.getMember().getId())
.withClaim("username", principalDetails.getMember().getUsername())
.sign(Algorithm.HMAC512("cos_jwt_token"));
response.addHeader("Authorization", "Bearer " + jwtToken);
}
- 재실행 후 postman에서 join으로 먼저 등록하고 login을 POST로 요청한다
- join 후 login 시도 시 Body에는 어떤 결과도 반영되지 않는다
- 하지만, Headers에 값을 확인해보면 Authorization - Bearer 값이 들어와 있는 것을 확인할 수 있다
- 로그인을 성공 했을 때 JWT 토큰을 생성한다
- 클라이언트에 JWT 토큰을 응답을 통해 보낸다
- 요청할 때마다 JWT 토큰을 가지고 요청하게 되고 서버는 JWT 토큰이 유효한지 필터를 통해 판단하기만 하면 된다
2. JWT 인증권한 테스트
- Security filter 에서 권한 및 인증이 필요한 주소를 요청 시 BasicAuthenticationFilter를 반드시 진행하게 되어있다
- 권한이나 인증이 필요하지 않을 경우 BasicAuthenticationFilter는 적용되지 않는다
1) 인증 권한 테스트
- 인증 권한이 필요한 url에 접속할 때 특정 필터가 적용되도록 테스트 해 본다
- filter 패키지에 JwtAuthorizationFilter 클래스를 생성한다
- doFilterInternal 메서드는 인증이나 권한이 필요한 주소 요청이 있을 때마다 해당 필터를 통하게 되어 있다
package com.json.token.filter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("인증이나 권한이 필요한 주소 요청 됨.");
super.doFilterInternal(request, response, chain);
}
}
- SecurityConfig 클래스에 해당 필터를 적용한다
- 필터가 적용되면 /api/v1/user , /api/v1/admin 등 접속 권한이 필요한 url에는 모두 로그가 출력되는 것을 확인할 수 있다
//코드 추가
.addFilter(new JwtAuthorizationFilter(authenticationManager));
2) Token을 통한 인증 처리
- JwtAuthorizationFilter 클래스에 코드를 추가 및 수정한다
- memberRepository가 필요하여 생성자에 추가했으므로 SecurityConfig에도 수정이 필요하다
- 요청에 Authorization header 값을 가져와서 토큰을 가지고 있는지 체크한다
- 토큰이 있더라도 verify() 메서드를 통해 username이 있는지 확인하여 서비스에 등록된 유저인지 확인한다
- username이 정상적으로 확인된다면 UsernamePasswordAuthenticationToken을 통해 authentication을 설정한다
package com.json.token.filter;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.json.token.model.Member;
import com.json.token.oauth.PrincipalDetails;
import com.json.token.repository.MemberRepository;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
//코드 추가
private MemberRepository memberRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, /*코드 추가*/MemberRepository memberRepository) {
super(authenticationManager);
//코드 추가
this.memberRepository = memberRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("인증이나 권한이 필요한 주소 요청 됨.");
//코드 추가
String jwtHeader = request.getHeader("Authorization");
if(jwtHeader == null || !jwtHeader.startsWith("Bearer")) {
chain.doFilter(request, response);
return;
}
String jwtToken = jwtHeader.replace("Bearer ", "");
String username = JWT.require(Algorithm.HMAC512("cos_jwt_token")).build().verify(jwtToken).getClaim("username").asString();
if (username != null) {
Member memberEntity = memberRepository.findByUsername(username);
PrincipalDetails principalDetails = new PrincipalDetails(memberEntity);
Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
super.doFilterInternal(request, response, chain);
}
}
- SecurityConfig 클래스를 수정한다
- JwtAuthorizationFilter를 수정한 후 실행 시 에러가 발생하는 부분을 수정한다
private final MemberRepository memberRepository;
.......
.addFilter(new JwtAuthorizationFilter(authenticationManager, memberRepository)); // 수정
3) 로그인 및 권한 테스트
- 권한을 확인하기 위해 RestApiController 클래스에 url을 추가한다
// 추가
@GetMapping("/api/v1/user")
public String user() {
return "user";
}
// 추가
@GetMapping("/api/v1/admin")
public String admin() {
return "admin";
}
- Postman에 post 요청으로 http://localhost:8080/join에 회원가입을 요청한다
- 회원가입 정보를 /login에 post 요청으로 로그인을 시도한다
- Body의 내용은 공백이 정상이다
- Header에 Authorization 값이 생성되었으며 그 값을 복사한다
- postman에서 http://localhost:8080/api/v1/user/ 등 권한이 필요한 url에 Header에 복사한 Bearer token 값을 넣고 Get 요청을 보낸다
- 'user'라는 결과를 응답한다
- 다른 권한 url인 admin, manager 등에서는 403 (접근 권한 없음)이 출력된다
'Spring Security' 카테고리의 다른 글
Spring Security - OAuth2 인증(Authentication) (0) | 2022.07.31 |
---|---|
Spring Security - Filter & FilterChain (0) | 2022.07.30 |
Spring Security - JWT Bearer 인증 (0) | 2022.07.27 |
Spring Security - JWT 인증(Authentication) (0) | 2022.07.27 |
Spring Security - 개요 (0) | 2022.07.22 |