본문 바로가기
Spring/Spring Security

공통 Session server 와 JWT를 이용한 로그인 구현 with spring security multimodule,spring boot ,redis - 1

by 디찌s 2024. 4. 9.
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
반응형

댓글