03. 스프링 시큐리티 (Spring Security) - JWT
OS | Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836) |
FrameWork | SpringBoot 2.3.1.RELEASE |
Security | spring-boot-starter-security |
EditTool | IntelliJ IDEA 2019.1.3 |
BuildTool | Gradle |
# 참고한 사이트
[Access Defined, Forbidden 핸들링]
의존성
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security
implementation 'io.jsonwebtoken:jjwt-api:0.10.7' // JWT
runtime 'io.jsonwebtoken:jjwt-impl:0.10.7' // JWT
runtime 'io.jsonwebtoken:jjwt-jackson:0.10.7' // JWT
시큐리티 (Security)
@EnableWebSecurity
@RequiredArgsConstructor
public class Security extends WebSecurityConfigurerAdapter {
// @RequiredArgsConstructor 로 인해 @Autowired 의존성 주입 됨
private final JwtTokenProvider jwtTokenProvider;
// 암호화에 필요한 PasswordEncoder 를 Bean 등록
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// authenticationManager 를 Bean 등록합니다.
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable() // rest api 이므로 기본설정 사용안함. 기본설정은 비인증시 로그인폼 화면으로 리다이렉트 된다.
.csrf().disable() // rest api이므로 csrf 보안이 필요없으므로 disable처리.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증할것이므로 세션필요없으므로 생성안함.
.and()
.authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크
.antMatchers("/v1/sign-in", "/v1/sign-up").permitAll() // 가입 및 인증 주소는 누구나 접근가능
.antMatchers(HttpMethod.GET, "/exception/**", "helloworld/**", "/error").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능
.anyRequest().hasRole(MemberType.BRONZE.name()) // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
// .anyRequest().hasRole("BRONZE") // 대체 가능 - 검사할 때 ROLE_BRONZE 로 검사됨
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); // UsernamePasswordAuthenticationFilter 전에 JwtAuthenticationFilter 삽입
}
}
JWT 토큰 생성 (JwtTokenProvider)
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
/** 보호키 */
private String secretKey = "12345678901234567890123456789012";
/** 토큰 지속 시간 mill * sec * min * hour * day */
private long tokenValidTime = 1000L * 60 * 30;
/** Security UserDetailsService */
private final UserDetailsService userDetailsService;
// 객체 초기화
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
System.err.println(String.format("secretKey = %s", secretKey));
}
// JWT 토큰 생성
public String createToken(String userPk, List<MemberType> roles) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
스프링 커스텀 필터 (JwtAuthenticationFilter)
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 헤더에서 JWT 를 받아옵니다.
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 유효한 토큰인지 확인합니다.
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext 에 Authentication 객체를 저장합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
커스텀 서비스 (CustomUserDetailService) - 필터에서 처리할 서비스
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {
return memberRepository.findByMemberId(memberId).orElseThrow(() -> new UsernameNotFoundException(memberId));
// return memberRepository.findByMemberId(memberId).orElseThrow(() -> new MemberNotFoundException(memberId));
}
}
회원 Entity
@Entity
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Builder
@EqualsAndHashCode(of = "memberId")
@ToString
public class Member implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Size(max = 20, message = "0~20 글자사이만 가능합니다")
@Column(name = "member_id", nullable = false, length = 20, unique = true)
private String memberId;
@Column(name = "password", nullable = false, length = 255)
private String password;
@Column(name = "point", nullable = false)
private Long point;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<MemberType> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles;
}
@Override
public String getUsername() {
return memberId;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
회원 타입
@RequiredArgsConstructor
public enum MemberType implements GrantedAuthority {
BRONZE("ROLE_BRONZE", 0L),
SILVER("ROLE_SILVER", 25L),
GOLD("ROLE_GOLD", 50L);
@Getter
private final String type;
@Getter
private final Long level;
@Override
public String getAuthority() {
return type;
}
}
회원 Dto
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginRequestDto {
private String memberId;
private String password;
}
API 컨트롤러 (MemberController)
@RestController
@RequestMapping("/v1")
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
/** 회원가입 */
@PostMapping("/sign-up")
public ResponseEntity<Member> register (@RequestBody MemberLoginRequestDto resources) throws URISyntaxException{
log.info("resources -> {}", resources);
Member member = memberService.register(resources);
log.info("member -> {}", member);
URI url = new URI(String.format("/members/%s", member.getId()));
return ResponseEntity.created(url).body(member);
}
/** 로그인 */
@PostMapping("/sign-in")
public String login(@RequestBody MemberLoginRequestDto memberLoginRequestDto) {
return memberService.login(memberLoginRequestDto);
}
/** 조회 */
@GetMapping("/members/{id}")
public Member details(@PathVariable Long id) {
return memberService.detail(id);
}
}
서비스 (MemberService)
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
public Member detail(Long id) {
return memberRepository.findById(id).orElseThrow(() -> new IllegalArgumentException(""+id));
// return memberRepository.findById(id).orElseThrow(() -> new MemberNotFoundException(id));
}
public Member register(MemberLoginRequestDto resources) {
Boolean existed = memberRepository.existsByMemberId(resources.getMemberId());
if (existed) {
throw new IllegalArgumentException(resources.getMemberId());
// throw new MemberIdDuplicateException(resources.getMemberId());
}
return memberRepository.save(Member.builder()
.memberId(resources.getMemberId())
.password(passwordEncoder.encode(resources.getPassword()))
.point(0L)
.roles(Collections.singletonList(MemberType.BRONZE))
.build());
}
public String login(MemberLoginRequestDto memberLoginRequestDto) {
Member member = memberRepository.findByMemberId(memberLoginRequestDto.getMemberId()).orElseThrow(() ->
new IllegalArgumentException(memberLoginRequestDto.getMemberId())
// new MemberNotFoundException(memberLoginRequestDto.getMemberId())
);
return jwtTokenProvider.createToken(member.getMemberId(), member.getRoles());
}
}
레파지토리 (MemberRepository)
public interface MemberRepository extends JpaRepository<Member, Long> {
Boolean existsByMemberId(String memberId);
Optional<Member> findByMemberId(String memberId);
}
요청 (request)
# sign
POST http://localhost:8080/v1/sign-up
Content-Type: application/json
{
"memberId": "root",
"password": "root"
}
###
# login
POST http://localhost:8080/v1/sign-in
Content-Type: application/json
{
"memberId": "root",
"password": "root"
}
###
# detail
GET http://localhost:8080/v1/members/1
X-AUTH-TOKEN: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyb290Iiwicm9sZXMiOlsiQlJPTlpFIl0sImlhdCI6MTU5NzY2MzIxNywiZXhwIjoxNTk3NjY1MDE3fQ.5mA8bNgtt-jVHvXeLGoFXtTEtPc0SJFZ4Ksn98mZeYg
###
응답 (response)
@Secured 메소드 권한 제어
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
/** 보호키 */
private String secretKey = "12345678901234567890123456789012";
/** 토큰 지속 시간 mill * sec * min * hour * day */
private long tokenValidTime = 1000L * 60 * 30;
/** Security UserDetailsService */
private final UserDetailsService userDetailsService;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
System.err.println(String.format("secretKey = %s", secretKey));
}
// JWT 토큰 생성 - CREATE
public String createToken(String userPk, Set<MemberType> roles) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
claims.put("roles", roles); // 정보는 key / value 쌍으로 저장된다.
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘과
// signature 에 들어갈 secret값 세팅
.compact();
}
// JWT 토큰 생성 - UPDATE
public String createToken(Member member) {
Claims claims = Jwts.claims().setSubject(member.getMemberId());
claims.put("roles", member.getRoles());
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidTime))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}
// 토큰의 유효성 + 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
@Entity
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Builder
@EqualsAndHashCode(of = "memberId")
@ToString
public class Member implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Size(max = 20, message = "0~20 글자사이만 가능합니다")
@Column(name = "member_id", nullable = false, length = 20, unique = true)
private String memberId;
@Column(name = "password", nullable = false, length = 255)
private String password;
@Column(name = "point", nullable = false)
private Long point;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private Set<MemberType> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles;
}
@Override
public String getUsername() {
return memberId;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public void addRoles(Set<MemberType> roles) {
this.roles.addAll(roles);
}
}
@RestController
@RequestMapping("/v1")
@RequiredArgsConstructor
@Slf4j
public class MemberController {
private final MemberService memberService;
// ... 생략
/** 수정 */
@PatchMapping("/members/{id}")
public String update(@RequestBody Member resources, @PathVariable Long id) {
log.info("resources -> {}", resources);
log.info("id -> {}", id);
return memberService.update(resources, id);
}
}
@Service
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 적용
public class MemberService {
private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final PasswordEncoder passwordEncoder;
@Secured("ROLE_GOLD") // ROLE_GOLD 권한만 가능
public Member detail(Long id) {
return memberRepository.findById(id).orElseThrow(() -> new MemberNotFoundException(id));
}
public List<Member> list() {
return memberRepository.findAll();
}
public Member create(Member resources) {
Boolean existed = memberRepository.existsByMemberId(resources.getMemberId());
if (existed) {
throw new MemberIdDuplicateException(resources.getMemberId());
}
return memberRepository.save(Member.builder()
.memberId(resources.getMemberId())
.password(passwordEncoder.encode(resources.getPassword()))
.roles(Collections.singleton(MemberType.BRONZE))
.point(0L)
.build());
}
// ... 생략
public String update(Member resources, Long id) {
Member member = memberRepository.findById(id).orElseThrow(() ->
new MemberNotFoundException(resources.getMemberId())
);
member.setPoint(resources.getPoint());
member.addRoles(resources.getRoles()); // 권한 추가
return jwtTokenProvider.createToken(memberRepository.save(member));
}
}
위 코드에서 기존에 List<MemberType> 이 였던 것을 Set<MemberType> 으로 바꿨습니다.
멤버타입은 중복을 가질 수 없기 때문
위 코드에서 멤버를 확인하는 작업 코드는 제거바람
필터에서 이미 DB 조회를 했기 때문에 메소드에서도 굳이 확인 작업할 필요가 없기 때문
큰 분류 권한 : 익명유저, 유저, 관리자
작은 분류 : 브론즈, 실버, 골드, 자기 자신
처리를 할 필요가 있음
모바일 접근
아이피 주소, 접근 브라우저, 시간 처리하기
비지니스 로직 세부화 하기
세부 권한별로 접근 제한 하기
카카오톡 처럼 원하는 리다이렉트 설정해보기
'10. Spring > Security' 카테고리의 다른 글
인프런 - 실전프로젝트 - 인가 프로세스 DB 연동 웹 계층 구현 (0) | 2020.08.25 |
---|---|
인프런 - 실전프로젝트 - 인증 프로세스 Ajax 인증 구현 (0) | 2020.08.24 |
인프런 - 스프링 시큐리티 (Spring Security) - 실전프로젝트 -인증 프로세스 Form 인증 구현 (0) | 2020.08.20 |
인프런 - 스프링 시큐리티 (Spring Security) - 스프링 시큐리티 주요 아키텍처 이해 (0) | 2020.08.19 |
인프런 - 스프링 시큐리티 (Spring Security) - 기본 API 및 Filter 이해 (0) | 2020.08.18 |
댓글
이 글 공유하기
다른 글
-
인프런 - 실전프로젝트 - 인증 프로세스 Ajax 인증 구현
인프런 - 실전프로젝트 - 인증 프로세스 Ajax 인증 구현
2020.08.24 -
인프런 - 스프링 시큐리티 (Spring Security) - 실전프로젝트 -인증 프로세스 Form 인증 구현
인프런 - 스프링 시큐리티 (Spring Security) - 실전프로젝트 -인증 프로세스 Form 인증 구현
2020.08.20 -
인프런 - 스프링 시큐리티 (Spring Security) - 스프링 시큐리티 주요 아키텍처 이해
인프런 - 스프링 시큐리티 (Spring Security) - 스프링 시큐리티 주요 아키텍처 이해
2020.08.19 -
인프런 - 스프링 시큐리티 (Spring Security) - 기본 API 및 Filter 이해
인프런 - 스프링 시큐리티 (Spring Security) - 기본 API 및 Filter 이해
2020.08.18