๐งธ ํ๋ก์ ํธ ๊ตฌ์กฐ
DB
Framework
Program
๋ฉ์ธ
ํ ์คํธ
๐งธ ํ๋ก์ ํธ ์ฝ๋ ์ค๋ช
- ํจํค์ง ๋ณ๋ก ์ค๋ช & import๋ฌธ ์๋ต
- ํ๋ก์ ํธ์ ์คํ์ ๋ํด์...
-> ์ด๋ฒ ์บ ํ๋ฅผ ํตํด ์ฒ์์ผ๋ก ์คํ๋ง์ ์์ํ๊ฒ ๋์๊ณ , ๊ทธ๋ ๊ธฐ์ ์ด๋ค ๊ธฐ์ ์ ์งํฅํ๊ณ ์ง์ํ๋ ์์ค์ ๊ฐ๋ฐ์ ์ด๋ ค์ธ ๊ฒ์ด๋ผ ํ๋จํ์ผ๋ฉฐ ๊ธฐ๋ณธ์ ์ธ ํํ ๋ฆฌ์ผ๋ก ๋ฐฐ์ด ๊ธฐ์ ์ ์ต๋ํ ์ฌ์ฉํด๋ณด๋ ๊ฒ์ ๋ชฉํ๋ก ํ๊ธฐ์ ๊ธฐ์ ์ฌ์ฉ์ ๋ํด ๋นํจ์จ์ ์ธ ๋ถ๋ถ์ด ์์ ์ ์์ต๋๋ค! ์ํด๋ถํ๋๋ฆฝ๋๋ค๐ข
@PostConstruct
: spring web application server๊ฐ ์ฌ๋ผ์์ bean์ด ์์ฑ, ๊ทธ๋ค์ ํธ์ถpackage me.ver.Authserver7.config;
import...
import javax.annotation.PostConstruct;
/**
* INSERT INTO authority (AUTHORITY_STATUS) values ('ROLE_USER');
* INSERT INTO authority (AUTHORITY_STATUS) values ('ROLE_ADMIN');
*/
@Component
@RequiredArgsConstructor
public class initDb {
private final InitService initService;
@PostConstruct
public void init() {
initService.dbInit();
}
@Component
@Transactional
@RequiredArgsConstructor
static class InitService {
private final AuthorityRepository authorityRepository;
public void dbInit() {
authorityRepository.save(new Authority(AuthorityEnum.ROLE_ADMIN));
authorityRepository.save(new Authority(AuthorityEnum.ROLE_USER));
}
}
}
@RequiredArgsConstructor
: ์ง์ ๋ง๋ TokenProvider์ JwtFilter๋ฅผ SecurityConfig์ ์ ์ฉํ๊ธฐ ์ํด์ ์ฌ์ฉํจ.SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
: ์ง์ ๋ง๋ JwtFilter๋ฅผ Security Filter ์์ ์ถ๊ฐํจ.package me.ver.Authserver7.config;
import...
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
private final StringRedisTemplate redisTemplate;
@Override
public void configure(HttpSecurity http) {
JwtFilter customFilter = new JwtFilter(tokenProvider, redisTemplate);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
: ์ํ๋ฆฌํฐ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ธ์
์ ์ฌ์ฉํ์ง๋ง, ์ด ํ๋ก์ ํธ์์ ์ฌ์ฉํ์ง ์๊ธฐ ์ํด stateless๋ก ์ค์ ..exceptionHandling()
: exception์ ํธ๋ค๋ง ํ ๋ ์ง์ ๋ง๋ ํด๋์ค๋ฅผ ์ถ๊ฐํจ..apply(new JwtSecurityConfig(tokenProvider, redisTemplate));
: JwtFilter๋ฅผ addFilterBefore๋ก ๋ฑ๋กํ JwtSecurityConfig์ ํด๋์ค๋ฅผ ์ ์ฉํจ.package me.ver.Authserver7.config;
import...
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final StringRedisTemplate redisTemplate;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.apply(new JwtSecurityConfig(tokenProvider, redisTemplate));
}
}
UserRequestDto
: ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ์๋ํ email๊ณผ password String์ด ์์.TokenRequestDto
: ํ ํฐ ์ฌ๋ฐ๊ธ์ ์ํด, AccessToken & RefreshToken String์ด ์์.package me.ver.Authserver7.controller;
import...
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
// ํ์๊ฐ์
@PostMapping("/signup")
public ResponseEntity<UserResponseDto> signup(@RequestBody UserRequestDto userRequestDto) {
return ResponseEntity.ok(authService.signup(userRequestDto));
}
// ๋ก๊ทธ์ธ
@PostMapping("/login")
public ResponseEntity<TokenDto> login(@RequestBody UserRequestDto userRequestDto) {
return ResponseEntity.ok(authService.login(userRequestDto));
}
// ํ ํฐ ์ฌ๋ฐ๊ธ
@PostMapping("/reissue")
public ResponseEntity<TokenDto> reissue(@RequestBody TokenRequestDto tokenRequestDto) {
return ResponseEntity.ok(authService.reissue(tokenRequestDto));
}
}
๋ก๊ทธ์ธ์ ํ ์ํ์์ ์ ๋ณด ์กฐํ, ์ ๋ณด ์์ , ํ์ํํด, ๋ก๊ทธ์์์ ์ฒ๋ฆฌํ๋ API
API ์์ฒญ์ด ๋ค์ด์ค๋ฉด -> ํํฐ์์ AccessToken์ ๋ณตํธํํด ์ ์ ์ ๋ณด๋ฅผ ๊บผ๋ด SecurityContext
์ ์ ์ฅํจ.
SecurityContextโ
: Authentication ๊ฐ์ฒด๊ฐ ์ ์ฅ๋๋ ๋ณด๊ด์, ํ์ ์ ์ธ์ ๋ ์ด ๊ฐ์ฒด๋ฅผ ๊บผ๋ด ์ธ ์ ์๋๋ก ์ ๋๋๋ ํด๋์ค(TereadLocal์ ์ ์ฅ๋์ด ์๋ฌด๋ฐ์๋ ์ฐธ์กฐ ๊ฐ๋ฅ), ์ธ์ฆ ์๋ฃ ์ HttpSession์ ์ ์ฅ๋์ด ์ดํ๋ฆฌ์ผ์ด์
์ ๋ฐ์ ๊ฑธ์ณ ์ ์ญ์ ์ฐธ์กฐ ๊ฐ๋ฅ
userService.getMyInfo()
: ๋ด ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด ์ฌ์ฉํจ.
SecurityUtil ํด๋์ค
: ์ ์ ์ ๋ณด์์ UserID๋ง ๋ฐํํ๋ ๋ฉ์๋๊ฐ ์ ์๋์ด ์์.
โก๏ธ ํด๋น ํด๋์ค๋ฅผ ์ปค์คํ
ํ๋ฉด ํ ํฐ์ ๋๋ค์์ ๋ฃ์ ์ ์์ ๊ฒ์ผ๋ก ๋ณด์ฌ, ๋ง์ด ์์๋ณด์์ผ๋ CostomUserDetails, CostomUserDetailsSevice์กฐ์ฐจ๋ ์ธ์๊ฐ์ด ์ ํด์ ธ ์์ด ๊ฐ๋ฅํ ์ง ๋ชจ๋ฅด๊ฒ ์.
์ฐธ๊ณ ๋งํฌ
๐Spring Security UserDetails, UserDetailsService ๋? - ์ฝ์ง์ค์ธ ๊ฐ๋ฐ์
๐UserDetails์ UserDetailsService ์ปค์คํฐ๋ง์ด์ง
๐์ปค์คํฐ๋ง์ด์ง1 - UserDetailsService, UserDetails
๐Spring Security Custom UserDetailsService ๊ตฌํํ๊ธฐ
๐Spring Security - ์ธ์ฆ ์ ์ฐจ ์ธํฐํ์ด์ค ๊ตฌํ (1) UserDetailsService, UserDetails
๐UserDetails์์ User ๊ฐ์ฒด ๊ฐ์ ธ์ค๊ธฐ
๐[Spring] ์คํ๋ง ์ํ๋ฆฌํฐ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํด๋ณด๊ธฐ
package me.ver.Authserver7.controller;
import...
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth/user")
@PreAuthorize("isAuthenticated()")
public class UserController {
private final UserService userService;
// ์ ๋ณด ์กฐํ
@GetMapping("/me")
public ResponseEntity<UserResponseDto> getMyInfo() {
return ResponseEntity.ok(userService.getMyInfo());
}
// ์ ๋ณด ์์
@PutMapping("/update")
public ResponseEntity<UserResponseDto> updateMyInfo(@RequestBody UserUpdateDto dto) {
userService.updateMyInfo(dto);
return ResponseEntity.ok(userService.getMyInfo());
}
// ํ์ ํํด
@DeleteMapping("/me")
public ResponseEntity<String> deleteMember(HttpServletRequest request) {
userService.logout(request);
userService.deleteMember();
return new ResponseEntity<>("ํ์ ํํด ์ฑ๊ณต", HttpStatus.OK);
}
// ๋ก๊ทธ์์
@GetMapping("/logout")
public ResponseEntity<String> logout(HttpServletRequest request) {
userService.logout(request);
return new ResponseEntity<>("๋ก๊ทธ์์ ์ฑ๊ณต", HttpStatus.OK);
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
: ํ๋ผ๋ฏธํฐ๊ฐ ์๋ ์์ฑ์์ ์์ฑ ์ด๋
ธํ
์ด์
์ผ๋ก PROTECTED ์ฌ์ฉ(์ธ๋ถ์์ ์์ฑ์ ์์ฑ ๋ง๊ธฐ ์ํจ)@Builder
: builder ์ฌ์ฉ์ผ๋ก ์์ฑ์์ ์ํฐํฐ ๊ฐ์ฒด ์์ฑpackage me.ver.Authserver7.domain;
import...
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Authority {
@Id
@Column(name = "authority_status")
@Enumerated(EnumType.STRING)
private AuthorityEnum authorityStatus;
public String getAuthorityStatus() {
return this.authorityStatus.toString();
}
@Builder
public Authority(AuthorityEnum authorityStatus) {
this.authorityStatus = authorityStatus;
}
}
Enum
ํ์
์ผ๋ก ๊ด๋ฆฌpackage me.ver.Authserver7.domain;
public enum AuthorityEnum {
ROLE_USER, ROLE_ADMIN
}
@CreatedDate
: ์ํฐํฐ ์์ฑ ์์ ๊ทธ ์๊ฐ์ ์๋์ผ๋ก ๊ธฐ์
ํด์ค@LastModifiedDate
: ์ํฐํฐ ์์ ์์ ๊ทธ ์๊ฐ์ ์๋์ผ๋ก ์์ ํด์คpackage me.ver.Authserver7.domain;
import...
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTime {
@CreatedDate
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime created_at;
@LastModifiedDate
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime modified_at;
}
key
: User ID ๊ฐ์ด ๋ค์ด๊ฐ๊ฒ ๋จvalue
: refresh token String์ด ๋ค์ด๊ฐ๊ฒ ๋จ์ ์ฅ์ ์ด๋์โ
: ์ผ๋ฐ์ ์ผ๋ก Redis๋ฅผ ๋ง์ด ์ฌ์ฉํ์ง๋ง, Redis ์ฌ์ฉ ๊ฒฝํ์ด ์์ด ํด๋น token์ MySQL์ ์ ์ฅ & ๋ก๊ทธ์์ ๊ธฐ๋ฅ์ Redis ์ฌ์ฉ(๋ก๊ทธ์์์ RDB์ ๊ฒฝ์ฐ ๋ฐฐ์น ์์
์ด ํ์ํ๋, ๊ตฌํ์ด ์ด๋ ค์ธ ๊ฒ์ผ๋ก ํ๋จํ์ฌ Redis ์ฌ์ฉํจ)package me.ver.Authserver7.domain;
import...
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class RefreshToken {
@Id
@Column(name = "rt_key")
private String key;
@Column(name = "rt_value")
private String value;
@Builder
public RefreshToken(String key, String value) {
this.key = key;
this.value = value;
}
public RefreshToken updateValue(String token) {
this.value = token;
return this;
}
}
ํ์ ์ ๋ณด : Id(๊ตฌ๋ถ์ ์ํจ)
, email
, password
, nickname
, created_at
, modified_at
@JoinTable
: user_authority ํ
์ด๋ธ์ ์กฐ์ธํ์ฌ ๊ฐ์
ํ ์ฌ์ฉ์์ ๊ถํ ์ ์ฅ
์์ ๊ฐ๋ฅ ์ ๋ณด : password
, username(์ค๋ช
)
, nickname
-> username์ ๊ฒฝ์ฐ ์์ ์ด ๋ถ๊ฐ๋ฅํ๋๋ก ํ ์๋ ์์.
package me.ver.Authserver7.domain;
import...
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id @GeneratedValue
@Column(name = "user_id")
private Long id;
@Column(unique = true)
private String email;
@Column
private String userName;
@Column
private String password;
@Column
private String nickName;
@Column
@CreatedDate
private LocalDateTime createdAt;
@Column
@LastModifiedDate
private LocalDateTime modifiedAt;
@ManyToMany
@Column
@JoinTable(
name = "user_authority",
joinColumns = {@JoinColumn(name="user_id",referencedColumnName = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "authority_status",referencedColumnName = "authority_status")})
private Set<Authority> authorities = new HashSet<>();
@Builder
public User(String email, String userName, String password, String nickName, Set<Authority> authorities, LocalDateTime createdAt, LocalDateTime modifiedAt) {
this.email = email;
this.userName = userName;
this.password = password;
this.nickName = nickName;
this.authorities = authorities;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
}
public void updateMember(UserUpdateDto dto, PasswordEncoder passwordEncoder) {
if(dto.getPassword() != null) this.password = passwordEncoder.encode(dto.getPassword());
if(dto.getUserName() != null) this.userName = dto.getUserName();
if(dto.getNickName()!= null) this.nickName = dto.getNickName();
}
}
๋ก๊ทธ์ธ
, ํ ํฐ ์ฌ๋ฐ๊ธ
์๋ต dtopackage me.ver.Authserver7.dto;
import...
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenDto {
private String grantType;
private String accessToken;
private String refreshToken;
private Long accessTokenExpiresIn;
}
ํ ํฐ ์ฌ๋ฐ๊ธ
๊ด๋ จ ์์ฒญ dto package me.ver.Authserver7.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class TokenRequestDto {
private String accessToken;
private String refreshToken;
}
ํ์๊ฐ์
์ ๋ํ ์๋ต dtopackage me.ver.Authserver7.dto;
import...
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class UserResponseDto {
private String email;
private String userName;
private String nickName;
private Set<Authority> authorities;
public static UserResponseDto of(User user) {
return new UserResponseDto(
user.getEmail(), user.getUserName(), user.getNickName(), user.getAuthorities());
}
}
ํ์๊ฐ์
, ๋ก๊ทธ์ธ
์์ฒญ์ ์ฌ์ฉ๋๋ dtopackage me.ver.Authserver7.dto;
import...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRequestDto {
private String email;
private String password;
private String userName;
private String nickName;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
private Set<Authority> authorities;
public User toMember(PasswordEncoder passwordEncoder, Set<Authority> authorities) {
return User.builder()
.email(email)
.password(passwordEncoder.encode(password))
.userName(userName)
.nickName(nickName)
.createdAt(createdAt)
.modifiedAt(modifiedAt)
.authorities(authorities)
.build();
}
public UsernamePasswordAuthenticationToken toAuthentication() {
return new UsernamePasswordAuthenticationToken(email, password);
}
}
์ ๋ณด ์์
์์ฒญ์ ๋ํ dtopackage me.ver.Authserver7.dto;
import...
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserUpdateDto {
private String userName;
private String password;
private String nickName;
}
SC_FORBIDDEN(403)
์๋ตpackage me.ver.Authserver7.jwt;
import...
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
SC_UNAUTHORIZED(401)
์๋ตpackage me.ver.Authserver7.jwt;
import...
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
OncePerRequestFilter
์ธํฐํ์ด์ค ๊ตฌํ
doFilterInternal
: filtering ๋ก์ง์ด ์ํ๋จ.
SecurityContext
์ ์ ์ ์ ๋ณด ์ ์ฅ(์์ฒญ์ด Controller์ ๋์ฐฉ = SecurityContext์ UserId ์กด์ฌ๊ฐ ๋ณด์ฆ๋จ)
์์
a.String jwt = resolveToken(request);
: request header์์ ํ ํฐ ๊บผ๋ด๊ธฐ
b.if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
: ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ(validateToken)
-> ์ ์ ํ ํฐ O : Authentication ๊ฐ์ ธ์ SecurityContext์ ์ ์ฅ
c.if (redisTemplate.opsForValue().get(jwt) != null) {
: ๋ก๊ทธ์์ ์ฒดํฌ
public String resolveToken(HttpServletRequest request) {
: request header์์ ํ ํฐ ์ ๋ณด ๊บผ๋ด๊ธฐ
package me.ver.Authserver7.jwt;
import...
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
private final TokenProvider tokenProvider;
private final StringRedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
if (redisTemplate.opsForValue().get(jwt) != null) {
throw new RuntimeException("๋ก๊ทธ์์ ๋ ์ฌ์ฉ์ ์
๋๋ค.");
}
Authentication authentication = tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
}
jwt.secret
: application.yml์ ์ ์๋์ด ์์.generateTokenDto
: ๋๊ฒจ๋ฐ์ ์ ์ ์ ๋ณด๋ก accessToken & refreshToken ์์ฑuthentication.getName()
: id
๊ฐ์ ๊ฐ์ ธ์ด.getAuthentication
: ํ ํฐ์ ๋ณตํธํํด accessToken์ ์๋ ์ ๋ณด ๊บผ๋.validateToken
: ํ ํฐ ์ ๋ณด ๊ฒ์ฆparseClaims
: ํ ํฐ์ด ๋ง๋ฃ๋์๋๋ผ๋, ์ ๋ณด๋ฅผ ๊บผ๋ผ ์ ์๋๋ก ํจ.package me.ver.Authserver7.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import me.ver.Authserver7.dto.TokenDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Component
public class TokenProvider {
private static final String AUTHORITIES_KEY = "auth";
private static final String BEARER_TYPE = "bearer";
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30๋ถ
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7์ผ
private final Key key;
//private static final String NICKNAME = "nickName";
//private String nickName;
public TokenProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public TokenDto generateTokenDto(Authentication authentication) {
// ๊ถํ๋ค ๊ฐ์ ธ์ค๊ธฐ
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
//payload ๋ถ๋ถ ์ค์
//Map<String, Object> payloads = new HashMap<>();
//payloads.put("nickname", nickName);
long now = (new Date()).getTime();
Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
String accessToken = Jwts.builder()
.setSubject(authentication.getName()) // payload "sub": "name"
//.claim(NICKNAME, User.getNickname())
//.setClaims(payloads)
.claim(AUTHORITIES_KEY, authorities) // payload "auth": "ROLE_USER"
.setExpiration(accessTokenExpiresIn) // payload "exp": 1516239022 (์์)
.signWith(key, SignatureAlgorithm.HS512) // header "alg": "HS512"
.compact();
String refreshToken = Jwts.builder()
.setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
return TokenDto.builder()
.grantType(BEARER_TYPE)
.accessToken(accessToken)
.accessTokenExpiresIn(accessTokenExpiresIn.getTime())
.refreshToken(refreshToken)
.build();
}
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
if (claims.get(AUTHORITIES_KEY) == null) {
throw new RuntimeException("๊ถํ ์ ๋ณด๊ฐ ์๋ ํ ํฐ์
๋๋ค.");
}
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("์๋ชป๋ JWT ์๋ช
์
๋๋ค.");
} catch (ExpiredJwtException e) {
log.info("๋ง๋ฃ๋ JWT ํ ํฐ์
๋๋ค.");
} catch (UnsupportedJwtException e) {
log.info("์ง์๋์ง ์๋ JWT ํ ํฐ์
๋๋ค.");
} catch (IllegalArgumentException e) {
log.info("JWT ํ ํฐ์ด ์๋ชป๋์์ต๋๋ค.");
}
return false;
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
package me.ver.Authserver7.repository;
import...
import java.util.Optional;
public interface AuthorityRepository extends JpaRepository<Authority, AuthorityEnum> {
Optional<Authority> findByAuthorityStatus(AuthorityEnum authorityStatus);
}
package me.ver.Authserver7.repository;
import...
import java.util.Optional;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
Optional<RefreshToken> findByKey(String key);
Optional<RefreshToken> deleteByKey(String key);
}
package me.ver.Authserver7.repository;
import...
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}
ํ์๊ฐ์
: ์ด๋ฉ์ผ ์ค๋ณต ์ฌ๋ถ ๊ฒ์ฆ -> authorityRepository์์ User ๊ถํ ์ฐพ์ User ๊ฐ์ฒด ํ๋ผ๋ฏธํฐ๋ก ๋๊ธด ํ์ ์ ์ฅ
๋ก๊ทธ์ธ
UsernamePasswordAuthenticationToken
๊ฐ์ฒด ์์ฑ key
: authentication.getName(), value
: tokenDto.getRefreshToken() -> refreshTokent ๊ฐ์ฒด ์์ฑ -> RefreshTokenRepository
์ ์ ์ฅํ ํฐ ์ฌ๋ฐ๊ธ
RefreshTokenRepository
: Authentication๊ฐ์ฒด๋ฅผ ํตํด ๊ฐ์ ธ์จ value์ dto์ refresh Token์ด ๋๊ฐ์์ง ๊ฒ์ฌpackage me.ver.Authserver7.service;
import...
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final AuthorityRepository authorityRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
/**
* ํ์๊ฐ์
*/
@Transactional
public UserResponseDto signup(UserRequestDto userRequestDto) {
if (userRepository.existsByEmail(userRequestDto.getEmail())) {
throw new RuntimeException("์ด๋ฏธ ๊ฐ์
๋์ด ์๋ ์ด๋ฉ์ผ์
๋๋ค.");
}
Authority authority = authorityRepository
.findByAuthorityStatus(AuthorityEnum.ROLE_USER).orElseThrow(()->new RuntimeException("๊ถํ ์ ๋ณด๊ฐ ์์ต๋๋ค."));
Set<Authority> set = new HashSet<>();
set.add(authority);
User user = userRequestDto.toMember(passwordEncoder, set);
return UserResponseDto.of(userRepository.save(user));
}
/**
* ๋ก๊ทธ์ธ
*/
@Transactional
public TokenDto login(UserRequestDto userRequestDto) {
UsernamePasswordAuthenticationToken authenticationToken = userRequestDto.toAuthentication();
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
RefreshToken refreshToken = RefreshToken.builder()
.key(authentication.getName())
.value(tokenDto.getRefreshToken())
.build();
refreshTokenRepository.save(refreshToken);
return tokenDto;
}
/**
* ์ฌ๋ฐ๊ธ
*/
@Transactional
public TokenDto reissue(TokenRequestDto tokenRequestDto) {
if (!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())) {
throw new RuntimeException("Refresh Token ์ด ์ ํจํ์ง ์์ต๋๋ค.");
}
Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getAccessToken());
RefreshToken refreshToken = refreshTokenRepository.findByKey(authentication.getName())
.orElseThrow(() -> new RuntimeException("๋ก๊ทธ์์ ๋ ์ฌ์ฉ์์
๋๋ค."));
if (!refreshToken.getValue().equals(tokenRequestDto.getRefreshToken())) {
throw new RuntimeException("ํ ํฐ์ ์ ์ ์ ๋ณด๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
}
TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
RefreshToken newRefreshToken = refreshToken.updateValue(tokenDto.getRefreshToken());
refreshTokenRepository.save(newRefreshToken);
return tokenDto;
}
}
UserDetailsService
๊ตฌํ ํด๋์คuserdetails
๊ฐ์ฒด๋ก ๋ง๋ค์ด ๋ฆฌํดpackage me.ver.Authserver7.service;
import...
public class CustomUserDetails implements UserDetails {
private String email;
private String password;
private String AUTHORITY;
private boolean ENABLED;
private String nickName;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
auth.add(new SimpleGrantedAuthority(AUTHORITY));
return auth;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return ENABLED;
}
public String getNickname() {
return nickName;
}
public void setNickname(String nickName) {
this.nickName = nickName;
}
}
์ฝ๋๋ฅผ ์
๋ ฅํ์ธ์
package me.ver.Authserver7.service.exception;
public enum AuthServiceException {
EXIST_AUTH("1101", "exist AUTH"),
NO_SUCH_AUTH("1102", "no such AUTH"),
ALREADY_GENERATE_PASSWORD("1103", "already generate password"),
NO_PASSWORD("1104", "no generated password"),
NO_MATCH_PASSWORD("1105", "no match password"),
;
private String message;
private String code;
AuthServiceException(String code, String message) {
this.code = code;
this.message = message;
}
public String getMessage() {
return message;
}
public String getCode() {
return code;
}
}
package me.ver.Authserver7.service.exception;
import...
@Getter
public class AuthServiceValidateException extends RuntimeException {
private final String code;
public AuthServiceValidateException(UserServiceException publisherServiceException) {
super(publisherServiceException.getMessage());
this.code = publisherServiceException.getCode();
}
}
package me.ver.Authserver7.service.exception;
public enum UserServiceException {
EXIST_USER("1101", "exist user"),
NO_SUCH_USER("1102", "no such user"),
ALREADY_GENERATE_PASSWORD("1103", "already generate password"),
NO_PASSWORD("1104", "no generated password"),
NO_MATCH_PASSWORD("1105", "no match password"),
;
private String message;
private String code;
UserServiceException(String code, String message) {
this.code = code;
this.message = message;
}
public String getMessage() {
return message;
}
public String getCode() {
return code;
}
}
d. UserServiceValidateException
package me.ver.Authserver7.service.exception;
import...
@Getter
public class UserServiceValidateException extends RuntimeException {
private final String code;
public UserServiceValidateException(UserServiceException publisherServiceException) {
super(publisherServiceException.getMessage());
this.code = publisherServiceException.getCode();
}
}
JwtFilter
์์SecurityContext
์ ์ธํ
ํ ์ ์ ์ ๋ณด ๊บผ๋id
๋ฅผ ์ ์ฅ -> Long ํ์
์ผ๋ก ํ์ฑSecurityContext
: ThreadLocal์ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ package me.ver.Authserver7.util;
import...
public class SecurityUtil {
private SecurityUtil() { }
public static Long getLoginMemberId() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new RuntimeException("๋ก๊ทธ์ธ ์ ์ ์ ๋ณด๊ฐ ์์ต๋๋ค.");
}
Long LoginId = Long.parseLong(authentication.getName());
return LoginId;
}
}
package me.ver.Authserver7.web.exception;
import...
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
private static void logger(Exception exception) {
log.error(exception.getClass()
.getSimpleName() + " = [{}][{}]",
exception.getClass(), exception.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({UserServiceValidateException.class})
public ApiResponse<ApiResponse.FailureBody> PublisherExHandler(UserServiceValidateException exception) {
logger(exception);
return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + exception.getCode(),
exception.getClass()
.getSimpleName(), exception.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({AuthServiceValidateException.class})
public ApiResponse<ApiResponse.FailureBody> RoomExHandler(AuthServiceValidateException exception) {
logger(exception);
return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + exception.getCode(),
exception.getClass()
.getSimpleName(), exception.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({IllegalStateException.class})
public ApiResponse<ApiResponse.FailureBody> illegalStateExHandler(IllegalStateException exception) {
logger(exception);
String defaultMessage = exception.getMessage();
return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + "001",
exception.getClass()
.getSimpleName(), defaultMessage);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public List<ApiResponse<ApiResponse.FailureBody>> bindingExHandler(BindException exception) {
logger(exception);
List<ApiResponse<ApiResponse.FailureBody>> errorResults = new ArrayList<>();
exception.getAllErrors()
.forEach(error -> {
FieldError fieldError = (FieldError) error;
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
ApiResponse<ApiResponse.FailureBody> bindingException = ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + "1001", exception.getClass()
.getSimpleName(), field + " ํ๋ ์
๋ ฅ์ด ํ์ํฉ๋๋ค. " + defaultMessage);
errorResults.add(bindingException);
});
return errorResults;
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ApiResponse<ApiResponse.FailureBody> exHandler(Exception exception) {
logger(exception);
String defaultMessage = exception.getMessage();
return ApiResponseGenerator.fail(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value() + "1000",
exception.getClass()
.getSimpleName(), defaultMessage);
}
}
package me.ver.Authserver7.web.response;
import...
@Getter
public class ApiResponse<T> extends ResponseEntity<T> {
public ApiResponse(T body, HttpStatus status) {
super(body, status);
}
@Getter
public static class FailureBody implements Serializable {
private Timestamp timestamp;
private String code;
private String error;
private String message;
public FailureBody(final String code, final String error, final String message) {
this.timestamp = new Timestamp(System.currentTimeMillis());
this.code = code;
this.error = error;
this.message = message;
}
}
@Getter
public static class withData<T> implements Serializable {
private Timestamp timestamp;
private String code;
private String message;
private T data;
public withData(T data, String code, String message) {
this.timestamp = new Timestamp(System.currentTimeMillis());
this.code = code;
this.message = message;
this.data = data;
}
}
@Getter
public static class withCodeAndMessage implements Serializable {
private Timestamp timestamp;
private String code;
private String message;
public withCodeAndMessage(String code, String message) {
this.timestamp = new Timestamp(System.currentTimeMillis());
this.code = code;
this.message = message;
}
}
}
package me.ver.Authserver7.web.response;
import...
@UtilityClass
public class ApiResponseGenerator {
public static <D> ApiResponse<ApiResponse.withData> success(final D data,
final HttpStatus status,
final String code, final String message) {
return new ApiResponse<>(new ApiResponse.withData<>(data, code, message), status);
}
public static ApiResponse<ApiResponse.withCodeAndMessage> success(
final HttpStatus status,
final String code, final String message) {
return new ApiResponse<>(new ApiResponse.withCodeAndMessage(code, message), status);
}
public static ApiResponse<ApiResponse.FailureBody> fail(
final HttpStatus status,
final String code, final String error, final String message) {
return new ApiResponse<>(new ApiResponse.FailureBody(code, error, message), status);
}
}
๐งธ ํ์ฌ๊น์ง์ ๊ฒฐ๊ณผ
DB
์ฌ๋ฌ ์ฒ๋ฆฌ ํ
์ฑ๊ณต
์คํจ
์ฑ๊ณต
์คํจ
์ฑ๊ณต
์คํจ
์ฑ๊ณต
์คํจ
์ฑ๊ณต
์คํจ
์ฑ๊ณต
์คํจ
์ฑ๊ณต
์คํจ
๐งธ ํด๊ฒฐ์ด ์ด๋ ค์ด ๊ฒ๋ค
{
"result": "success"
"data": null // ๋ณด๋ด์ค ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด null๋ก ๋ณด๋ด๋ ๊ฒ.
}
{
"result": "success"
"data" {
...
}
}
ํ์ฌ ์ํ
์ฐธ๊ณ ๋งํฌ
๐spring security filter exception์ customํ๊ฒ ์ฒ๋ฆฌํด๋ด
์๋ค
๐์คํ๋ง API ๊ณตํต ์๋ต ํฌ๋งท ๊ฐ๋ฐํ๊ธฐ
๐[Spring] ๊ณตํต response DTO ๋ง๋ค๊ธฐ