Overview
인증(Authentication)과 인가(Authorization)
인증과 인가 기능을 구현하기 전 차이를 먼저 알아야 하는데,
- 인증
- 인증이란, 사용자가 누구인지 확인하는 절차로 대표적인 예로 회원가입과 로그인을 말한다.
- 인가
- 인가란, 사용자가 요청하는 동작을 할 수 있는 권한이 있는지 확인하는 절차로 대표적인 예로 글삭제 등 인증 이후의 절차를 말한다.
Spring Security
Spring 애플리케이션의 인증, 권한 부여 및 기타 보안 기능을 제공하는 스프링 하위 프레임워크이다.
https://docs.spring.io/spring-security/reference/index.html
Spring Security :: Spring Security
If you are ready to start securing an application see the Getting Started sections for servlet and reactive. These sections will walk you through creating your first Spring Security applications. If you want to understand how Spring Security works, you can
docs.spring.io
JWT(Json Web Token)
Json을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 웹 토큰으로 선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준이다.
스프링부트 프로젝트 생성
Spring Security와 JWT로 인증 및 인가 기능을 구현하기 위해서는 Spring Boot 프로젝트가 필요하다.
아래 게시글을 참고하여 프로젝트를 생성하고, 필요한 라이브러리는 아래 이미지를 참고한다.
2023.03.05 - [Web Application/Spring boot] - [spring] 1. IntelliJ를 이용하여 Spring boot 프로젝트 생성
[spring] 1. IntelliJ를 이용하여 Spring boot 프로젝트 생성
지금부터 Intellij를 이용하여 스프링푸트 프로젝트 생성 및 실행하는 법을 알아본다. 1. InterlliJ 실행 후 New Project 클릭한다. 2. Spring Initializr 클릭 후 프로젝트 정보 입력 후 Next를 클릭한다. 2-1. Name
lims-dev.tistory.com

프로젝트가 생성 되었으면, pom.xml에 JWT 라이브러리도 추가해준다.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Configuration 생성
1. security config
WebSecurityConfigurerAdapter를 상속받는 Security config를 생성한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
* WebSecurity(전체적인 보안구성)
- HttpSecurity 보다 우선적으로 고려
- 만약, Web Security와 HttpSecurity가 동시에 설정되어 있다면 HttpSecurity의 인가설정은 무시된다.
- 인증, 인가서비스가 필요하지 않는 곳에 설정
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/resources/**")
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/img/**");
}
- HttpSecurity(Http 요청에 대한 보안구성)에는 아래의 설정 말고도 다양한 설정 등이 있다.
- antMatchers의 "인증"을 무시
- URL 접근 권한 설정 ( antMatchers : 특정 리소스에 대한 권한 설정, permitAll : 인증절차 없이 접근 가능)
- 인증 로직을 커스텀 하기 위한 커스텀 필터 설정
- addFilterBefore는 myFilter() 가 실행되기 전 AnotherFilter가 실행된다.
- addFilter는 myFilter() 가 실행된 후에 AnotherFilter가 실행된다.
http.addFilterBefore(new MyFilter(), AnotherFilter.class);
============================
http.addFilter(new MyFilter());
http.addFilter(new AnotherFilter());
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/users/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
- BCryptPasswordEncoder
- 사용자 비밀번호화 암호화되어 저장된 비밀번호가 일치하는지 확인
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- AuthenticationManagerBuilder
- 인증 객체를 만들어주는 기능을 제공하는 Builder
- 사용자 인증정보를 검색하고 비밀번호를 암호화
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
- cors 설정
- Spring Security는 기본적으로 CORS를 허용하지 않도록 설정되어 있기 때문에 허용하고자 하는 경우 설정
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
- filter 등록
- jwt인증을 처리하기 위한 필터 등록
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
***전체 코드
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private JwtTokenProvider jwtTokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/resources/**")
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/img/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/users/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
CustomUserDetails 생성
- CustomUserDetailsServices
- 데이터베이스에서 사용자 정보를 가져오는 서비스
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
String user = "";
if (user == null) {
throw new UsernameNotFoundException("사용자 없음 " + email);
}
CustomUserDetails customUserDetails = new CustomUserDetails();
return customUserDetails;
}
}
- CustomUserDetails
- 인증된 사용자의 정보를 저장하기 위한 클래스
@Getter
public class CustomUserDetails implements UserDetails {
private User user;
private String username; //사용자이름
private String email; //사용자 아이디
private String password; //사용자이름
private String phoneno; //휴대폰번호
private Collection<GrantedAuthority> authorities; //권한 목록
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 사용자가 가지고 있는 권한
return authorities;
}
@Override
public String getPassword() {
// 사용자 패스워드
return password;
}
public String getUsername(String username) {
// 사용자 아이디
return email;
}
@Override
public boolean isAccountNonExpired() {
// 계정이 만료되었는지 여부
return true;
}
@Override
public boolean isAccountNonLocked() {
// 계정이 잠겨있는지 여부
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// 자격 증명이 만료되었는지 여부
return true;
}
@Override
public boolean isEnabled() {
// 사용자가 사용 가능한지 여부
return true;
}
}
서비스에서는 데이터베이스에 있는 사용자 정보를 받아와서 CustomUserDetiails 객체를 생성하고,
CustomUserDetails 객체에 인증된 사용자의 정보를 저장한다.
이렇게 생성된 CustomUserDetails는 Authentication 객체를 생성하는 데 사용한다.
JWT 관련 클래스 생성
* JwtAuthenticationFilter
- HTTP 인증작업에 대한 요청을 수행하는 역할로, 클라이언트에서 넘어온 토큰을 검증하고, 유효한 토큰은 추출하고, 추출된 정보를 사용하여 인증 정보를 저장하는 역할을 수행
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
public JwtAuthenticationFilter() {}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 메인 페이지 URL 체크
if (request.getRequestURI().equals("/")) {
filterChain.doFilter(request, response); // 필터 체인 종료 후 요청 처리
return;
}
String token = jwtTokenProvider.resolveToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
* JwtTokenProvider
- jwt 토큰을 생성하고 검증하는 역할
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
private final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
private final CustomUserDetailsService customUserDetailsService;
public JwtTokenProvider(CustomUserDetailsService customUserDetailsService) {
this.customUserDetailsService = customUserDetailsService;
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public Authentication getAuthentication(String token) {
String username = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
}
예외처리 클래스 생성
인증되지 않은 사용자가 접근하려고 할 때 발생하는 예외처리를 해주기 위한 클래스를 생성한다.
- 401 응답코드를 리턴
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.error("에러메세지 - {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
인증 / 인가 기능에 필요한 클래스들을 만들었다.
데이터베이스를 추가하는 과정과 내 프로젝트에 맞게 수정하는 과정이 필요하다.
이 코드를 바탕으로 다음 포스팅에서는 카카오 api를 이용한 로그인 기능을 구현한다.
'Web Application > Spring boot' 카테고리의 다른 글
| [spring] Spring Security + JWT로 로그인 구현하기(1) - 카카오 로그인 api로 로그인 기능 구현 (0) | 2023.03.31 |
|---|---|
| [spring] SpringBoot에 MyBatis 세팅 및 적용하는 방법 (0) | 2023.03.28 |
| [spring] Oauth2 (1) - 네이버 로그인 SDK를 사용하여 로그인 기능 구현(네아로) (0) | 2023.03.22 |
| [spring] 간편결제 - 네이버페이(naver pay) 결제 api (1) 결제 기능 구현 (1) | 2023.03.18 |
| [spring] 간편결제 - 카카오페이(kakao pay) 결제 api (1) 단건결제 기능 구현 (3) | 2023.03.18 |