시큐리티 로그인/회원가입/인증
로그인
@PostMapping("/signup")
public ResponseEntity<Object> login(@RequestBody MemberDto loginDto)
{
// add check for email exists in DB
if(memberService.existsByEmail(loginDto.getEmail())) {
return new ResponseEntity<>("이메일이 존재합니다.",HttpStatus.BAD_REQUEST);
}
User user = memberService.memberLogin(loginDto);
if(user == null) return new ResponseEntity<>("회원가입 실패",HttpStatus.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>("success", HttpStatus.OK);
}
User user = memberService.memberLogin(loginDto); // 회원가입 진행
@Override
public User memberLogin(MemberDto memberDto) {
try{
String rawPwd= memberDto.getPassword();
String encPwd = bCryptPasswordEncoder.encode(rawPwd);
String nickname = memberDto.getEmail().substring(0,memberDto.getEmail().indexOf("@"));
User member = User.builder()
.nickname(nickname)
.email(memberDto.getEmail())
.password(encPwd)
.role(RoleEnum.USER.getKey())
.build();
return memberRepository.save(member);
}catch (StringIndexOutOfBoundsException s){
return null;
}
}
JwtAuthenticationEntryPoint
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint
{
// 예외시 호출, 허가 받지 않은 유저가 접근 할때
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
로그인
- 로그인시 jwt 토큰 생성
- 타입 : Bearer
@PostMapping("/signin")
public ResponseEntity<JWTAuthResponse> signin(@RequestBody MemberDto memberDto)
{
String token =memberService.userSignin(memberDto);
return new ResponseEntity<>(new JWTAuthResponse(token),HttpStatus.OK);
}
Console 테스트
doFilterInternal, null
2022-11-17 13:55:13.966 INFO 17568 --- [nio-8888-exec-6] p6spy : #1668660913966 | took 1ms | statement | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email=?
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email='j2@naver.com';
서비스 처리
2022-11-17 13:55:13.972 INFO 17568 --- [nio-8888-exec-6] p6spy : #1668660913972 | took 0ms | statement | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email=?
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email='j2@naver.com';
유저 체크 j2@naver.com
유저 존재
2022-11-17 13:55:13.973 INFO 17568 --- [nio-8888-exec-6] p6spy : #1668660913973 | took 0ms | commit | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
;
jwtSecret: jwtsecretkeyisthisissavingquestionandrandomdordergeneration, 604800000
- doFilterInternal, null
- SecurityConfig 의 securityFilterChain() 에 의해 필터 실행
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig
{
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(){
return new JwtAuthenticationFilter();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
return httpSecurity
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.exceptionHandling()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.antMatchers("/api/v1/member/**").permitAll()
.antMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
- .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
JwtAuthenticationFilter
doFilterInternal() 실행
package com.javapp.qg.security;
public class JwtAuthenticationFilter extends OncePerRequestFilter
{
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private PrincipalDetailsService principalDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getJWTfromRequest(request);
System.out.println("doFilterInternal, "+token);
// 토큰 검증
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token))
{
System.out.println("jwt필터검증");
Map<String, Object> userInfo = jwtTokenProvider.getUserFromJWT(token);
UserDetails userDetails = principalDetailsService.loadUserByUsername((String)userInfo.get("email"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// set String Security
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
// Bearer <accessToken>
private String getJWTfromRequest(HttpServletRequest request)
{
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
- 로그인시 token 이 null 이기 때문에 if 실행 X
ServiceImpl
@Override
public String userSignin(MemberDto memberDto) {
User user = memberRepository.findByEmail(memberDto.getEmail()).get();
// null 에러 처리 추후 수정
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
memberDto.getEmail(), memberDto.getPassword()
));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.generateToken(user);
return token;
}
- authenticate에 의해 시큐리티 로그인
- PrincipalDetailsService loadUserByUsername 실행
PrincipalDetailsService
loadUserByUsername()
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService
{
private final MemberRepository memberRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {// 닉네임
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
Optional<User> user = memberRepository.findByEmail(email);
System.out.println("유저 체크 "+ user.get().getEmail());
if(user.isPresent()) {
System.out.println("유저 존재");
User authenUser = user.get();
grantedAuthorities.add(new SimpleGrantedAuthority(authenUser.getRole()));
return new PrincipalDetails(authenUser, grantedAuthorities);
}
return null;
}
}
Console
유저 체크 j2@naver.com
유저 존재
- PrincipalDetails 으로 반환
PrincipalDetails
public class PrincipalDetails implements UserDetails
{
private User user;
private Set<GrantedAuthority> grantedAuthorities;
public PrincipalDetails(User user) {
this.user = user;
}
public PrincipalDetails(User user, Set<GrantedAuthority> grantedAuthorities) {
this.user = user;
this.grantedAuthorities = grantedAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority>collection = grantedAuthorities;
// collection.add(new GrantedAuthority() {
// @Override
// public String getAuthority() {
// return user.getRole();
// }
// });
return collection;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
public String getNickname(){
return user.getNickname();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- String token = tokenProvider.generateToken(user); // 로그인 성공시 토큰 생성
@Component
public class JwtTokenProvider
{
@Value("${app.jwt-secret}")
private String jwtSecret;
@Value("${app.jwt-expiration-milliseconds}")
private int jwtExpirationInMs;
public String generateToken(User user)
{
System.out.println("jwtSecret: "+jwtSecret +", "+ jwtExpirationInMs);
Key key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
Map<String,Object> payloads = new HashMap<>();
payloads.put("nickname", user.getNickname());
payloads.put("email",user.getEmail());
Date currentDate = new Date();
Date expireDate= new Date(currentDate.getTime() + jwtExpirationInMs);
String token = Jwts.builder()
.setSubject("user-info")
.setClaims(payloads)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(key,SignatureAlgorithm.HS256)
.compact();
return token;
}
// get username from the token
public Map<String, Object> getUserFromJWT(String token) {
Map<String, Object> claimMap = null;
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes())) // 시크릿 키 필요
.build()
.parseClaimsJws(token)
.getBody();
claimMap = claims;
return claimMap;
}
public boolean validateToken(String token)
{
System.out.println("토큰 검증");
try {
Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
.build()
.parseClaimsJws(token);
System.out.println("유효한 토큰");
return true;
// }catch(SignatureException ex) {
// throw new BlogAPIException(HttpStatus.BAD_REQUEST, "Invalid JWT signature");
// }catch(MalformedJwtException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Invalid JWT token");
// }catch(ExpiredJwtException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Expired JWT token");
// }catch(UnsupportedJwtException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Unsupported JWT token");
// }catch(IllegalArgumentException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "JWT claims string is empty");
// }
}catch (Exception e){
System.out.println("유효하지 않은 토큰");
return false;
}
}
}
인증
인증(로그인) 없이 접근시 접근 제한
.anyRequest().authenticated()
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("/test")
public String test(){
return "test";
}
그래서 해당 자원 접근시 Bearer 타입 토큰으로 인증
생성된 테스트 로그
doFilterInternal, eyJhbGciOiJIUzI1NiJ9.eyJuaWNrbmFtZSI6ImoyIiwiZW1haWwiOiJqMkBuYXZlci5jb20iLCJpYXQiOjE2Njg2NjI3NDIsImV4cCI6MTY2OTI2NzU0Mn0.funHk5zBeUm5XydoH8BgNQj2H80QcgaTrLlnagIubo4
토큰 검증
유효한 토큰
jwt필터검증
2022-11-17 14:25:49.615 INFO 5232 --- [nio-8888-exec-3] p6spy : #1668662749615 | took 1ms | statement | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email=?
/* select generatedAlias0 from User as generatedAlias0 where generatedAlias0.email=:param0 */ select user0_.id as id1_1_, user0_.email as email2_1_, user0_.nickname as nickname3_1_, user0_.password as password4_1_, user0_.provider as provider5_1_, user0_.provider_id as provider6_1_, user0_.role as role7_1_ from users user0_ where user0_.email='j2@naver.com';
유저 체크 j2@naver.com
유저 존재
2022-11-17 14:25:49.618 INFO 5232 --- [nio-8888-exec-3] p6spy : #1668662749618 | took 1ms | commit | connection 5| url jdbc:mysql://localhost:3306/questionappdb?userSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
;
SecurityConfig 에 의해 JwtAuthenticationFilter 필터 클래스 실행
- jwtTokenProvider.validateToken(token) 에서 토큰 검증, 유효한 토큰 검증
public boolean validateToken(String token)
{
System.out.println("토큰 검증");
try {
Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes()))
.build()
.parseClaimsJws(token);
System.out.println("유효한 토큰");
return true;
// }catch(SignatureException ex) {
// throw new BlogAPIException(HttpStatus.BAD_REQUEST, "Invalid JWT signature");
// }catch(MalformedJwtException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Invalid JWT token");
// }catch(ExpiredJwtException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Expired JWT token");
// }catch(UnsupportedJwtException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "Unsupported JWT token");
// }catch(IllegalArgumentException ex) {
// throw new BlogAPIException(HttpStatus.BAD_GATEWAY, "JWT claims string is empty");
// }
}catch (Exception e){
System.out.println("유효하지 않은 토큰");
return false;
}
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getJWTfromRequest(request);
System.out.println("doFilterInternal, "+token);
// 토큰 검증
if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token))
{
System.out.println("jwt필터검증");
Map<String, Object> userInfo = jwtTokenProvider.getUserFromJWT(token);
UserDetails userDetails = principalDetailsService.loadUserByUsername((String)userInfo.get("email"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// set String Security
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
// 토큰에서 email과 nickname 가져옴
Map<String, Object> userInfo = jwtTokenProvider.getUserFromJWT(token);
// 유저 인증
UserDetails userDetails = principalDetailsService.loadUserByUsername((String)userInfo.get("email"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// set String Security
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
테스트
JWT 토큰 생성시 키 설정 에러
io.jsonwebtoken.security.WeakKeyException
2022-11-17 11:52:37.638 ERROR 15548 --- [nio-8888-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.] with root cause
io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.
The specified key byte array is 72 bits
--> 현재 설정된 Key 의 bit 길이가 "jwtsecret" 으로 72 bits 임.
HMAC-SHA algorithms MUST have a size >= 256 bits
--> 이것을 256 bits 이상으로 설정해야함.
application.yml
app:
jwt-secret: Q4NSl604sgyHJj1qwEkR3ycUeR4uUAt7WJraD7EN3O9DVM4yyYuHxMEbSF4XXyYJkal13eqgB0F7Bq4H
spring boot 2.7.0 이상
jwt 토큰 키 설정
'Back-end > Spring Boot + REST API' 카테고리의 다른 글
스프링부트 API JPA 최적화 (N+1) 컬렉션 조회 최적화, DTO 조회 성능 향상 (0) | 2023.12.21 |
---|---|
스프링부트 API JPA 최적화 ToOne 관계 (N+1 문제) , 페치 조인 (0) | 2023.12.19 |
Spring Boot 실행 프로세스와 Embedded Servers , CLI 실행 (0) | 2022.09.09 |
Spring boot - blog application (REST API) : AWS RDS, Elastic Beanstalk (0) | 2022.09.08 |
Spring boot - blog application (REST API) : Swagger REST API Documentation (0) | 2022.08.09 |
댓글