ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Security] JWT(JSON Web Token) 의 코드 구현
    Server/Spring 2020. 5. 17. 23:58

    🐥 깃허브 : 전체 코드 Spring Boot에 Spring Security와 JWT를 사용해 로그인 구현

    1. 동작과정

    Generating JWT

    generate jwt

    1. Client : 로그인 요청 POST (id, pw)
    2. Server : id, pw가 맞는지 확인 후 맞다면 JWT를 SecretKey로 생성
    3. Client : Server에게 받은 JWT를 로컬 or 세션에 저장
    4. Client : 서버에 요청할 때 항상 헤더에 Token을 포함시킴
    5. Server : 요청을 받을 때마다 SecretKey를 이용해 Token이 유효한지 검증
      • 서버만이 SecretKey를 가지고 있기 때문에 검증 가능
      • Token이 검증되면 따로 username, pw를 검사하지 않아도 사용자 인증 가능
    6. Server : response

    AuthenticationManager

    2. Token 유효 검증

    1. 클라이언트의 요청 (Header : Token)
    2. Spring의 Interceptor에 의해 요청이 Intercept됨
    3. 클라이언트에게 제공되었던 Token과 클라이언트의 Header에 담긴 Token 일치 확인
    4. auth0 JWT를 이용해 issuer, expire 검증

    Validating JWT

    validating jwt

    3. Spring Boot Security + JWT 코드

    3-1. dependency 추가

    pom.xml

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
    </dependency>
    • spring-boot-starter-securityjjwt 추가



    3-2. Secret Key 설정

    • Hashing algorithm과 함께 사용할 Secret Key를 설정
    • Secret Key는 Header, Payload와 결합되어 Hash 생성

    application.properties

    jwt.secret=aoneeJjangjwt



    3. JwtUtil

    • JWT를 생성하고 검증하는 역할 수행
    • io.jsonwebtoken.Jwts 라이브러리 사용
    @Component
    public class JwtUtil {
        @Value("${jwt.secret}")
        private String SECRET_KEY;
    
        public String extractUsername(String token) {
            return extractClaim(token, Claims::getSubject);
        }
    
        public Date extractExpiration(String token) {
            return extractClaim(token, Claims::getExpiration);
        }
    
        public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = extractAllClaims(token);
            return claimsResolver.apply(claims);
        }
        private Claims extractAllClaims(String token) {
            return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        }
    
        private Boolean isTokenExpired(String token) {
            return extractExpiration(token).before(new Date());
        }
    
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return createToken(claims, userDetails.getUsername());
        }
    
        private String createToken(Map<String, Object> claims, String subject) {
    
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
        }
    
        public Boolean validateToken(String token, UserDetails userDetails) {
            final String username = extractUsername(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    }

    createToken

    • Token 생성
      • claim : Token에 담을 정보
      • issuer : Token 발급자
      • subject : Token 제목
      • issuedate : Token 발급 시간
      • expiration : Token 만료 시간
        • milliseconds 기준!
    • signWith (알고리즘, 비밀키)



    4. MyUserDetailsService

    • DB에서 UserDetail를 얻어와 AuthenticationManager에게 제공하는 역할
    • 이번에는 DB 없이 하드코딩된 User List에서 get userDetail
    @Service
    public class MyUserDetailsService implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        	if ("user_id".equals(username)) {
                return new User("aonee","$2a$10$VKu6eW.2pHLJn3yeW0eMxuEUBxXCq/b2Vo3HwSqROGI2mmYRnXqpm",new ArrayList<>());
            } else {
                throw new UsernameNotFoundException("User not found with username: " + username);
            }
        }
    }
    • Spring Security 5.0에서는 Password를 BryptEncoder를 통해 Brypt화한다.
    • id : user_id, pw: user_pw로 고정해 사용자 확인
    • 사용자 확인 실패시 throw Exception



    5. LoginController

    • 사용자가 입력한 id, pw를 body에 넣어서 POST API mapping /authenticate
    • 사용자의 id, pw를 검증
    • jwtUtil을 호출해 Token을 생성하고 JwtResponse에 Token을 담아 return ResponseEntity
    @RestController
    @CrossOrigin
    public class LoginController {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private JwtUtil jwtTokenUtil;
    
        @Autowired
        private MyUserDetailsService userDetailsService;
    
    
        @GetMapping("/main")
        public String main(){
            return "Welcome!! This is main page";
        }
    
        @GetMapping("/hello")
        public String hello(){
            return "hello";
        }
    
        @PostMapping("/authenticate")
        public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
            try {
                authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(
                                authenticationRequest.getUsername(),
                                authenticationRequest.getPassword())
                );
    
            } catch (BadCredentialsException e) {
                throw new Exception("Incorrect username or password", e);
            }
    
            final UserDetails userDetails = userDetailsService
                    .loadUserByUsername(authenticationRequest.getUsername());
            final String jwt = jwtTokenUtil.generateToken(userDetails);
            return ResponseEntity.ok(new AuthenticationResponse(jwt));
        }
    
    }
    



    6. AuthenticationRequest

    • 사용자에게서 받은 id, pw를 저장
    @Getter
    @Setter
    public class AuthenticationRequest {
        private String username;
        private String password;
    
        public AuthenticationRequest() {
        }
    
        public AuthenticationRequest(String username, String password) {
            this.username = username;
            this.password = password;
        }
    }
    



    7. AuthenticationResponse

    • 사용자에게 반환될 JWT를 담은 Response
    @Getter
    public class AuthenticationResponse {
        private final String jwt;
    
        public AuthenticationResponse(String jwt) {
            this.jwt = jwt;
        }
    }
    



    8. JwtRequestFilter

    • Client의 Request를 Intercept해서 Header의 Token가 유효한지 검증
    • if 유효한 Token
      • Spring Security의 Authentication을 Setting, to specify that the current user is authenticated
    @Component
    public class JwtRequestFilter extends OncePerRequestFilter {
    
        @Autowired
        private MyUserDetailsService userDetailsService;
    
        @Autowired
        private JwtUtil jwtUtil;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            final String authorizationHeader = request.getHeader("Authorization");
    
            String username= null;
            String jwt = null;
    
            if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
                jwt = authorizationHeader.substring(7);
                username = jwtUtil.extractUsername(jwt);
            }
    
            if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
    
                if(jwtUtil.validateToken(jwt, userDetails)){
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
            chain.doFilter(request,response);
        }
    }
    
    반응형
Designed by Tistory.