Spring Security, JWT, 인증, 인가
Spring Security를 베이스로 JWT를 사용해서 해당 프로젝트의 인증과 인가를 구현한다.
이와 관련돼서 생성된 클래스는 다음과 같다.
● SecurityConfig : Spring Security관련 설정
● UserAccount : Spring Security에서 인증 요소(principal)로 사용되는 객체. Userdetails를 상속받고 Account의 정보를 갖는다.
● PrincipalDetailService : 인증 시, DB에서 Account를 찾고 UserAccount로 반환하는 loadUserByUsername 메서드를 갖는다.
● JwtAutienticationFilter : jwt를 사용해서 인증 처리
● JwtAutiorizationFilter : jwt를 사용해서 인가 처리
1. SecurityConfig
해당 프로젝트에서 Spring Security가 사용할 정책, 필터, 인가 권한 등을 설정한다.
[SecurityConfig 전체 코드]
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AccountRepository accountRepository;
private final JwtProcessor jwtProcessor;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilter(corsFilter())
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtProcessor))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), accountRepository, jwtProcessor));
http
.authorizeRequests()
.mvcMatchers("/home", "/login").permitAll() //** 홈페이지, login
.anyRequest().hasAuthority("ROLE_USER");
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
■ 기본 설정
[SecurityConfig 기본 설정]
http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable();
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
● csrf.disable() : API를 작성하는데 프런트가 정해져있지 않기 때문에 csrf설정은 우선 꺼놓는다.
● formLogin.disable() : formLogin 대신 Jwt를 사용하기 때문에 disable로 설정
● httpBasic.disable() : httpBasic 방식 대신 Jwt를 사용하기 때문에 disable로 설정
● SessionCreationPolicy.STATELESS : Jwt를 사용하기 때문에 session을 stateless로 설정한다. stateless로 설정 시 Spring Security는 세션을 사용하지 않는다.
■ 추가 필터
[추가 필터]
http
.addFilter(corsFilter())
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtProcessor))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), accountRepository, jwtProcessor));
□ corsFilter
[corsFilter]
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
cors관련 설정을 포함한 필터.
기본적으로 서버 또는 지정된 특정 도메인의 요청만 허용하지만 프런트가 정해져있지 않기 때문에 모든 도메인을 허용하는 방식으로 설정.
● setAllowCredentials : 내 서버가 응답을 할 때 json을 자바스크립트에서 처리할수 있게 할지를 설정
● addAllowedOriginPattern : 허용할 도메인 목록
● addAllowedHeader : 허용할 헤더 목록
● addAllowedMethod : 허용할 메서드(GET, PUT, 등) 목록
● source.registerCorsConfiguration : 지정한 url에 config 적용
□ JwtAuthenticationFilter
Jwt를 사용한 인증을 구현한 필터
□ JwtAuthorizationFilter
Jwt를 사용한 인가를 구현한 필터
■ 인가
[인가 관련 코드]
http
.authorizeRequests()
.mvcMatchers("/home", "/login").permitAll() //** 홈페이지, 로그인
.anyRequest().hasAuthority("ROLE_USER");
● authorizationRequest : 요청에 따른 인가 설정
○ 기본적으로 모든 uri은 ROLE_USER의 권한만 허용
○ 홈페이지와 로그인, 스웨거 관련 uri은 모두 허용
○ configure(WebSecurity web) : HttpSecurity에서 설정하지 않은 정적리소스와 HTML 등에 관한 권한을 설정한다.
◎ web.ignoring().requestMathers(PathRequest.toStaticResources().atCommonLocations())
static 리소스의 자원을 시큐리티에서 제외(시큐리티에서 걸러지지 않고 접근 가능)
■ authenticationManagerBean
[authenticationManagerBean]
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
WebSecurityConfigurerAdepter를 상속받은 SecurityConfigure 외에서 AuthenticationManager를 사용하려면 authenticationManagerBean()을 오버라이드 해서 @Bean으로 직접 등록해야 한다.
2. UserDetails, UserDetailsService
Spring Security에서 인증, 인가를 할 때 사용되는 Principal과 관련 서비스 클래스를 만든다.
■ UserAccount
Principal은 인증, 인가시 검증되는 객체이기 때문에 본 프로젝트에서 사용되는 회원의 정보인 Account를 갖고 있어야 한다.
[UserAccount]
@Getter
public class UserAccount implements UserDetails {
private Account account;
public UserAccount(Account account) {
this.account = account;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
String roleName = account.getRole().getRoleName();
authorities.add(() -> roleName);
return authorities;
}
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public String getUsername() {
return account.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;
}
}
● UserDetails를 상속받는다.
● 회원 계정 엔티티인 Account를 필드로 갖는다.
● getAuthorities() : account의 Role에 저장된 권한 정보를 authorities에 담고 반환한다.
● isAccountNonExpired() : 계정이 만료되지 않았는지를 리턴(true => 만료되지 않음을 의미)
● isAccountNonLocked() : 계정이 잠겨있는지를 리턴(true => 잠겨있지 않음을 의미)
● isCredentialNonExpired() : 계정의 패스워드가 만료되어있는지 를 리턴(true => 만료되지 않음을 의미)
● isEnabled() : 계정이 사용 가능한지를 리턴
■ PrincipalDetailService
[PrincipalDetailService]
@Service
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {
private final AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByUsername(username)
.orElseThrow(() -> new NonExistResourceException("해당 username을 갖는 Account를 찾을 수 없습니다."));
return new UserAccount(account);
}
}
● UserDetailsService를 상속받는다.
□ loadUserByUsername(String username)
[loadUserByUsername(String username)]
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByUsername(username)
.orElseThrow(() -> new NonExistResourceException("해당 username을 갖는 Account를 찾을 수 없습니다."));
return new UserAccount(account);
}
○ Spring Security에서 AutenticationManager가 authenticate()를 통해서 인증을 할 때, 지정된 repository에서 인증 대상 객체를 찾아서 Principal 형태로 반환
[AccountRepository]
public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findByUsername(String username);
}
● 스프링 데이터 JPA를 사용해서 Repository를 생성한다.
● findByUsername : username(= id)으로 Account를 찾아서 반환한다.
3. Jwt
Jwt를 생성하고 디코딩하는 클래스와 Jwt를 사용한 인증, 인가 필터를 구현한 클래스를 만든다.
■ JwtProcessor
[JwtProcessor]
@Component
public class JwtProcessor {
public String createAuthJwtToken(UserAccount userAccount) {
return JWT.create()
.withSubject(userAccount.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
.withClaim("id", userAccount.getAccount().getId())
.withClaim("username", userAccount.getAccount().getUsername())
.sign(Algorithm.HMAC512(JwtProperties.SECRET));
}
public String decodeJwtToken(String jwtToken, String secretKey, String claim) {
return JWT.require(Algorithm.HMAC512(secretKey)).build()
.verify(jwtToken)
.getClaim(claim)
.asString();
}
public String extractBearer(String jwtHeader) {
int pos = jwtHeader.lastIndexOf(" ");
return jwtHeader.substring(pos + 1);
}
}
□ creatAuthJwtToken
[creatAuthJwtToken]
public String createAuthJwtToken(UserAccount userAccount) {
return JWT.create()
.withSubject(userAccount.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
.withClaim("id", userAccount.getAccount().getId())
.withClaim("username", userAccount.getAccount().getUsername())
.sign(Algorithm.HMAC512(JwtProperties.SECRET));
}
● userAccount를 받아서 JwtToken을 생성하고 반환.
● Account의 id(엔티티의 id)와 username(로그인 시 id로 사용됨)을 HMAC512 알고리즘으로 암호화한다.
● 만료시간은 현재 시간으로부터 JwtProperties(Jwt관련 설정 정보를 모아놓은 클래스)에 정의된 EXPIRATION_TIME까지로 설정
※ 만료시간은 밀리 세컨드로 설정됨
□ decodeJwtToken
[decodeJwtToken]
public String decodeJwtToken(String jwtToken, String secretKey, String claim) {
return JWT.require(Algorithm.HMAC512(secretKey)).build()
.verify(jwtToken)
.getClaim(claim)
.asString();
}
● JwtToken을 받으면 secretKey를 사용해서 지정된 claim을 반환한다.
□ extractBearer
[extractBearer]
public String extractBearer(String jwtHeader) {
int pos = jwtHeader.lastIndexOf(" ");
return jwtHeader.substring(pos + 1);
}
● Authentication 해더의 Jwt Token은 앞에 "Bearer "가 붙기 때문에 "Bearer "를 제거하고 뒤의 순수한 Jwt Token만을 추출한다.
■ JwtProperties
[JwtProperties]
public interface JwtProperties {
String SECRET = (JWT 암호화시 사용할 SecretKey);
int EXPIRATION_TIME = 60000 * 60;
String TOKEN_PREFIX = "Bearer";
String HEADER_STRING = "Authorization";
}
● JWT와 관련된 설정 수치들을 지정한 인터페이스
● JWT 암호화 시 사용되는 SecretKey의 값이 있기 때문에 gitIgnore 설정
■ JwtAuthenticationFilter
[JwtAuthenticationFilter]
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtProcessor jwtProcessor;
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
ObjectMapper objectMapper = new ObjectMapper();
Account account = objectMapper.readValue(request.getInputStream(), Account.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(account.getUsername(), account.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
return authentication;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
UserAccount userAccount = (UserAccount) authResult.getPrincipal();
String jwtToken = jwtProcessor.createAuthJwtToken(userAccount);
response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + " " + jwtToken);
}
}
● JWT로 인증을 하기 위한 클래스
● Spring Security 로그인 시 인증을 담당하는 UsernamePasswordAuthenticationFilter를 상속받는다.
※ Spring Bean으로 등록하지 않는 이유는 해당 클래스가 AuthenticationManager를 의존성 주입받는데 해당 클래스를 사용하는 SecurityConfig에서 AuthenticationManager를 빈으로 등록하기 때문에 순환 참조가 발생하기 때문이다.
□ attemptAuthentication
[attemptAuthentication]
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
ObjectMapper objectMapper = new ObjectMapper();
Account account = objectMapper.readValue(request.getInputStream(), Account.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(account.getUsername(), account.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
return authentication;
}
● 로그인 시 인증을 위해 실행되는 메서드
● 오버라이드 해서 Json으로 들어오는 id와 password로 인증을 하도록 변경한다.
● 반환 값은 인증된 Authentication 객체이다.
□ successfulAuthentication
[successfulAuthentication]
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
UserAccount userAccount = (UserAccount) authResult.getPrincipal();
String jwtToken = jwtProcessor.createAuthJwtToken(userAccount);
response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + " " + jwtToken);
}
● 인증에 성공할 시 실행되는 메서드
● 인증에 성공할 시 인증된 Account의 정보를 통해 JWT Token을 만들고 헤더(Authentication 헤더)에 포함시킨다.
■ JwtAuthorizationFilter
[JwtAuthorizationFilter]
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private final AccountRepository accountRepository;
private final JwtProcessor jwtProcessor;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, AccountRepository accountRepository,
JwtProcessor jwtProcessor) {
super(authenticationManager);
this.accountRepository = accountRepository;
this.jwtProcessor = jwtProcessor;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String jwtHeader = request.getHeader(JwtProperties.HEADER_STRING);
if (jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
String jwtToken = jwtProcessor.extractBearer(jwtHeader);
String username = jwtProcessor.decodeJwtToken(jwtToken, JwtProperties.SECRET, "username");
if (username != null) {
Account account = accountRepository.findByUsername(username)
.orElseThrow(() -> new NonExistResourceException("해당 username을 갖는 Account를 찾을 수 없습니다."));
UserAccount userAccount = new UserAccount(account);
Authentication authentication = new UsernamePasswordAuthenticationToken(userAccount, null,
userAccount.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
● JWT로 인가를 하기 위한 클래스
● 헤더를 통한 인증 시 적용되는 BasicAuthenticationFilter를 상속받는다.
● BasicAuthenticationFilter는 AuthenticationManager를 사용하기 때문에 super를 사용해서 주입해준다.
□ doFilterInternal
[doFilterInternal]
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String jwtHeader = request.getHeader(JwtProperties.HEADER_STRING);
if (jwtHeader == null || !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
String jwtToken = jwtProcessor.extractBearer(jwtHeader);
String username = jwtProcessor.decodeJwtToken(jwtToken, JwtProperties.SECRET, "username");
if (username != null) {
Account account = accountRepository.findByUsername(username)
.orElseThrow(() -> new NonExistResourceException("해당 username을 갖는 Account를 찾을 수 없습니다."));
UserAccount userAccount = new UserAccount(account);
Authentication authentication = new UsernamePasswordAuthenticationToken(userAccount, null,
userAccount.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
● 필터 적용 시 실행되는 메서드.
● 헤더에 담겨있는 JWT Token을 디코딩해서 얻은 username값이 올바른지 판단하고 username으로 DB에서 Account를 찾아온다.
● 찾아진 Account로 만든 Authentication 객체를 SecurityContextHolder에 넣어서 인가를 처리한다.
'프로젝트 > PongGame' 카테고리의 다른 글
LoginAccountIdArgumentResolver (0) | 2022.04.28 |
---|---|
FileProcessor (0) | 2022.04.28 |
Account 생성 (0) | 2022.04.14 |
build.gradle, application.yml (0) | 2022.04.13 |
PongGame (0) | 2022.04.10 |
댓글