1) 스프링부트 2.7.0 사용
2) jwt 라이브러리 사용
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.19.2</version> </dependency>
JWT 설정에 전체적인 프로젝트 디렉토리 구조는 다음과 같다.

1. creae database
create user `jwt1`@`localhost` identified by '1234'; create database jwt1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; grant all privileges on jwt1.* to `jwt1`@`localhost` ; use jwt1;
2.라이브러리
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.19.2</version> </dependency> </dependencies>
3. User entitiy 및 UserRepository 생성
User
package com.cos.jwt.model;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.Data;
import lombok.ToString;
@Data
@Entity
@ToString
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
private String password;
private String roles; //USER, ADMIN
public List<String> getRoleList(){
if(this.roles.length() >0) {
return Arrays.asList(this.roles.split(","));
}
return new ArrayList<>();
}
}
UserRepository
package com.cos.jwt.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.cos.jwt.model.User;
//CRUD 함수를 JpaRepository 가 들고 있음.
//@Repostiory 라는 어노테이션이 없어도 loC가 된다. 이유는 JpaRepository 상속했기 때문에.... 가능
public interface UserRepository extends JpaRepository<User, Long>{
//findBy 규칙 -> Username 문법
//select * from user where username =1?
public User findByUsername(String username);
// select * from user where email = ?
//Jpa Query Method
public User findByEmail(String email);
}
4. RestApiController 생성
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.cos.jwt.auth.PrincipalDetails;
import com.cos.jwt.model.User;
import com.cos.jwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequiredArgsConstructor
@Slf4j
public class RestApiController {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/home")
public String home() {
log.info(" home ");
return "<h1>home</h1>";
}
@PostMapping("/token")
public String token() {
return "<h1>token</h1>";
}
@GetMapping("admin/users")
public List<User> users(){
return userRepository.findAll();
}
@PostMapping("join")
public String join(@RequestBody User user) {
log.info("회원 가입 파라미터 : {} " , user.toString());
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
user.setRoles("ROLE_USER");
userRepository.save(user);
return "회원가입완료";
}
//user, manager, admin 권한만 가능
@GetMapping("/api/v1/user")
public String user(Authentication authentication){
PrincipalDetails principalDetails=(PrincipalDetails)authentication.getPrincipal();
log.info("/api/v1/user - Authentication " +principalDetails.getUsername());
return "user";
}
//manager, admin 권한만 가능
@GetMapping("/api/v1/manager")
public String manager(){
return "manager";
}
//admin 권한만 가능
@GetMapping("/api/v1/admin")
public String admin(){
return "admin";
}
}
5. SecurityConfig , CosConfig 생성
SecurityConfig
package com.cos.jwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
import com.cos.jwt.filter.MyFilter3;
import com.cos.jwt.jwt.JwtAuthenticationFilter;
import com.cos.jwt.jwt.JwtAuthorizationFilter;
import com.cos.jwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter{
private final CorsFilter corsFilter;
private final UserRepository userRepository;
// 해당 메서드의 리턴되는 오브젝트를 IoC 로 등록해 준다.
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//The type SecurityContextPersistenceFilter is deprecated
//http.addFilterBefore(new MyFilter3("SecurityFilter"), SecurityContextPersistenceFilter.class);
//시큐리티 필터에 정의된 필터가 FilterConfig 의 필터보다 가장 먼저 실행 처리 된다.
//http.addFilterBefore(new MyFilter3(), BasicAuthenticationFilter.class);
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//1.세션을 사용하지 않겠다.
.and().addFilter(corsFilter) //2.Cross-Origin 정책 사용 X 모든 요청 허용 - @CrossOrigin과의 차이점 : @CrossOrigin은 인증이 없을 때 문제, 그래서 직접 시큐리티 필터에 등록!
.formLogin().disable() //3.폼로그인 비활성화
.httpBasic().disable() //4. basic 비활성화 기본 http 방식 안씀.
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), userRepository))
.authorizeHttpRequests()
.antMatchers("/api/v1/user/**").hasAnyAuthority("ROLE_USER","ROLE_MANAGER","ROLE_ADMIN")
.antMatchers("/api/v1/manager/**").hasAnyAuthority("ROLE_MANAGER","ROLE_ADMIN")
.antMatchers("/api/v1/admin/**").hasAnyAuthority("ROLE_ADMIN")
.anyRequest().permitAll();
}
}
CosConfig
package com.cos.jwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import lombok.extern.slf4j.Slf4j;
@Configuration
@Slf4j
public class CosConfig { //크로스 오리진 정책 설정
/**
Cross-Domain 의 의미는,
IP 주소가 다르거나 Port 번호가 다른 곳에서 리소스를 가져오는 것을 금지한다는 것이다.
*/
@Bean
public CorsFilter corsFilter(){
log.info("cors Filter");
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
CorsConfiguration config=new CorsConfiguration();
config.setAllowCredentials(true); //내서버가 응답을 할 때 json을 자바스크립트에서 처리할 수 있게 할지를 설정하는 것
config.addAllowedOrigin("*");//모든 ip 에 응답을 허용하겠다.
config.addAllowedHeader("*"); //모든 header 에 응답을 허용하겠다.
config.addAllowedMethod("*");// 모든 post, get,put , delete, patch 허용하겠다.
source.registerCorsConfiguration("/api/**", config); // /api/** 로 들어오는 모든 요청들은 config를 따르도록 등록!
return new CorsFilter(source);
}
}
6. PrincipalDetails , PricipalDetailsService
PrincipalDetails
package com.cos.jwt.auth;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.cos.jwt.model.User;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Data
public class PrincipalDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private User user;
public PrincipalDetails(User user) {
this.user = user;
}
/**
* 사용자에게 부여된 권한을 반환합니다. null을 반환할 수 없습니다.
*/
//해당 User 의 권한을 리턴하는 곳!!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities=new ArrayList<>();
user.getRoleList().forEach(res->{
authorities.add(()->res);
});
return authorities;
}
/**
* 사용자를 인증하는 데 사용된 암호를 반환합니다.
*/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* 사용자를 인증하는 데 사용된 사용자 이름을 반환합니다. null을 반환할 수 없습니다.
*/
@Override
public String getUsername() {
return user.getUsername();
}
/**
* 사용자의 계정이 만료되었는지 여부를 나타냅니다. 만료된 계정은 인증할 수 없습니다.
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 사용자가 잠겨 있는지 또는 잠금 해제되어 있는지 나타냅니다. 잠긴 사용자는 인증할 수 없습니다.
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 사용자의 자격 증명(암호)이 만료되었는지 여부를 나타냅니다. 만료된 자격 증명은 인증을 방지합니다.
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 사용자가 활성화되었는지 비활성화되었는지 여부를 나타냅니다. 비활성화된 사용자는 인증할 수 없습니다.
*/
@Override
public boolean isEnabled() {
//우리 사이트 1년동안 회원이 로그인을 안하면!! 휴먼 계정으로 하기로 함.
//현재시간-로긴시간=>1년을 초과하면 return false;
return true;
}
}
PricipalDetailsService
package com.cos.jwt.auth;
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.Service;
import com.cos.jwt.model.User;
import com.cos.jwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
public class PricipalDetailsService implements UserDetailsService{
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity=userRepository.findByUsername(username);
if(userEntity==null) {
throw new UsernameNotFoundException("아이디 혹은 비밀번호가 일치하지 않습니다.");
}
log.info("PrincipalDetailService 의 loadUse userEntity : =>" +userEntity);
return new PrincipalDetails(userEntity);
}
}
7. JwtProperties , JwtAuthenticationFilter , JwtAuthorizationFilter 생성
JwtProperties 토큰 설정값
package com.cos.jwt.jwt;
public interface JwtProperties {
String SECRET = "test1234!@#$"; // 우리 서버만 알고 있는 비밀값
int EXPIRATION_TIME = 10*24*60*60*1000; //10일*24시간*60분*60초 (1/1000초) =864,000,000(10일)
String TOKEN_PREFIX = "Bearer ";
String HEADER_STRING = "Authorization";
}
JwtAuthenticationFilter 로그인시 토큰 생성
package com.cos.jwt.jwt;
import java.io.IOException;
import java.util.Date;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.cos.jwt.auth.PrincipalDetails;
import com.cos.jwt.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**1.세션 방식
* 유저네임, 패스워드 로그인 정상
* 서버쪽 세션 ID 생성
* 클라이언트 쿠키 세션ID를 응답
*
*
* 요청할 때마다 쿠키값 세션 ID를 항상 들고 서버쪽으로 요청하기 때문에
* 서버는 세션 ID가 유효한지 판단해서 유효하면 인증이 필요한 페이지로 접근하게 하면 되요.
*
*
*
*2.JWT 토큰 방식
*유저네밈 , 패스워드 로그인 정상
*JWT 토큰을 생성
*클라이언트 쪽으로 JWT토큰을 응답
*
*
*요청할 때마다 JWT 토큰을 가지고 요청
*서버는 JWT 토큰이 유효한지를 판단 (필터를 만들어야 함)
*
*
*
*3. 처리 과정
*
*1) login(/login)요청을 하면 JSON 데이터 username, password 을 ObjectMapper 이용하여 User로 받는다.
2) Authentication authentication=authenticationManager.authenticate(authenticationToken) 처리 실행함으로 서
loadUserByUsername() 함수가 실행된 후 정상이면 authentication 이 리턴됨.
PrincipalDetailsService 가 호출 loadUserByUsername() 함수 실행됨.
3)PrincipalDetails를 세션에 담고(권한관리를 위해서)
4)JWT토큰을 만들어서 응답해주면 됨.
*
*
*/
//스프링 시큐리티에서 UsernamePasswrodAuthenticationFilter 가 있음.
//login 요청해서 username, passwrod 전송하면 (post)
//UsernamePasswordAuthenticationFilter 동작을 함.
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
private final AuthenticationManager authenticationManager;
//login 요청을 하면 로그인 시도를 위해서 실행되는 함수=> /login
// Authentication 객체 만들어서 리턴 => 의존 : AuthenticationManager
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
log.info("JwtAuthenticationFilter : 로그인 시도중 ");
try {
//JSON 형태의 데이터를 받아와서 User 객체에 넣는다.
// request에 있는 username과 password를 파싱해서 자바 Object로 받기
ObjectMapper objectMapper=new ObjectMapper();
User user =objectMapper.readValue(request.getInputStream(), User.class);
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
//PrincipalDetailsService 의 loadUserByUsername() 함수가 실행된 후 정상이면 authentication 이 리턴됨.
//DB에 있는 username 과 passwrod 가 일치한다.
Authentication authentication=authenticationManager.authenticate(authenticationToken);
//authentication 객체가 session 영역에 저장됨 => 로그인이 되었다는 뜻.
PrincipalDetails principalDetails=(PrincipalDetails)authentication.getPrincipal();
log.info(principalDetails.getUser().getUsername()); //로그인 정상적으로 되었다는 것.
//authentication 객체가 session 영역에 저장을 해야하고 그 방법이 return 해주면 됨.
//리턴의 이유는 권한 관리를 security 가 대신 해주기 때문에 편하력하는 것임.
//굳이 JWT 토큰을 사용하면서 세션을 만들 이유가 없음. 근데 단지 권한 처리때문에 session 넣어 줌
return authentication;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 인증이 안되면 401 에러 난다. 따라서 해 정상적으로 인증이 되었으면
* successfulAuthentication 메소드를 통과하기 때문에 successfulAuthentication 메소드에서
* JWT 토큰을 발행 시킨다.
*{
"timestamp": "2022-06-18T09:22:16.260+00:00",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login"
}
*
*
*
* */
// ★★ successfulAuthentication 메서드는 attemptAuthentication 실행 후 인증이 정상적으로 되었으며 successfulAuthentication 함수가 실행됨.
//JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 respons 해주면 됨.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
log.info("successfulAuthentication 실행됨 : 인증이 완료되었다는 뜻임");
PrincipalDetails principalDetails=(PrincipalDetails)authResult.getPrincipal();
//RSA방식은 아니구 Hash 암호방식
String jwtToken=JWT.create()
.withSubject(principalDetails.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis()+(JwtProperties.EXPIRATION_TIME))) //토큰 만료시간
.withClaim("id", principalDetails.getUser().getId())
.withClaim("username", principalDetails.getUser().getUsername())
.sign(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()));
/**
다음과 같은 토큰을 헤더 Authorization 에 반환
BearereyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjb3PthqDtgbAiLCJpZCI6MSwiZXhwIjoxNjU1NTYzOTc5LCJ1c2VybmFtZSI6InNzYXJsIn0.9jpWJQF8X21tj_VSNlH_ybtIBUqHxJWoUAuPOG4qFDlEoeWY1SS_UJOp1clz92wNJG51EkcTPK1nVavAd1mGqA
*/
response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX+jwtToken);
}
}
JwtAuthorizationFilter
package com.cos.jwt.jwt;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.cos.jwt.auth.PrincipalDetails;
import com.cos.jwt.model.User;
import com.cos.jwt.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
//시큐리티가 filter 가지고 있는데 그 필터중에서 BasicAuthenticationFilter 라는 것이 있음.
//권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어 있음.
//만약에 권한이 인증이 필요한 주소가 아니라면 이 필터를 안 탄다.
@Slf4j
public class JwtAuthorizationFilter extends BasicAuthenticationFilter{
private UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager,UserRepository userRepository) {
super(authenticationManager);
this.userRepository=userRepository;
}
//인증이나 권한이 필요한 주소요청이 있을 때 해당 필터를 타게 됨.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("인증이나 권한이 필요한 주소 요청이 됨");
String jwtHeader=request.getHeader(JwtProperties.HEADER_STRING);
//header가 있는지 확인
if(jwtHeader==null|| !jwtHeader.startsWith(JwtProperties.TOKEN_PREFIX)){
chain.doFilter(request, response);
return;
}
//헤더 Authorization 의 값 "Bearer " 제거 처리
String jwtToken=request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, "");
// 토큰 검증 (이게 인증이기 때문에 AuthenticationManager도 필요 없음)
// 내가 SecurityContext에 집적접근해서 세션을 만들때 자동으로 UserDetailsService에 있는 loadByUsername이 호출됨.
String username=JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes())).build().verify(jwtToken).getClaim("username").asString();
//서명이 정상으로 됨
if(username!=null) {
User userEnity=userRepository.findByUsername(username);
// 인증은 토큰 검증시 끝. 인증을 하기 위해서가 아닌 스프링 시큐리티가 수행해주는 권한 처리를 위해
// 아래와 같이 토큰을 만들어서 Authentication 객체를 강제로 만들고 그걸 세션에 저장!
PrincipalDetails principalDetails=new PrincipalDetails(userEnity);
Authentication authentication=
new UsernamePasswordAuthenticationToken(
principalDetails, //나중에 컨트롤러에서 DI해서 쓸 때 사용하기 편함.
null,// 패스워드는 모르니까 null 처리, 어차피 지금 인증하는게 아니니까!!
principalDetails.getAuthorities());
//강제로 시큐리티의 세션에 접근하여 Authentication 객체를 저장.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
8. PostMan 테스트
1) 회원가입

2) DB 등록 확인

3) 로그인 (( 헤더에서 토큰값을 반환)

4) Headers 에 Authorization 에 키값에 로그인후 반환된 토큰값을 입력후
http://localhost:8080/api/v1/user, http://localhost:8080/api/v1/manager, http://localhost:8080/api/v1/admin 별로 테스트 확인

소스 :
https://github.com/braverokmc79/jwt1
https://github.com/codingspecialist/Springboot-Security-JWT-Easy
참조 및 강의
https://www.youtube.com/playlist?list=PL93mKxaRDidERCyMaobSLkvSPzYtIk0Ah

















댓글 ( 4)
댓글 남기기