시작에 앞서 파일구조
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서버에서 토큰값을 이용한 로그인 과정을 개발 할것입니다.
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security에 대한 이해]인증 공급자 (Authentication Provider)(UserDetailsService,UserDetails,GrantedAuthority,UserDetailsManager) - 2 (0) | 2023.02.17 |
---|---|
[Spring Security에 대한 이해] 기본 구성 - 1 (0) | 2023.02.16 |
댓글