이전 포스팅에서는 Spring Security 환경에서 CORS Filter를 적용하는 것에 대해서 공부하였습니다.
이번 포스팅은 CORS Filter를 이용해 로그인 환경을 커스터마이징해보려고 합니다.
Security 설정 파일
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//패스워드 인코딩
@Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//1~6 생략....
//7. authenticationManager얻기
AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));
//9. 로그인진행시키기
//10. 로그인처리하기
//http.addFilterBefore( new CustomLoginFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class ); //로그인진행 필터 - 이렇게 해도됩니다.
http.addFilter( new CustomLoginFilter(authenticationManager)); //로그인진행 필터
return http.build();
}
//8. authenticationManager객체
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
CustomLoginFilter 클래스
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
//2. authenticationManager를 시큐리티 설정에서 전달하기
public CustomLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
//login을 시도함
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
System.out.println("===========attemptAuthentication===========>");
//1. /login요청으로 들어오는 username, userpassword의 폼 형식 로그인데이터를 얻는다.
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println(username);
System.out.println(password);
//3. 로그인 실행시키기 - 시큐리티 로그인 기능이 사용하는 전용토큰에 아이디, 비밀번호를 담습니다.
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
//어쏜티케이션매니저의 authenticate가 실행되면 MyUserDetailsService의 loadUserByUsername가 실행됩니다.
//여기 리턴이 있다는 것은 로그인 성공이라는 의미
Authentication authentication = authenticationManager.authenticate(token);
System.out.println("===========로그인성공=========>");
System.out.println("실행됨?:" + authentication);
return authentication; //리턴 (여기서 리턴된 값은 시큐리티세션( new Authentication(new MyUserDetails()) ) 형식으로 저장됩니다.
}
//successfulAuthentication오버라이딩 - 로그인 성공시 이후에 실행
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("===========로그인성공 success핸들러=========>");
//오쏘티케이션안에는 new Authentication(new MyUserDetails() 형식으로 유저객체가 저장됩니다.
MyUserDetails principal = (MyUserDetails)authResult.getPrincipal(); //오쏘티케이션 안에 유저객체를 얻습니다.
System.out.println(principal.getUsername());
System.out.println("===========JWT 토큰 생성 후 header에 담기=========>");
String token = JWTService.createToken(principal.getUsername() );
response.setContentType("text/html; charset=UTF8;");
response.addHeader("Authorization", "Bearer " + token);
response.getWriter().println("로그인성공(아이디):" + principal.getUsername() );
}
//unsuccessfulAuthentication오버라이딩
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
System.out.println("===========로그인성공 fail핸들러=========>");
System.out.println("인증에 실패함(로그인실패)");
response.setContentType("text/plain; charset=UTF8");
response.sendError(500, "아이디 비밀번호를 확인하세요");
}
로그인을 커스터마이징하기 위해 기본 스프링 시큐리티의 로그인을 담당하는 UsernamePasswordAuthenticationFilter 를 상속받고 연결을 해줘야한다.
UsernamePasswordAuthenticationFilter 은 loadByUser를 호출하는 필터이다.
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
위와 같이 오버라이딩한 attempAuthentication 메서드는 공식이 있다.
무조건 POST 방식으로 요청을 받아야 하며, /login 요청이 들어와야 attempAuthentication 메서드와 연결이된다.
String username = request.getParameter("username"); // form 방식으로 받을 때 이렇게 사용
String password = request.getParameter("password");
위 방식은 form 방식으로 받을 때 사용하는 방식으로, 사용자가 로그인을 시도할 때 어떤 값이 들어오는 지 확인할 수 있다.
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
이 값을 Authentication 인증 매니저인 AuthenticationManager로 전달한다.
Authentication authentication =
authenticationManager.authenticate(token); // 로그인 실패시 null 반환
유저 서비스의 loadByUser가 호출되는 부분이고, 로그인 성공시 정보를 확인할 수 있고, 실패시 Null을 반환한다.
처음 Spring Security에 대해 알아볼 때 authentication 내부에 principal이 있다는 것을 기억하면 값을 확인하기 편하다.
로그인 성공 / 실패 시 사용할 메서드 오버라이딩
UsernamePasswordAuthenticationFilter 내부의 메서드들 중에서 AbstractAuthenticationProcessingFilter 내부의
successfulAuthentication 과 unsuccessfulAuthentication 메서드를 이용하면 로그인 성공시와 실패시 처리해줄 비즈니스로직을 구현하여 서비스를 처리할 수가 있다.
successfulAuthentication : JWT 토큰 발행
unsuccessfulAuthentication : 실패시 에러 메세지
// 로그인 성공 시 이 메서드가 실행
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("로그인 sucees 후에 실행됌");
// jwt 토큰 발행
MyUserDetails details = (MyUserDetails)authResult.getPrincipal();
String token = JWTService.createToken(details.getUsername()); // 토큰 생성
// 헤더에 담는다.
response.setContentType("text/html; charset-UTF8");
response.setHeader("Authorization", token);
// 데이터도 보낼 수 있는데
response.getWriter().println("서버에서 보낸 메세지");
}
// 로그인 실패 시에는 이 메서드가 실행
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
System.out.println("로그인 실패시 실행");
response.setContentType("text/plain; charset-UTF8");
response.sendError(500, "아이디와 비밀번호를 확인하세요");
}
oken을 검사하는 필터 BasicAuthenticationFilter 토큰검사 커스터마이징
시큐리티 필터 BasicAuthenticationFilter는 HTTP요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolde(시큐리티세션)에 저장한다.
로그인 처리가 끝났다면, 다음으로 해야할 작업은 토큰을 검사하는 필터를 생성하는 작업이다.
- 로그인 이후에 token이 생성되어서 클라이언트로 전달되었다.
- 클라이언트는 token을 받는다.
- 클라이언트는 token을 기준으로 우리의 서비스를 호출하게 된다. (왜? - 토큰이 인증이야)
- 서버에서는 token의 유효성검사를 진행한 후, 유효한 토큰이면, 서비스를 호출하게 처리한다.
SecurityConfig 설정 파일
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//1~6 생략....
//7. authenticationManager얻기
AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));
//9. 로그인진행시키기
//10. 로그인처리하기
//http.addFilterBefore( new CustomLoginFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class ); //로그인진행 필터 - 이렇게 해도됩니다.
http.addFilter( new CustomLoginFilter(authenticationManager)); //로그인진행 필터
//11. 토큰검사필터 등록하기
//http.addFilterBefore( new JwtAuthrizationFilter(authenticationManager), BasicAuthenticationFilter.class); //토큰검사 필터 - 이렇게 해도됩니다.
http.addFilter( new JwtAuthrizationFilter(authenticationManager)); //토큰검사 필터
return http.build();
}
//8. authenticationManager객체
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
JWTAuthorizationFilter 클래스
public class JwtAuthrizationFilter extends BasicAuthenticationFilter {
//오쏘티케이션매니저를 기본으로 받음
public JwtAuthrizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
System.out.println("=========JWT 필터 실행========>");
//헤더값 얻기
String headers = request.getHeader("Authorization");
System.out.println("헤더값:" + headers);
//헤더가 없거나 Bearer로 시작하지 않으면
if(headers == null || headers.startsWith("Bearer ") == false ) {
response.setContentType("text/plain; charset=UTF8");
response.sendError(403, "토큰이 없습니다.");
return; //함수종료
}
//토큰 유효성 검사
try {
String token = headers.substring( 7 ); //Bearer공백 이후
boolean result = JWTService.validateToken(token); //토큰유효성검사
if(result) {
filterChain.doFilter(request, response); //다음필터로 연결
} else {
response.setContentType("text/html; charset=UTF8;");
response.sendError(403, "토큰이 만료되었습니다");
}
} catch (Exception e) {
response.setContentType("text/html; charset=UTF8;");
response.sendError(403, "토큰이 위조되었습니다");
}
}
}
URL요청별로 실행시키기
어떤 요청은 token을 필요로 하는 요청이 있다. (api 서비스)
어떤 요청은 token을 필요하지 않는 요청이 있다. (회원가입)
컨트롤러에 간략한 요청 코드를 생성해준다.
/join,
/api/v1/hello,
/api/v1/getInfo
/api/v2/~~~~
부메랑에서 URL요청별로 필터를 실행
시큐리티 설정 파일
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//1~6 생략....
//7. authenticationManager얻
AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));
//9. 로그인진행시키기
//10. 로그인처리하기
//http.addFilterBefore( new CustomLoginFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class ); //로그인진행 필터 - 이렇게 해도됩니다.
//http.addFilter( new CustomLoginFilter(authenticationManager)); //로그인진행 필터
//11. 토큰검사필터 등록하기
//http.addFilterBefore( new JwtAuthrizationFilter(authenticationManager), BasicAuthenticationFilter.class); //토큰검사 필터 - 이렇게 해도됩니다.
//http.addFilter( new JwtAuthrizationFilter(authenticationManager)); //토큰검사 필터
//12. 로그인 + 토큰검사필터 + URL요청별로 분기하기
http.requestMatchers().antMatchers("/login")
.and()
.addFilter(new CustomLoginFilter(authenticationManager));
http.requestMatchers().antMatchers("/api/v1/**")
.antMatchers("/api/v2/**")
.and()
.addFilter(new JwtAuthrizationFilter(authenticationManager));
return http.build();
}
//8. authenticationManager객체
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
아직은 이해가 크게 되지 않지만, 일단 흐름을 이해하려고 노력하고 있다.
추후에 더 깊숙하게 공부해 어떤 흐름으로 정확히 동작하는 지 서술할 예정이다.
아래는 전체 소스코드이다.
MemberVO
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class MemberVO {
private String username;
private String password;
private String role;
}
Controller
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class APIController {
// JWT - API 기반의 인증 정보를 처리할 때, 토큰 발급 ( 발행자 )
// @PostMapping("/login")
// public ResponseEntity<Object> login(@RequestBody MemberVO vo){
//
// // 사용자 정보를 받아서 로그인 처리
// // ok - 로그인 성공
// // 토큰 발행
// String token = JWTService.createToken(vo.getUsername());
//
// return new ResponseEntity<>(token, HttpStatus.OK);
// }
// @PostMapping("/api/v1/getInfo")
// public ResponseEntity<Object> getInfo(HttpServletRequest request){
//
// // 클라이언트에서 토큰을 헤더라는 곳에 담는다.
// // 토큰을 전달받아 유효성을 확인한 후 만료인지, 통과인지 확인
// String token = request.getHeader("Authorization");
// System.out.println(token);
//
// // 토큰이 유효한지 확인,
// try {
// boolean result = JWTService.validateToken(token);
// System.out.println("토큰의 무결성 여부 : " + result);
// } catch (Exception e) {
// e.printStackTrace(); //토큰이 위조 됐을 경우
// return new ResponseEntity<>("토큰 위조", HttpStatus.UNAUTHORIZED); //토큰 위조
// }
//
//
//
//
// return new ResponseEntity<>("통과된 사람이면 여기에 회원정보 발급", HttpStatus.OK);
// }
// 필터 동작 확인
@GetMapping("/api/v1/filter")
public String hello() {
return "<h3>hello</h3>";
}
@PostMapping("/api/v1/getInfo")
public ResponseEntity<Object> getInfo(HttpServletRequest request){
System.out.println("getInfo메서드는 토큰이 있는 자만 호출할 권리가 있다.");
return new ResponseEntity<Object>("getInfo ~~", HttpStatus.OK);
}
}
MemberMapper.java
import org.apache.ibatis.annotations.Mapper;
import com.project.jwt.command.MemberVO;
@Mapper
public interface MemberMapper {
void join(MemberVO vo);
MemberVO login(String username);
}
MemberMapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.project.jwt.member.MemberMapper">
<insert id="join">
insert into members values (#{username}, #{password}, #{role})
</insert>
<select id="login" resultType="com.project.jwt.command.MemberVO">
select * from members where username = #{username}
</select>
</mapper>
SecurityConfig
import java.util.Arrays;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.project.jwt.util.filter.CustomLoginFilter;
import com.project.jwt.util.filter.FilterOne;
import com.project.jwt.util.filter.FilterTwo;
import com.project.jwt.util.filter.JWTAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.csrf().disable();
http.formLogin().disable();
http.httpBasic().disable(); // http의 기본 인증 형태도 폐기 Authorization(아이디,비밀번호 형태 폐기)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션방식 비활성화
// 2. 크로스 오리진 활성화
http.cors( Customizer.withDefaults() );
// 3. 시큐리티에 필터 추가
//http.addFilter(new FilterOne()); //addFilter에는 시큐리티 타입의 필터만 등록이 된다.
// 시큐리티 타입의 필터가 아니라 ,일반 필터라면 시큐리티 필터 전후로 필터를 추가한다.
//http.addFilterBefore(new FilterOne(), UsernamePasswordAuthenticationFilter.class);
//http.addFilterAfter(new FilterTwo(), FilterOne.class);
// 스프링 시큐리티 타입의 필터를 커스터마이징하여 사용
// 클라이언트에서 /login post방식으로 요청
AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));
//http.addFilter(new CustomLoginFilter(authenticationManager) ); // 로그인 필터
//http.addFilter(new JWTAuthenticationFilter(authenticationManager)); // JWT 인증 필터
http.requestMatchers().antMatchers("/login")
.and()
.addFilter(new CustomLoginFilter(authenticationManager));
http.requestMatchers().antMatchers("/api/v1/**")
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager));
return http.build();
}
//1. 크로스 오리진 필터등록
//서버가 다를 때, 옵션 요청을 보내게 되는데, 옵션 요청에 요청을 도메인 주소를 헤더에 담아 보내는 작업
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
//configuration.setAllowedOrigins(Arrays.asList("http://localhost:8181")); //부메랑에서 요청이 deny됨
configuration.setAllowedOrigins(Arrays.asList("*")); //모든요청 주소를 허용 == CrossOrigin
configuration.setAllowedMethods(Arrays.asList("*")); // * : 모든요청 메서드를 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
MyUserDetails
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.project.jwt.command.MemberVO;
public class MyUserDetails implements UserDetails {
// 로그인해서 조회한 MemberVO 객체
private com.project.jwt.command.MemberVO memberVO;
// 반드시 UserVO 객체를 멤버변수로 담고 생성
public MyUserDetails(MemberVO vo) {
this.memberVO = vo;
}
// role 리턴
public String getRole() {
return memberVO.getRole();
}
// 로그인시 권한을 리턴해주는 함수
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
// 권한이 여러 개라면 반복문
list.add( () -> memberVO.getRole() );
return list;
}
// 유저의 비밀번호를 반환하는 자리
@Override
public String getPassword() {
return memberVO.getPassword();
}
// 유저의 아이디를 반환하는 자리
@Override
public String getUsername() {
return memberVO.getUsername();
}
// 계정 만료 여부 확인 ( true = 네 )
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true; // 계정 락 여부, 락이 걸리지 않았습니까?
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 비밀번호 만료 여부 / true 만료 x
}
@Override
public boolean isEnabled() {
return true; // 계정 사용 여뷰
}
}
MyUserDetailsService
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.project.jwt.member.MemberMapper;
import com.project.jwt.command.MemberVO;
//빈으로 등록되어 있으면, 스프링이 UserDetailsService 타입을 찾아서, 사용자가 로그인시
//loadUserByUsername을 실행시킨다.
@Service
public class MyUserDetailsService implements UserDetailsService{
@Autowired
private MemberMapper memberMapper;
//loginProcessingUrl에 로그인 URL을 등록
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("사용자가 로그인을 시도함");
System.out.println("사용자가 입력한 아아디 : " + username);
// 로그인 시도 (비밀번호는 시큐리티가 알아서 비교 후에 처리한다.)
// 로그인을 하여 vo객체를 얻어온다.
MemberVO vo = memberMapper.login(username);
// 회원정보 존재
// 비밀번호 비교를 위해, userDeatils 타입으로 리턴
if(vo != null) {
return new MyUserDetails(vo); // 스프링 시큐리티가 비밀번호 비교, 권한 확인한 이후 로그인 시도 처리
}
// 스프링 시큐리티의 설정한 형식대로, 권한까지 처리를 한다.
// 아이디가 없거나 비밀번호가 틀리면 login?error 페이지로 기본 이동 ( 설정으로 변경 가능 )
// 시큐리티는 특별한 세션의 모형을 사용 -> 시큐리티세션(new Authentication(new MyUserDetails() ) ) 모형으로 저장
return null;
}
}
JWTService
import java.util.Date;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator.Builder;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.JWTVerifier;
public class JWTService {
private static String secretKey = "coding404"; // 시그니처를 만들기 위한 비밀키
// JWT 토큰 생성
public static String createToken(String username) {
//암호화알고리즘
Algorithm algorithm = Algorithm.HMAC256(secretKey);
//만료시간
long expire = System.currentTimeMillis() + 3600000; //1시간뒤
// 페이로드 생성
Builder builder = JWT.create()
.withSubject(username) // 토큰의 주제(subject) 설정
.withIssuedAt(new Date()) // 토큰 발급 시간 설정 (현재 시간)
.withExpiresAt(new Date(expire)) // 토큰 만료 시간 설정
.withIssuer("coding404서버") //토큰발행자
.withClaim("admin", "나야나~"); //공개클래임
return builder.sign(algorithm); // 비밀 키로 서명하여 토큰 생성
}
//토큰검증
public static boolean validateToken(String token) throws JWTVerificationException { //확인단계중하나라도 실패
Algorithm algorithm = Algorithm.HMAC256(secretKey); //검증 알고리즘
JWTVerifier verifier = JWT.require(algorithm).build(); //token을 검증할 객체생성
verifier.verify(token); // 토큰 검증을 수행하며, 만료 시간도 자동으로 검사됨
return true; // 검증 성공 시 true 반환
}
}
FilterOne
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterOne implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("필터1 실행");
chain.doFilter(request, response);
}
}
FilterTwo
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterTwo implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// TODO Auto-generated method stub
System.out.println("필터2 실행");
chain.doFilter(request, response);
}
}
CustomLoginFilter
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.web.authentication.UsernamePasswordAuthenticationFilter;
import com.project.jwt.security.MyUserDetails;
import com.project.jwt.util.JWTService;
//LoadByUser를 호출하는 필터가 UsernamePasswordAuthenticationFilter이다.
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter{
//UsernamePasswordAuthenticationFilter : 로그인을 담당하는 필터 중 하나
// 사용자 로그인을 커스터마이징 시킬 때 상속받고, 연결해주면 된다.
private AuthenticationManager authenticationManager;
public CustomLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
// Post방식의 /login 요청이 들어오면 attemptAuthentication으로 연결
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
System.out.println("attemptAuthentication Filter 실행");
String username = request.getParameter("username"); // form 방식으로 받을 때 이렇게 사용
String password = request.getParameter("password");
System.out.println(username + " " + password);
//UsernamePasswordAuthentication
// 이 값을 인증 매니저 Authuentication Manager에게 전달
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
// 유저 서비스의 loadByUser가 호출된다.
Authentication authentication = authenticationManager.authenticate(token); // 로그인 실패시 null 반환
System.out.println("필터 로그인 성공 시 : " + authentication);
return authentication; // 스프링 시큐리티가 이 값을 가져가 스피링 세션에 등록시킴
}
// 로그인 성공 시 이 메서드가 실행
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("로그인 sucees 후에 실행됌");
// jwt 토큰 발행
MyUserDetails details = (MyUserDetails)authResult.getPrincipal();
String token = JWTService.createToken(details.getUsername()); // 토큰 생성
// 헤더에 담는다.
response.setContentType("text/html; charset-UTF8");
response.setHeader("Authorization", token);
// 데이터도 보낼 수 있는데
response.getWriter().println("서버에서 보낸 메세지");
}
// 로그인 실패 시에는 이 메서드가 실행
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
System.out.println("로그인 실패시 실행");
response.setContentType("text/plain; charset-UTF8");
response.sendError(500, "아이디와 비밀번호를 확인하세요");
}
}
JWTAuthenticationFilter
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.project.jwt.util.JWTService;
// BasicAuthenticationFilter는 HTTP 요청의 (BASIC) 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장
public class JWTAuthenticationFilter extends BasicAuthenticationFilter{
// 생성자는 반드시 authenticationManager에서 받아줌
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
// TODO Auto-generated constructor stub
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("jwt basic 헤더 검사하는 필터 실행됌");
// 헤더에 담긴 토큰을 검사해서 토큰의 무결성 여부를 확인해 에러를 띄우거나, 컨트롤러를 실행한다.
// 1.헤더값 얻기
String header = request.getHeader("Authorization");
System.out.println(header);
// 토큰을 보내지 않거나, 베어러로 시작하지 않으면
if(header == null || header.startsWith("Bearer ") == false ) {
response.setContentType("text/plain; charset-UTF-8");
response.sendError(403, "토큰이 없습니다.");
return; //함수 종료하지 않으면 Controller로 연결
}
// 토큰의 유효성 검사
try {
String token = header.substring(7);
boolean result = JWTService.validateToken(token);
// 정상적인 토큰인 경우
if(result) {
chain.doFilter(request, response); //컨트롤러로 연결
}else { // 토큰의 유효기간이 만료된 경우
response.setContentType("text/plain; charset-UTF-8");
response.sendError(403, "만료된 토큰입니다..");
}
} catch (Exception e) {
response.setContentType("text/plain; charset-UTF-8");
response.sendError(403, "잘못된 토큰입니다.");
}
}
}
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.18'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.project'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '11'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.0'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
implementation 'com.auth0:java-jwt:4.4.0'
implementation 'org.springframework.boot:spring-boot-starter-security'
//시큐리티 타임리프에서 사용
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
//시큐리티 테스트
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
'Programming > Spring Boot' 카테고리의 다른 글
Spring Security - BadCredentialsException (1) | 2024.06.15 |
---|---|
[Spring boot] Spring Security static 폴더(CSS,JS) 적용 문제 해결하기 (0) | 2024.06.15 |
[Spring Boot] Spring Security를 이용한 JWT CORS필터체이닝 (0) | 2024.05.30 |
[Spring Boot] JWT HMAC 암호화 (0) | 2024.05.30 |
[Spring Boot] JWT (1) | 2024.05.30 |