ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공통 Session server 와 JWT를 이용한 로그인 구현 with spring security multimodule,spring boot ,redis - 1
    Spring/Spring Security 2024. 4. 9. 17:07
    728x90
    반응형

     

     

     

     

     

     

    시작에 앞서 파일구조

    shared-session-server/
    │
    ├── session-server/
    │   ├── src/
    │   │   ├── main/
    │   │   │   ├── java/
    │   │   │   │   ├── com/
    │   │   │   │   │   ├── sessionserver/
    │   │   │   │   │   │   ├── config/
    │   │   │   │   │   │   │   ├── SecurityConfig.java
    │   │   │   │   │   │   │   ├── RedisConfig.java
    │   │   │   │   │   │   │   ├── JwtTokenUtil.java
    │   │   │   │   │   │   │   ├── JwtAuthenticationFilter.java
    │   │   │   │   │   │   ├── controller/
    │   │   │   │   │   │   │   ├── AuthController.java
    │   │   │   │   │   │   │   ├── HomeController.java
    │   │   │   │   │   │   ├── model/
    │   │   │   │   │   │   │   ├── User.java
    │   │   │   │   │   │   ├── service/
    │   │   │   │   │   │   │   ├── UserDetailsServiceImpl.java
    │   │   │   │   │   │   │   ├── UserService.java
    │   │   │   │   │   │   │   ├── UserServiceImpl.java
    │   │   │   │   ├── resources/
    │   │   │   │   │   ├── application.yml
    │
    ├── rest-server1/
    │   ├── src/
    │   │   ├── main/
    │   │   │   ├── java/
    │   │   │   │   ├── com/
    │   │   │   │   │   ├── restserver1/
    │   │   │   │   │   │   ├── config/
    │   │   │   │   │   │   │   ├── SecurityConfig.java
    │   │   │   │   │   │   ├── controller/
    │   │   │   │   │   │   │   ├── HomeController.java
    │   │   │   ├── resources/
    │   │   │   │   ├── application.yml
    │
    ├── rest-server2/
    │   ├── src/
    │   │   ├── main/
    │   │   │   ├── java/
    │   │   │   │   ├── com/
    │   │   │   │   │   ├── restserver2/
    │   │   │   │   │   │   ├── config/
    │   │   │   │   │   │   │   ├── SecurityConfig.java
    │   │   │   │   │   │   ├── controller/
    │   │   │   │   │   │   │   ├── HomeController.java
    │   │   │   ├── resources/
    │   │   │   │   ├── application.yml

     

    MSA 개발을 위해 multi module 형식으로 프로젝트 구조를 만들었지만, 아직 MSA 도메인에 대한 지식이 많지 않아서

     

    공통으로 security 으로 보안구성을 할 예정이다.

     

     jwt 로그인 구현은 msa에서 사용하기엔 부적절하며, 지금은 반 모놀리식 방식으로 개발을 진행할것이다.

     

    multi module를 구현하는법은 구글링해서 찾아보길바란다.

     

    1.Spring security Config 구성하기 세션 서버 (session-server)

    1-1JwtTokenUtil

     

    @Component
    public class JwtTokenUtil {
    
        private static final String SECRET_KEY = "ei95OZo4Sw+FwtYlHPK/4/n8N8jWZkzcxzQKgvUu93FHBdGvBqNYw98vWK3P9GCj";
    
        public String generateToken(UserDetails userDetails) {
     // 충분히 강력한 키 생성 (256비트)
    
    
            Date now = new Date();
            Date expiryDate = new Date(now.getTime() + 3600000); // Token expires in 1 hour
            String role = userDetails.getAuthorities().stream().map(item->item.getAuthority()).collect(Collectors.joining(","));
            String userName = userDetails.getUsername();
            byte[] apiKeySecretBytes = Base64.getDecoder().decode(SECRET_KEY);
            SecretKey secretKey = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
    
    
            return Jwts.builder()
            .setSubject(userName)
            .setExpiration(expiryDate)
            .claim("role", userDetails.getAuthorities().stream().map(item->item.getAuthority()).collect(Collectors.toList()))
            .signWith(secretKey)
            .compact();
    
        }
    
        public UserDetails getUsernameFromToken(String token) {
             byte[] apiKeySecretBytes = Base64.getDecoder().decode(SECRET_KEY);
            SecretKey secretKey = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
    
            Jws<Claims> jwsClaims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token);
    
            // Extract claims from the parsed JWT token
            Claims claims = jwsClaims.getBody();
            // Get the username from the token's claims
            String username = claims.getSubject();
            List<String> roles = (List<String>)claims.get("role");
            
            List<GrantedAuthority> authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());
    
            UserDetails userDetails = User.builder().username(username).password("username").authorities(authorities).build();
    
            return userDetails;
        }
    
        public boolean validateToken(String token) {
            byte[] apiKeySecretBytes = Base64.getDecoder().decode(SECRET_KEY);
            SecretKey secretKey = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
    
    
            try {
                Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
                return true;
            } catch (JwtException | IllegalArgumentException e) {
                // JWT token validation failed
                e.printStackTrace(); // Handle or log the exception as needed
                return false;
            }
        }
    }

    JwtTokenUtil.java: JWT 생성 및 검증을 담당하는 유틸리티 클래스입니다.

    1-2.RedisConfig

    @Configuration
    @EnableCaching
    public class RedisConfig extends CachingConfigurerSupport {
    
        @Bean
        public LettuceConnectionFactory redisConnectionFactory() {
            return new LettuceConnectionFactory();
        }
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate() {
            RedisTemplate<String, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory());
            return template;
        }
    
        @Bean
        public CacheManager cacheManager() {
            RedisCacheManager cacheManager = RedisCacheManager.create(redisConnectionFactory());
            return cacheManager;
        }
    }

    RedisConfig.java: Redis 연결 및 설정 파일입니다.

     

    1-3. RedisService 

    Service
    @RequiredArgsConstructor
    public class RedisService {
    
        private final RedisTemplate<String, Object> redisTemplate;
    
        public void saveData(String key, Map<String, String> data) {
            HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
            hashOperations.putAll(key, data);
        }
    
        public Map<String, String> getData(String key) {
            HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
            return hashOperations.entries(key);
        }
    }

     

    RedisService : redisTemplate를 사용한 중복 코드를 제거하고 String key인 map value를 받기위해 service를 작성합니다.

     

     

    1-4 JwtAuthenticationFilter

    @Component
    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
      
        private final JwtTokenUtil jwtTokenUtil;
        private final RedisService redisService;
        
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, java.io.IOException {
            String header = request.getHeader("Authorization");
    
            if (header != null && header.startsWith("Bearer ")) {
                String authToken = header.substring(7);
                UserDetails userDetails = jwtTokenUtil.getUsernameFromToken(authToken);
    
                if (userDetails.getUsername() != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    Map<String,String> userData = redisService.getData(userDetails.getUsername());
                    String token = String.valueOf(userData.get(userDetails.getUsername()));
                    
    
                    if (jwtTokenUtil.validateToken(authToken)&&token.equals(authToken)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
            filterChain.doFilter(request, response);
        }
    }

     

    JwtAuthenticationFilter.java: JWT를 사용하여 사용자를 인증하는 필터입니다.

     

    Map<String,String> userData = redisService.getData(userDetails.getUsername());

     

    는 redis서버에서 token정보를 로그인된 정보인지 확인하기위해 가져옵니다.

     

    1-5.SecurityConfig

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final JwtAuthenticationFilter jwtAuthenticationFilter;
        private final CustomSuccessHandler customSuccessHandler;
        private final CustomFailutreHandler customFailutreHandler;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
            http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
            
            http.formLogin()
                .loginProcessingUrl("/auth/login")
                .permitAll()
                .usernameParameter("memberId")
                .passwordParameter("password")
                .successHandler(customSuccessHandler)
                .failureHandler(customFailutreHandler);
    
    
           
        }
    
        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

     

    SecurityConfig.java: Spring Security 설정 파일입니다. JWT 인증 및 Redis 세션 관리를 설정합니다.

     

    FormLong() 메소드를 이용해서 springsecurity에서 제공하는 로그인기능을 사용합니다.

     

    각 파라미터는 'memberId',''password"를 받아오고 로그인 성공하면 , successHandler를 통해 토큰을 발급하고 , 실패하면 failterHandler를 통해 에러를 발생시킵니다.

     

    BcrptPasswordEncoder를 bean으로 만들어 추후에 회원가입시 비밀번호를 암호화시키기위해 사용합니다.

     

     

    1-6 CustomSuccessHandler

    @Component
    @RequiredArgsConstructor
    public class CustomSuccessHandler implements AuthenticationSuccessHandler{
    
        private final RedisService redisService;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
            
            JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();
    
            UserDetails userDetails =(UserDetails) authentication.getPrincipal();
    
            String token = jwtTokenUtil.generateToken(userDetails);
            
            ObjectMapper objectMapper = new ObjectMapper();
            
            AuthResponse authResponse = new AuthResponse(token);
            Map<String,String> userData = new HashMap();
            userData.put("memberId", userDetails.getUsername());
            userData.put("token", token);
            userData.put("role", "ROLE_USER");
            redisService.saveData(userDetails.getUsername(), userData);
           
    
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().write(objectMapper.writeValueAsString(authResponse));
            response.getWriter().flush();        
        }
    
    
    
    
    }

     

    1.7 CustomFailureHandler

     

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final JwtAuthenticationFilter jwtAuthenticationFilter;
        private final CustomSuccessHandler customSuccessHandler;
        private final CustomFailureHandler customFailureHandler;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
            http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
            
            http.formLogin()
                .loginProcessingUrl("/auth/login")
                .permitAll()
                .usernameParameter("memberId")
                .passwordParameter("password")
                .successHandler(customSuccessHandler)
                .failureHandler(customFailureHandler);
    
    
           
        }
    
        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }

     

     

     

    2.회원 가입 개발

    2-1 AuthController

    @RestController
    @RequestMapping(value = {"auth"})
    @RequiredArgsConstructor
    public class AuthController {
    
        
        private final BCryptPasswordEncoder bCryptPasswordEncoder;
    
        private final MemberRepository memberRepository;
    
        
       
    
        @PostMapping("/join")
        public ResponseEntity<?> join(@RequestBody JoinMemberRequest joinMemberRequest) {
            // Authenticate user
            
            Member member = Member.builder().memberId(joinMemberRequest.getMemberId()).password(bCryptPasswordEncoder.encode(joinMemberRequest.getPassword())).build();
    
            memberRepository.save(member);
    
            // If authentication successful, generate JWT token
    
            // Return JWT token in response
            return ResponseEntity.ok().build();
        }
    }

     

    'auth/join'를 토대로 회원가입을 한다.

     

    2-2 MemberRepository

    @Repository
    public interface MemberRepository extends JpaRepository<Member,Long>{
        Member findByMemberId(String memberId);
    }

     

     

    3.로그인된 회원정보를 Response로 받기

    3-1 TestController 개발

    @RestController
    public class TestController {
    
        @RequestMapping(value = "test", method=RequestMethod.GET)
        public ResponseEntity<UserDetails> getData(@AuthenticationPrincipal UserDetails userDetails){
            return ResponseEntity.ok().body(userDetails);
        }
    }

    4.회원가입후 로그인 시도  with POSTMAN

    4-1 회원가입

     

    3-2 로그인 후 토큰값 받기

     

     

    3-3 로그인 토큰을 통해 로그인 정보 값 리턴 받기

     

    header에 토큰값을 넣고 get 요청하면 user 정보를 잘 받아오는것을 볼수있다.

     

     

    이제 세션서버는 다 완성됬습니다.

     

    다음장에서는 세션서버를 중심으로 세션을 유지한채 2개의 rest서버에서 토큰값을 이용한 로그인 과정을 개발 할것입니다.

    728x90
    반응형
Designed by Tistory.