티스토리 뷰
pom.xml
...
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
...
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
securityConfiguration
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public SecurityConfiguration(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// REST API는 UI를 사용하지 않으므로 기본설정을 비활성화
httpSecurity.httpBasic().disable()
// REST API는 csrf 보안이 필요 없으므로 비활성화
.csrf().disable()
.cors()
// JWT Token 인증방식으로 세션은 필요 없으므로 비활성화
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 인증없이 허용할 매핑. ex)로그인, 회원가입, error페이지,,,
.and()
.authorizeRequests()
.antMatchers("/v1/api/....", "/v1/api/....",
"/v1/api/....").permitAll()
// .antMatchers(HttpMethod.GET, "/product/**").permitAll()
// .antMatchers("**exception**").permitAll()
// 권한체크
.anyRequest().hasAnyRole("ADMIN", "USER")
//.anyRequest().hasRole("ADMIN")
// JWT Token 필터를 id/password 인증 필터 이전에 추가
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);
}
}
이제 요청마다 JWT Filter를 통하게 된다.
JWT 과정을 살펴보자.
JWT 인증 과정
1. 로그인 하면서 jwt 생성
public SignDto signIn(String email, String password) throws RuntimeException {
...
SignDto signDto = SignDto.builder()
.token(jwtTokenProvider.createToken(String.valueOf(user.getEmail()), user.getRoles()))
.build();
...
return signDto;
public String createToken(String email, List<String> roles) {
Claims claims = Jwts.claims().setSubject(email);
claims.put("roles", roles);
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidMillisecond))
.signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, secret 값 세팅
.compact();
return token;
}
이렇게 만든 token을 dto에 담아서 화면까지 가져가서 sessionStorage에 담는다.
submit({
url: "/v1/api/signin",
body: values,
success: (res) => {
if (res.success) {
sessionStorage.setItem("jwt", res.token);
router.push('/')
}
}
});
(위 코드는 해당 프로젝트 환경에 맞게 구현한것이에요. 실제로 어디서나 돌아가는 소스는 아니에요.)
이제 token이 필요할때마다 sessionStorage에서 가져와서 쓸수있음.
2. http요청
submit({
url: "/v1/api/get",
headers: {
...
"X-AUTH-TOKEN": sessionStorage.getItem("jwt"),
...
}
body: {
...
},
success: (res) => {
// 콜백
}
});
headers에 "X-AUTH-TOKEN"라는 key의 값에 token을 담아서 요청 보냄
3. JWT Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
FilterChain filterChain)
throws ServletException,
IOException {
String token = jwtTokenProvider.resolveToken(servletRequest);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
유효한 token을 받았으면, token으로 부터 사용자 정보를 알아내고 spring security에 담아둔다.
(SecurityContextHolder.getContext().setAuthentication(authentication);)
String token = jwtTokenProvider.resolveToken(servletRequest);
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}
http요청(HttpServletRequest)에서 header에서 "X-AUTH-TOKEN"라는 key의 값을 가져옴
=> UI에서 http요청시, header에 "X-AUTH-TOKEN"키에 token을 넣어서 보내야 함.
jwtTokenProvider.validateToken(token)
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
...
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
token : "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbWFpbDAxQGRvby5jb20iLCJyb2xlcyI6WyJST0xFX1VTRVIiXSwiaWF0IjoxNjY4NjA0NjE1LCJleHAiOjE2Njg2MDgyMTV9.6g4Y5_MM8WHvZQL54AL7kDvidP92H954CMBg7ndEc3c" 일경우,
claims : header={alg=HS256},body={sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215},signature=6g4Y5_MM8WHvZQL54AL7kDvidP92H954CMBg7ndEc3c
claims.getBody() : {sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215}
claims.getBody().getExpiration() : Wed Nov 16 23:16:55 KST 2022
token을 decode하려면 jwt.io에 접속해서 복붙하면 알 수 있다.
jwtTokenProvider.getAuthentication(token)
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "",
userDetails.getAuthorities());
}
...
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) : header={alg=HS256},body={sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215},signature=6g4Y5_MM8WHvZQL54AL7kDvidP92H954CMBg7ndEc3c
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody() : {sub=email01@doo.com, roles=[ROLE_USER], iat=1668604615, exp=1668608215}
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject() : email01@doo.com
=> 결론은 token으로부터 email(로그인할때 id로 쓴 값)을 가져옴.
4. UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
...
@Override
public UserDetails loadUserByUsername(String email) {
return userRepository.findByEmail(email);
}
}
그 email로 사용자DB에서 해당 사용자를 조회.
5. User
User entity를 만들면서 org.springframework.security.core.userdetails.UserDetails의 구현체로 만듬.
...
public class User implements UserDetails {
@Id private String email;
private String password;
...
@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());
}
@Override public String getUsername() { return this.email; }
@Overridepublic boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
회원가입 하면서 role값을 받아서 jpa로 save하면 DB에 "ROLE_"이라는 prefix가 붙어서 저장됌.
이후에 SecurityConfiguration에서는 "ROLE_"을 제외한 부분의 role로 권한체크를 함.
'spring(boot)' 카테고리의 다른 글
[microservices]2.소스 추가 (0) | 2023.01.31 |
---|---|
[microservices]1.프로젝트 기본 뼈대(총 5개 프로젝트) (0) | 2023.01.30 |
[junit]jacoco로 coverage보기 (0) | 2022.11.23 |
[java]parameter로 method 넘기기 (0) | 2022.10.30 |
[boot]Scheduling: method를 주기적으로 실행시키려면? (0) | 2022.10.26 |