인증, 인가를 프로젝트에 끼워넣기
=================
Spring Security+jwt 입니다
로그인해서(/v1/admin(auth)/signin으로 세팅돼있습니다)
연습용 토큰을 발급하고(POSTMAN의 헤더 부분에 떠있습니다)
다른 API호출할 땐 POSTMAN의 Authorization에 Type을 Bearer Token으로 해놓고 발급받은 토큰을 넣어주시면 됩니다
=================
로그인이 필요한 admin, auth 도메인은 인증, 인가 둘다 필요하고
나머지는 인가만 필요하니까 두개 나눠서 적었습니다. 둘 중에 하나만 보시면 됩니다
=================
[1]인가
[2]인증+인가
=================
[1]인가 : admin, auth 외 나머지
이걸 하면 사용자가 헤더에 jwt 토큰을 갖고 있을 때만 그 설정한 API를 부를 수 있습니다~
1.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
추가
2.
WebSecurityConfigurerAdapter를 상속하는 클래스 만들기
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
static public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors();
http
.authorizeRequests()
.antMatchers("/v1/admin/health-check").permitAll()
.antMatchers("/v1/admin/signup").permitAll()
.antMatchers("/v1/admin/findid").permitAll()
.antMatchers("/v1/admin/findpw").permitAll();
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(getAuthenticationFilter());
}
@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","PATCH","OPTIONS","DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
- antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣는다
3.
token.expiration_time = 86400000
token.secret = usertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkoreausertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkorea
application.properties에 이것 추가
4.
HandlerInterceptorAdapter를 상속하는 클래스 만들기
package com.cloudlibrary.admin.ui.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SuppressWarnings("deprecation")
public class TokenValidationInterceptor extends HandlerInterceptorAdapter {
Environment env;
public TokenValidationInterceptor(Environment env) {
this.env = env;
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret")).parseClaimsJws(jwt).getBody()
.getSubject();
} catch (Exception e) {
returnValue = false;
}
if (subject == null || subject.isEmpty())
returnValue = false;
return returnValue;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
throw new AccessDeniedException("인증 정보 누락");
}
String jwt = authorizationHeader.replace("Bearer", "").trim();
if (!isJwtValid(jwt)) {
throw new AccessDeniedException("인증 오류");
}
return super.preHandle(request, response, handler);
}
}
5.
WebMvcConfigurer implements하는 클래스 추가
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired
Environment env;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenValidationInterceptor(env))
.addPathPatterns("/**")
.excludePathPatterns("/v1/lending");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("*").allowedOriginPatterns("*");
}
}
- 아까 antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣은것처럼 여기에서도 exclude에 넣기
왜 두번하는진 나중에 찾아보겠습니다
---인가 끝---
=================
[2]인증+인가 : admin, auth
1.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
추가
2.
WebSecurityConfigurerAdapter를 상속하는 클래스 만들기
package com.cloudlibrary.admin.ui.security;
import com.cloudlibrary.admin.application.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AdminService adminService;
@Autowired
Environment env;
@Bean
static public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors();
http
.authorizeRequests()
.antMatchers("/v1/admin/health-check").permitAll()
.antMatchers("/v1/admin/signup").permitAll()
.antMatchers("/v1/admin/findid").permitAll()
.antMatchers("/v1/admin/findpw").permitAll();
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(getAuthenticationFilter());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminService).passwordEncoder(passwordEncoder());
}
private AuthenticationFilter getAuthenticationFilter() throws Exception {
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), adminService, env);
return authenticationFilter;
}
@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","PATCH","OPTIONS","DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
- 패스워드를 암호화시키는 BCryptPasswordEncoder을 스프링에 등록
- antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣는다
- userDetailsService라는 애한테 loadUserByUserName 이라는 애가 있을 서비스와 암호화방식을 등록해준다
- 필터 리턴
=>복붙하고 클래스명과 변수명만 바꿔주면 잘돌아갑니다
3.
UsernamePasswordAuthenticationFilter를 상속하는 클래스 만들기
package com.cloudlibrary.admin.ui.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cloudlibrary.admin.application.service.AdminReadUseCase;
import com.cloudlibrary.admin.application.service.AdminService;
import com.cloudlibrary.admin.ui.requestBody.AdminLoginRequest;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AdminService adminService;
Environment env;
@Autowired
public AuthenticationFilter(AuthenticationManager authenticationManager, AdminService adminService, Environment env) {
super.setAuthenticationManager(authenticationManager);
super.setFilterProcessesUrl("/v1/admin/signin");
this.adminService = adminService;
this.env = env;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
AdminLoginRequest creds = new ObjectMapper().readValue(request.getInputStream(), AdminLoginRequest.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(
creds.getId(), creds.getPw(), new ArrayList<>());
return getAuthenticationManager().authenticate(authentication);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String userName = ((User) authResult.getPrincipal()).getUsername();
AdminReadUseCase.FindAdminResult findAdminResult = adminService.getAdminById(userName);
String token = Jwts.builder()
.setSubject(findAdminResult.getId())
.setExpiration(new Date(System.currentTimeMillis()
+ Long.parseLong(env.getProperty("token.expiration_time"))))
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
Cookie setCookie = new Cookie("token", token);
response.addCookie(setCookie);
response.addHeader("token", token);
response.addHeader("adminId", Long.toString(findAdminResult.getAdminId()));
response.addHeader("userId", findAdminResult.getId());
response.addHeader("libraryName", findAdminResult.getLibraryName());
response.addHeader("email", findAdminResult.getEmail());
}
}
---아래는 안 읽어도 되고 변수명만 바꿔주면 되는 부분---
값이 넘어오면,
vo로 바꾸고,
Manager의 authentication메소드에 전달하고,
매니저는 여러 Provider들을 모아놨다가 객체가 넘어오면 그에 맞는 Provider를 선택해서,
Provider에 의해 인증과정을 거치고
성공 실패 여부에 따라 successful / unsuccessful 메소드가 호출됨
successful / unsuccessful 는 request, response, FilterChain, Authentication(Result)등을 전달받고, Response를 돌려줌
- AuthenticationFilter는 UsernamePasswordAuthenticationFilter클래스를 상속받는데, 여기엔 PathMatcher가 있고, [POST] /login 으로 오는 로그인 Request를 해당 Filter에서 인터셉트할 수 있다
- 이 클래스의 attemptAuthentication()은 토큰을 생성하는데,
id pwd기반이라서, UsernamePasswordAuthenticationFilter를 쓸 것
(1)attemptAuthentication() 메서드와 (2)successfulAuthentication() 메서드를 구현해 줘야 함
(1)attemptAuthentication() : HTTP Request가 처음으로 들어오는 메소드,
- Request Body에 있는 데이터를 내 vo에 넣고,
- Provider Manager의 authentication메소드에 전달함.
*JSON을 Java로 deserialization 하기 위해 ObjectMapper의 readValue()메소드를 이용, request한테서 RequestLogin데이터를 추출해서 가져옴
(2)successfulAuthentication()
Provider에 의해서 인증과정을 거치고, 인증의 성공/실패 여부에 따라서 호출되는 메소드. authentication의 결과가 성공이면 인증된 객체가 successfulAuthentication 메소드로 돌아오고, 발행된 토큰과 같은 적절한 데이터를 포함한 HTTP Response를 돌려주면 된다
*결과가 실패면 인자로 넘어온 예외를 활용해 사용자에게 적절한 데이터를 보여줘 이후 프로세스를 진행할 수 있도록
Authentication 는 인터페이스고, 인증객체는 이 인터페이스를 구현한 모든 클래스의 객체를 말함
Authentication 인터페이스에는 구현해야할 6개의 메소드가 있는데 이미 구현해놓은 클래스들이 있다. 예를 들어 UsernamePasswordAuthenticationToken 클래스이다.
이걸로 List를 만들고, manager의 메소드에 넣는다
authResult.getPrincipal() 에 담겨오는건 문자열, 문자열로 캐스팅(간단하게 해당 사용자를 고유하게 식별할 수 있는 객체가 들어가는 공간, String형 객체에 아이디를 저장해 사용할 수도 있다)
맨 아래에는 토큰 생성 로직
4.
Service클래스로 가서 UserDetailsService를 상속받고 loadUserByUsername() 메소드 오버라이드
@Slf4j
@Service
public class AdminService implements AdminOperationUseCase, AdminReadUseCase {
private final AdminEntityRepository adminEntityRepository;
private final AdminMapper adminMapper;
@Autowired
public AdminService(AdminEntityRepository adminEntityRepository
, AdminMapper adminMapper){
this.adminEntityRepository = adminEntityRepository;
this.adminMapper = adminMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<AdminEntity> resultId = adminMapper.findAdminById(username);
if (resultId == null) {
throw new UsernameNotFoundException(username);
}
return new User(resultId.get().getId(), resultId.get().getEncryptedPw(),
true, true, true, true, new ArrayList<>());
}
}
사용자 정보를 조회 -> 존재하는 경우 -> 스시 내부에서 사용하고 있는 User객체 형태로 변환
5.
token.expiration_time = 86400000
token.secret = usertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkoreausertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkorea
application.properties에 이것 추가
6.
JWT토큰을 검증하는 인터셉터, HandlerInterceptorAdapter를 상속하는 클래스 만들기
package com.cloudlibrary.admin.ui.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SuppressWarnings("deprecation")
public class TokenValidationInterceptor extends HandlerInterceptorAdapter {
Environment env;
public TokenValidationInterceptor(Environment env) {
this.env = env;
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret")).parseClaimsJws(jwt).getBody()
.getSubject();
} catch (Exception e) {
returnValue = false;
}
if (subject == null || subject.isEmpty())
returnValue = false;
return returnValue;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
throw new AccessDeniedException("인증 정보 누락");
}
String jwt = authorizationHeader.replace("Bearer", "").trim();
if (!isJwtValid(jwt)) {
throw new AccessDeniedException("인증 오류");
}
return super.preHandle(request, response, handler);
}
}
7.
WebMvcConfigurer를 implements하는 애 만들고 인터셉터 등록
- 아까 antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣은것처럼 여기에서도 exclude에 넣기
package com.cloudlibrary.admin.ui.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired
Environment env;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenValidationInterceptor(env))
.addPathPatterns("/**")
.excludePathPatterns("/v1/admin/signup")
.excludePathPatterns("/v1/admin/findid")
.excludePathPatterns("/v1/admin/findpw");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("*").allowedOriginPatterns("*");
}
}
---끝---