해당 포스팅은 Spring Securty와 jwt를 이용하여 인증, 인가를 구현하기 위해 인프런 Spring Boot JWT Tutorial 강의를 보고 공부한 내용을 남긴 포스팅입니다.
JWT
- RFC7519 인터넷 표준
- 토큰 기반의 인증 시스템에서 가장 널리 사용되는 인증 방식
JWT의 구성
Header
- Signature를 해싱하기 위한 알고리즘 정보
Payload
- 서버와 클라이언트가 주고받는, 시스템에서 실제로 사용될 정보에 대한 내용들을 담고 있다.
Signature
- 토큰의 유효성 검증을 위한 문자열 → 이 토큰이 유효한 토큰인지 검증 가능
JWT의 장점
- 중앙의 인증서버, 데이터 스토어에 대한 의존성 없음. ⇒ 시스템 수평 확장 유리
- Base64 URL Safe Encoding > URL, Cookie, Header 모두 사용 가능
JWT 단점
- payload의 정보가 많아지면 네트워크 사용량이 증가하여 트래픽 크기가 커질 수 있어서 이를 고려한 설계가 필요
- 토큰이 클라이언트에 저장되기 때문에 서버에서 클라이언트의 토큰을 조작할 수 없다.
필요한 의존성
- Spring Web
- Spring Security
- Spring Data JPA
- H2 Database → 메모리에 데이터베이스 저장하는 방법을 이용
- Lombok
- Validation
이 설정대로 api 통신을 시도하면 401 Unauthorized 에러가 발생합니다. 이를 해결하기 위해 어떻게 하면 좋을까요?
401 Unauthorized 해결
SecurityConfig class를 만들어 다음과 같이 설정한다.
package me.yes.tutorial.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/hello").permitAll()
.anyRequest().authenticated();
}
}
@EnableWebSecurity
- 기본 적인 웹 보안을 활성화하겠다는 annotation
추가적인 설정을 위해서 WebSecurityConfigurer을 implements 하거나 WebSecurityConfigurerAdapter를 extends하는 방법이 있다.
⇒ 여기서는 extends 하는 방법을 사용!
.authorizeRequests()
- HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다는 의미
.antMatchers("/api/hello").permitAll()
- /api/hello에 대한 요청은 인증 없이 접근 허용
.anyRequest().authenticated();
- 나머지 요청들에 대해서는 인증이 필요
application.properties 설정
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true
loggin.level.me.yes=DEBUG
- h2 DB를 사용하여 메모리상에 데이터를 저장하는 방식
- jpa 설정
- create-drop : SessionFactory가 시작될때 Drop, Create, Alter를 하고, 종료될때 Drop을 진행한다.
- format_sql, show_sql: sql을 보기좋게 보여주는 설정
User, Authority Class
package me.yes.tutorial.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "user")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
@JsonIgnore
@Id
@Column(name = "user_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@Column(name = "username", length = 50, unique = true)
private String username;
@JsonIgnore
@Column(name = "password", length = 100)
private String password;
@Column(name = "nickname", length = 50)
private String nickname;
@JsonIgnore
@Column(name = "activated")
private boolean activated;
@ManyToMany
@JoinTable(
name = "user_authority",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
private Set<Authority> authorities;
}
import lombok.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Authority {
@Id
@Column(name = "authority_name", length = 50)
private String authorityName;
}
@Entity
- 데이터베이스의 테이블과 1:1 매핑되는 객체
@Getter, @Setter, @Builder, @AllArgsConstructor, @NoArgsConstructor
- Lombok annotation → 실무에서는 주의할 부분들이 있다.
@ManyToMany, @JoinTable
- 테이블 간의 다대다 관계를 일대다, 다대일 관계의 조인 테이블로 정의했다는 뜻
h2-console 확인
SecurtyConfig에 h2-console 하위 모든 요청들과 파비콘 관련 요청은 무시하도록 설정
@Override
public void configure(WebSecurity web) {
web
.ignoring()
.antMatchers(
"/h2-console/**",
"/favicon.ico"
);
}
서버 구동 후, http://localhost:8080/h2-console 페이지로 이동하면 h2를 브라우저에서 확인 가능합니다.
JWT
여기서는 HS512 알고리즘을 사용하기 때문에, Secret Key가 64Byte 이상이 되어야 합니다.
특장 문자열을 Base64로 인코딩한 값을 사용한다.
JWT 관련 의존성 추가
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// jwt 관련 의존성
compile group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtime group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtime group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
test {
useJUnitPlatform()
}
TokenProvider
package me.yes.tutorial.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
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.stereotype.Component;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
@Component
public class TokenProvider implements InitializingBean {
private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private final String secret;
private final long tokenValidityInMilliseconds;
private Key key;
public TokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
this.secret = secret;
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
}
@Override
public void afterPropertiesSet() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity = new Date(now + this.tokenValidityInMilliseconds);
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts
.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
logger.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
logger.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
logger.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
logger.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
}
afterPropertiesSet()
- InitializingBean 인터페이스의 메소드로 Bean Factory에 의해 모든 property가 설정되고 난 뒤 실행되는 메소드
- 실행시점의 custom 초기화 로직이 필요하거나 주입받은 property를 확인하는 용도로 사용된다.
- 여기서는 빈이 생성이 되고 의존성 주입 후에, secret 값을 Base64 Decode해서 key 변수에 할당하기 위함
createToken
- 토큰 만들어줌
getAuthentication
- 토큰에서 클레임을 얻어 Authentication 객체 리턴
validateToken
- 토큰의 유효성 검사
JwtFilter
package me.yes.tutorial.jwt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
public static final String AUTHORIZATION_HEADER = "Authorization";
private TokenProvider tokenProvider;
public JwtFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
String requestURI = httpServletRequest.getRequestURI();
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Authentication authentication = tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
} else {
logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
doFilter
- 필터링 로직
- JWT 토큰의 인증정보를 현재 실행중인 Securty Context에 저장하는 역할 수행
resolveToken
- RequestHeader에서 토큰 정보를 꺼낸다.
jwtSecurityConfig
JwtFilter, TokenProvider를 SecurityConfig에 적용할 때 사용
package me.yes.tutorial.jwt;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenProvider tokenProvider;
public JwtSecurityConfig(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void configure(HttpSecurity http) {
JwtFilter customFilter = new JwtFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
TokenProvider를 주입받아서 JwtFilter를 통해 Security 로직에 필터를 등록합니다.
JwtAuthenticationEntryPoint
유요한 자격증명을 제공하지 않고 접근하려 할때 401 Unathorized 에러를 리턴
package me.yes.tutorial.jwt;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 유효한 자격증명을 제공하지 않고 접근하려 할때 401
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
JwtAccessDeniedHandler
필요한 권한이 존재하지 않는 경우에 403 Forbidden 에러 리턴
package me.yes.tutorial.jwt;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//필요한 권한이 없이 접근하려 할때 403
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
Jwt 설정들을 모두 적용하기 위해 SecurityConfig 수정
package me.yes.tutorial.config;
import me.yes.tutorial.jwt.JwtSecurityConfig;
import me.yes.tutorial.jwt.JwtAccessDeniedHandler;
import me.yes.tutorial.jwt.JwtAuthenticationEntryPoint;
import me.yes.tutorial.jwt.TokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
public SecurityConfig(
TokenProvider tokenProvider,
CorsFilter corsFilter,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtAccessDeniedHandler jwtAccessDeniedHandler
) {
this.tokenProvider = tokenProvider;
this.corsFilter = corsFilter;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(
"/h2-console/**"
,"/favicon.ico"
,"/error"
);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// token을 사용하는 방식이기 때문에 csrf를 disable합니다.
.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// enable h2-console
.and()
.headers()
.frameOptions()
.sameOrigin()
// 세션을 사용하지 않기 때문에 STATELESS로 설정
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/hello").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/signup").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtSecurityConfig(tokenProvider));
}
}
@EnableGlobalMethodSecurty
- @PreAuthorize annotation을 메소드단위로 추가하기 위해서 적용
csrt().disable()
- token을 사용하는 방식이기 때문에 csrf를 disable합니다.
.authenticationEntryPoint(jwtAuthenticationEntryPoint), .accessDeniedHandler(jwtAccessDeniedHandler)
- 에러 핸들링을 위해 직접 만든 설정 추가
package me.yes.tutorial.repository;
import me.yes.tutorial.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = "authorities")
Optional<User> findOneWithAuthoritiesByUsername(String username);
}
extends JpaRepository<>
- 기본적으로 findAll, save 등의 메서드를 사용할 수 있게 된다.
@EntityGraph
- 쿼리가 수행이 될 때, Lazy 조회가 아니고 Eager 조회로 authorities 정보를 같이 가져오게 된다.
findOneWithAuthoritiesByUsername
- Username 기준으로 Authority 정보와 함께 가져온다.
UserDetailsService
SpringSecurity에서 정말 중요한 부분
package me.yes.tutorial.service;
import me.yes.tutorial.entity.User;
import me.yes.tutorial.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional
public UserDetails loadUserByUsername(final String username) {
return userRepository.findOneWithAuthoritiesByUsername(username)
.map(user -> createUser(username, user))
.orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
}
private org.springframework.security.core.userdetails.User createUser(String username, User user) {
if (!user.isActivated()) {
throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
}
List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getUsername(),
user.getPassword(),
grantedAuthorities);
}
}
loadUserByUsername
- 로그인시에 DB에서 유저정보와 권한정보를 가져오고, 이를 기반으로 userdetails.User 객체를 생성하여 리턴한다.
AuthController
package me.yes.tutorial.controller;
import me.yes.tutorial.dto.LoginDto;
import me.yes.tutorial.dto.TokenDto;
import me.yes.tutorial.jwt.JwtFilter;
import me.yes.tutorial.jwt.TokenProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/api")
public class AuthController {
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
this.tokenProvider = tokenProvider;
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
@PostMapping("/authenticate")
public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.createToken(authentication);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);
return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
}
}
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
- .authenticate() 실행시, CustomUserDeatilsService에서 loadUserByUsername가 실행된다. 이걸로 authentication 객체 생성→ authentication 객체로 jwt 생성
- → responseHeader, responseBody에 넣어서 return
- → SecurityContext에 저장
SecurityUtil
package me.yes.tutorial.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Optional;
public class SecurityUtil {
private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
private SecurityUtil() {
}
public static Optional<String> getCurrentUsername() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
logger.debug("Security Context에 인증 정보가 없습니다.");
return Optional.empty();
}
String username = null;
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
username = springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
username = (String) authentication.getPrincipal();
}
return Optional.ofNullable(username);
}
}
getCurrentUsername()
- Security Context의 Authentication 객체를 꺼내와서 이를 이용해 username을 리턴
- → Security COntext에 Authentication 객체가 저장되는 시점은, JwtFilter의 doFilter 메소드에서 Request가 들어올때 SecurityCOntext에 Authentication 객체를 저장해서 사용
UserService
package me.yes.tutorial.service;
import me.yes.tutorial.dto.UserDto;
import me.yes.tutorial.entity.Authority;
import me.yes.tutorial.entity.User;
import me.yes.tutorial.repository.UserRepository;
import me.yes.tutorial.util.SecurityUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public User signup(UserDto userDto) {
if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
throw new RuntimeException("이미 가입되어 있는 유저입니다.");
}
//빌더 패턴의 장점
Authority authority = Authority.builder()
.authorityName("ROLE_USER")
.build();
User user = User.builder()
.username(userDto.getUsername())
.password(passwordEncoder.encode(userDto.getPassword()))
.nickname(userDto.getNickname())
.authorities(Collections.singleton(authority))
.activated(true)
.build();
return userRepository.save(user);
}
@Transactional(readOnly = true)
public Optional<User> getUserWithAuthorities(String username) {
return userRepository.findOneWithAuthoritiesByUsername(username);
}
@Transactional(readOnly = true)
public Optional<User> getMyUserWithAuthorities() {
return SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername);
}
}
signup
- 회원가입 로직 진행
getUserWithAuthorities(String username)
- username을 기준으로 정보를 가져온다
getMyUserWithAuthorities()
- SecurityContext에 저장된 username의 정보만 가져온다.
UserController
package me.yes.tutorial.controller;
import me.yes.tutorial.dto.UserDto;
import me.yes.tutorial.entity.User;
import me.yes.tutorial.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/signup")
public ResponseEntity<User> signup(
@Valid @RequestBody UserDto userDto
) {
return ResponseEntity.ok(userService.signup(userDto));
}
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public ResponseEntity<User> getMyUserInfo() {
return ResponseEntity.ok(userService.getMyUserWithAuthorities().get());
}
@GetMapping("/user/{username}")
@PreAuthorize("hasAnyRole('ADMIN')")
public ResponseEntity<User> getUserInfo(@PathVariable String username) {
return ResponseEntity.ok(userService.getUserWithAuthorities(username).get());
}
}
@PreAuthorize("hasAnyRole('[role]')")
- [role]에 적인 권한을 허용한다.
권한에 맞지 않는 요청이 오면, 403 Forbidden 에러가 발생한다.
참고 자료
'dev log' 카테고리의 다른 글
홈동 개발을 마치면서 (0) | 2021.08.30 |
---|---|
JPA Fetch 전략 (0) | 2021.08.03 |
MariaDB의 장점(MySQL과의 비교) (0) | 2021.07.21 |
[AWS] AWS EC2 서버 환경 만들기 (0) | 2021.04.07 |
[Backend] Servlet 정리 (0) | 2021.04.05 |