-
공통 Session server 와 JWT를 이용한 로그인 구현 with spring security multimodule,spring boot ,redis - 1Spring/Spring Security 2024. 4. 9. 17:07728x90반응형
시작에 앞서 파일구조
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반응형'Spring > Spring Security' 카테고리의 다른 글
[Spring Security에 대한 이해]인증 공급자 (Authentication Provider)(UserDetailsService,UserDetails,GrantedAuthority,UserDetailsManager) - 2 (0) 2023.02.17 [Spring Security에 대한 이해] 기본 구성 - 1 (0) 2023.02.16