1. JWT 란?
사용자의 요청에 따라 서버에서 만들어진 암호화된 토큰을 반환하여 사용자 측에 저장
이후 토큰을 사용하여 인증과정을 진행
2. JWT 구조
- Header
-> 어떤 타입의 데이터
어떤 알고리즘을 사용
- Payload(Claims)
- Signature
-> 데이터와 토큰이 위변조 되지 않았음을 증명
3. Spring Security + JWT 구현
Spring Security에서 알아두어야 할 두가지 개념
- Authentication (인증)
Authentication은 주체(principal)의 신원을 증명하는 과정입니다.
- Authorization (인가)
Authorization은 인증을 마친 사용자에게 권한을 부여하여 특정 리소스에 접근할 수 있도록 허가하는 과정입니다.
스프링 시큐리티 위에 JWT를 구현하기 위해서는 시큐리티의 동작원리를 우선 파악하는게 큰 도움이 된다.
JWT 디펜던시 - Maven ( 버전은 다를 수 있음 )
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Spring Security에서 JWT를 쓰기위해서는 Security에서 기본적으로 지원하는 Session 설정을 해제해야한다.
Security는 기본적으로 비인증시 로그인 페이지로 이동하기 때문에 SPA(Single Page App)방식의 구현을 위해서 해제해야한다.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtRequestFilter JwtRequestFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() //Request 요청
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
/*hasRole('ADMIN')을 사용하는 경우 ADMIN Enum에서 ADMIN 대신 ROLE_ADMIN 여야합니다.
hasAuthority('ADMIN')을 사용하는 경우 ADMIN Enum는 ADMIN이어야합니다.
*/
.antMatchers("/**").permitAll()
.and()
.addFilterBefore(JwtRequestFilter, UsernamePasswordAuthenticationFilter.class); //JWT인증필터를 먼저 실행되도록 배치
}
}
- @EnableWebSecurity 어노테이션을 추가
- WebSecurityConfigurerAdapter 클래스 상속
- configure() 메소드 오버라이드 (HttpSecurity 객체 파라미터 받는다.)
- API서버용도로 사용하기 때문에 CSRF보안은 필요없으므로 해제.
- antMatchers() 이용해서 특정 URL 경로는 모두 허용
- exceptionHandling() 이용해서 권한 불충분시 예외처리
Request 요청이 들어올때 처음 마주하는 Filter 만들기
스프링 시큐리티는 기본적으로 제공하는 Filter Chain들이 있고, implement 혹은 override를 통해서 커스텀해서 사용하면 된다. Filter가 중첩 호출되지 않도록 하나의 Request에 한번의 Filter 처리만 될 수 있도록 GenericFilterBean을 상속한 OncePerRequestFilter가 있다. OncePerRequestFilter를 상속하여 구현할 경우 기존 필터의 Method인 doFilter 대신 doFilterInternal 를 구현하면 된다.
JWT 토큰 인증을 Filter에서 걸러서 처리하는 로직을 구현해보자.
@Component
public class JwtRequestFilter extends OncePerRequestFilter{
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException{
String token = request.getHeader("X-AUTH-TOKEN"); //Request Header에서 토큰을 가져옴
if(token != null && jwtUtils.validateToken(token)) {
Authentication authentication = jwtUtils.getAuthentication(token); //토큰이 유효하면 토큰으로부터 유저 정보를 가져옴.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
JWT을 생성하고 관련 기능을 모아 놓은 JWTUtils
@Component
public class JwtUtils {
private String secretKey ="secret";
private long tokenValidTime = 30 * 60 * 1000L;
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
//JWT 토큰 생성
public String createToken(String userPk,String roles) {
Claims claims = Jwts.claims().setSubject(userPk); //JWT Payload에 저장되는 단위
claims.put("roles", roles);
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) //토큰 만료 시간
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
//토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
System.out.println("getAuthentication");
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
}
//토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public Boolean validateToken(String token) {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); //토큰에 담긴 claims 리스트를 가져옴
return !claims.getBody().getExpiration().before(new Date());
//토큰의 만료시간이 현재시간보다 작으면 true
}
}
UserDetailsService를 커스터마이징하여 구현한 JwtUserDetailsService
@Service
public class JwtUserDetailsService implements UserDetailsService{
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
UserVO userVO = userService.getUserIdPassword(username);
if(userVO.getUser_id().equals(username)) {
List<GrantedAuthority> roles = new ArrayList<GrantedAuthority>();
roles.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(userVO.getUser_id(),userVO.getUser_password(),roles); //ArrayList = role
}else {
throw new UsernameNotFoundException("User not found with username: " +username);
}
}
}
Controller 구현
@RestController
@CrossOrigin
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
UserService userService;
@PostMapping("/signUp")
public ResponseEntity<String> signUp(UserVO userVO) throws Exception {
try{
userService.setUser(userVO);
}catch(Exception e) {
e.printStackTrace();
return new ResponseEntity<>("fail",HttpStatus.NOT_ACCEPTABLE);
}
return new ResponseEntity<>("ok",HttpStatus.OK);
}
@RequestMapping(value="/authenticate",method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception{
try{
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(),authenticationRequest.getPassword()));
/*
* authenticate - UsernamePasswordAuthenticationToken을 기본 AuthenticationProvider로 전달
* UserDetailsService를 사용하여 사용자 이름을 기반으로 사용자를 가져오고 해당 사용자의 비밀번호를 인증 토큰의 비밀번호와 비교
* Spring Security는 하나의 실제 AuthenticationManager만 구현
* */
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password",e);
} //id,password 검증 문제있을 경우 throw Exception
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtUtils.createToken(userDetails.getUsername(),"USER"); //유저이름, 권한List를 파라미터로 넣음
return ResponseEntity.ok(new AuthenticationResponse(token));
}
}
authenticationManager.authenticate() - UserDetailsService를 통해 사용자 이름을 기반으로 사용자를 가져오고 해당 사용자의 비밀번호를 인증 토큰의 비밀번호와 비교한다.
-- 작성 중 --
'Spring' 카테고리의 다른 글
[JPA] 연관관계 조회 방식별(Fetch, Lazy) 성능 차이 테스트 (0) | 2023.07.25 |
---|---|
[Spring] MapStruct 사용 주의 사항 (0) | 2023.07.25 |
[Spring] Redis Template과 Redis Repository 특징과 장단점 (0) | 2023.07.24 |
[Spring] Http Status Code 제어 (0) | 2020.10.26 |
[JPA]Spring Data JPA 시작 (0) | 2020.09.16 |