build.gradle에 library 추가
SpringSecurity 및 Jwt 관련 라이브러리를 build.gradle에 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
JwtTokenProvider 생성
Jwt 토큰 생성 및 유효성 검증을 하는 컴포넌트.
Jwt는 알고리즘과 비밀키를 가지고 토큰을 생성하게 된다.
이때 Claim 정보에는 토큰을 부가적으로 실어 정보를 세팅할 수 있다. Claim 정보에 회원을 구분할 수 있는 값을 세팅하였다가 토큰이 들어오면 해당 값으로 회원을 구분하여 리소스를 제공하면 된다.
그리고 Jwt 토큰에는 expire 시간을 세팅할 수 있다. 토큰 발급 후 일정 시간 이후에는 토큰을 만료시키는 데 사용된다.
resolveToken 메서드는 Http request header에 세팅된 토큰 값을 가져와 유효성을 체크한다.
제한된 리소스를 요청할 때 Http header에 토큰을 세팅하여 호출하면 유효성을 검증하여 사용하자 인증을 할 수 있다.
package com.rest.api.config.security;
// import 생략
@RequiredArgsConstructor
@Component
public class JwtTokenProvider { // JWT 토큰을 생성 및 검증 모듈
@Value("spring.jwt.secret")
private String secretKey;
private long tokenValidMilisecond = 1000L * 60 * 60; // 1시간만 토큰 유효
private final UserDetailsService userDetailsService;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
// Jwt 토큰 생성
public String createToken(String userPk, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userPk);
claims.put("roles", roles);
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 데이터
.setIssuedAt(now) // 토큰 발행일자
.setExpiration(new Date(now.getTime() + tokenValidMilisecond)) // set Expire Time
.signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, secret값 세팅
.compact();
}
// Jwt 토큰으로 인증 정보를 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// Jwt 토큰에서 회원 구별 정보 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 파싱 : "X-AUTH-TOKEN: jwt토큰"
public String resolveToken(HttpServletRequest req) {
return req.getHeader("X-AUTH-TOKEN");
}
// Jwt 토큰의 유효성 + 만료일자 확인
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 생성
Jwt가 유효한 토큰인지 인증하기 위한 필터입니다.
이 필터는 Security설정 시 UsernamePasswordAuthenticationFilter 앞에 세팅한다.
package com.rest.api.config.security;
// import 생략
public class JwtAuthenticationFilter extends GenericFilterBean {
private JwtTokenProvider jwtTokenProvider;
// Jwt Provier 주입
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
// Request로 들어오는 Jwt Token의 유효성을 검증(jwtTokenProvider.validateToken)하는 filter를 filterChain에 등록합니다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
SpringSecurity Configuration
서버에 보안 설정을 적용합니다.
아무나 접근 가능한 리소스는 permitAll()로 세팅하고 나머지 리소스는 다음과 같이 'ROLE_USER'권한이 필요하다고 명시하다.
anyRequest().hasRole("USER") 또는 anyRequest.authenticated()는 동일한 동작을 한다.
해당 filter는 UsernamePasswordAuthenticationFilter 앞에 설정해야 한다.
package com.rest.api.config.security;
// import 생략
@RequiredArgsConstructor
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
@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("/*/signin", "/*/signup").permitAll() // 가입 및 인증 주소는 누구나 접근가능
.antMatchers(HttpMethod.GET, "helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능
.anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); // jwt token 필터를 id/password 인증 필터 전에 넣는다
}
@Override // ignore check swagger resource
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/**",
"/swagger-ui.html", "/webjars/**", "/swagger/**");
}
}
Custom UserDetailsService 정의
토큰에 세팅된 유저 정보로 회원정보를 조회하는 UserDetailsService를 재정의한다.
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
private final UserJpaRepo userJpaRepo;
public UserDetails loadUserByUsername(String userPk) {
return userJpaRepo.findById(Long.valueOf(userPk)).orElseThrow(CUserNotFoundException::new);
}
}
User Entity 수정
SpringSecurity의 보안을 적용하기 위해 User entity에 UserDetails Class를 상속받아 추가 정보를 재정의한다.
roles는 회원이 가지고 있는 권한 정보이고, 가입했을 때는 기본 "ROLE_USER"가 세팅된다.
권한은 회원당 여러 개가 세팅될 수 있으므로 Collection으로 선언한다.
@Builder // builder를 사용할수 있게 합니다.
@Entity // jpa entity임을 알립니다.
@Getter // user 필드값의 getter를 자동으로 생성합니다.
@NoArgsConstructor // 인자없는 생성자를 자동으로 생성합니다.
@AllArgsConstructor // 인자를 모두 갖춘 생성자를 자동으로 생성합니다.
@Table(name = "user") // 'user' 테이블과 매핑됨을 명시
public class User implements UserDetails {
@Id // pk
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long msrl;
@Column(nullable = false, unique = true, length = 30)
private String uid;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(nullable = false, length = 100)
private String password;
@Column(nullable = false, length = 100)
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getUsername() {
return this.uid;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isEnabled() {
return true;
}
}
https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/
https://webfirewood.tistory.com/115
'java spring' 카테고리의 다른 글
자바의 클래스 멤버 변수 초기화 순서 (0) | 2021.12.16 |
---|---|
Spring Boot Rest API image 파일 전송 (0) | 2021.08.15 |
Spring boot Exception 처리 (0) | 2021.07.30 |
DAO, DTO, Service (0) | 2021.07.29 |
[Java Spring MVC] Maria DB와 MyBatis로 로그인 화면 만들기(3) (0) | 2021.02.06 |