회원가입 기능 구현이 완료되었으니 이제 로그인 기능을 구현해보려 한다. 이 포스팅을 작성하면서 SpringSecurity에 대한 기본적인 개념과 인증, 인가에 대한 공부를 살짝... 아주 살짝 했다. 이번 포스팅에서는 기본적인 로그인 기능 구현과 Spring Security 초기 설정, JWT 생성하는 부분까지 작성해보려 한다. Spring Security 초기설정부터 작성해 보겠다.
SpringSecuriry 초기 설정
1. 의존성 주입 (build.gradle.kts)
implementation("org.springframework.boot:spring-boot-starter-security")
의존성 주입 후 SpringApplication을 실행시켜 보면 로그인 창이 뜨면서 홈페이지로 접근이 안된다.
아이디는 user, 패스워드는 콘솔창에 뜨는 암호를 넣어주면 된다.
하지만 SpringApplication을 실행시킬 때마다 로그인을 해줘야 하는 건 너무 번거로우니... 추가적인 설정을 해보겠다.
2. SecurityConfig 클래스 작성
초기 설정인만큼 모든 페이지에 대한 접근권한을 부여하였고, 사이트 위변조 방지(csrf)도 해제해 주었다. 이유에 대해 작성하면 너무 길어지기 때문에 이 포스팅을 참고하길 바란다.
package kr.co.vibevillage.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
// springframework.security에서 제공하는 패스워드인코더 인터페이스를 이용하여 객체 생성 후 Bean 등록
// 회원가입 처리되는 비즈니스 로직에서 이 객체를 이용하여 비밀번호 암호화 진행
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 모든 페이지에 대한 접근권한 설정, 사이트 위변조 방지 해제
http.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/**"))
.permitAll())
.csrf((csrf) -> csrf.disable());
return http.build();
}
}
3. .env , application.properties 작성
추가적인 설정이 없으면 아이디는 user, 비밀번호는 콘솔창에 뜨는 정보를 입력했었지만, 여기서 아이디와 비밀번호를 설정하여 준다.
// .env
SECURITY_USER="계정"
SECURITY_PASSWORD="비밀번호"
// application.properties
# SpringSecurity
spring.security.user.name=${SECURITY_USER}
spring.security.user.password=${SECURITY_PASSWORD}
이렇게 설정해 주고 SpringApplication을 실행시키면 홈페이지 접속이 잘 된다.
로그인 기능 구현
Spring Security 초기 설정을 완료하였으니 기본적인 로그인 기능 - 유저로부터 값을 입력받고 데이터베이스와 조회하여 일치하면 메인 화면으로 이동하는 부분을 작성해 보자.
1. HTML 작성
유저로부터 계정과 암호를 입력받고 로그인 버튼을 누르면 form 태그를 이용하여 /login 도메인과 매핑되게 작성하였다.
<form th:action="@{/login}" th:object="${userDTO}" method="GET">
<input class="form-control" id="loginId" type="text" th:field="*{userId}" placeholder="계정"/>
<input id="loginPassword" type="password" th:field="*{userPassword}" placeholder="암호"/>
<button type="submit">로그인</button>
</form>
2. Controller 작성
@GetMapping("/login")을 통해 /login과 매핑되는 코드를 작성하고 Service객체를 이용하여 login 메서드를 호출한다. 암호화된 비밀번호를 반환받아 getPassword라는 변수에 초기화하고, 평문 비밀번호와 암호화된 비밀번호를 비교하여 일치한다면 FormController를 호출한다.(메인화면으로 이동한다)
package kr.co.vibevillage.user.controller;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import kr.co.vibevillage.jwt.config.JWTConfig;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.service.LoginServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class LoginController {
// 서비스 객체 생성
private final LoginServiceImpl loginService;
// 비밀번호 비교를 위한 passwordEncoder 객체 생성
private final PasswordEncoder passwordEncoder;
@GetMapping("/login")
public String login(@RequestParam("userId") String userId, @RequestParam("userPassword") String userPassword) {
System.out.println("--------------------------logincontroller-------------------------");
// 계정을 이용하여 데이터베이스에 있는 암호화된 비밀번호를 가져온다.
String getPassword = loginService.login(userId, userPassword);
// matches 메소드를 이용하여 평문 비밀번호와 암호화된 비밀번호를 비교한다.
if(passwordEncoder.matches(userPassword, getPassword)){
// FormController호출
return "redirect:form";
} else {
return null;
}
}
}
3. Service 작성
mapper 객체를 이용하여 login메서드를 호출한다.
package kr.co.vibevillage.user.model.service;
public interface LoginService {
// 로그인
public String login(String userId, String userPassword);
}
package kr.co.vibevillage.user.model.service;
import kr.co.vibevillage.user.model.mapper.LoginMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService{
// Mapper 객체 생성
private final LoginMapper loginMapper;
@Override
public String login(String userId, String userPassword) {
// mapper 객체를 이용해 login 메서드 호출
return loginMapper.login(userId, userPassword);
}
}
4. Mapper 작성
유저가 입력한 계정을 이용, 데이터베이스로부터 암호화된 비밀번호를 가져온다.
package kr.co.vibevillage.user.model.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LoginMapper {
// 로그인
public String login(String userId, String userPassword);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.vibevillage.user.model.mapper.LoginMapper">
<resultMap id="login" type="kr.co.vibevillage.user.model.dto.UserDTO">
<result column="U_PASSWORD" property="userPassword"></result>
</resultMap>
<select id="login" resultType="string">
select U_PASSWORD from ONAIR.USER_VIBEVILLAGE where U_ID = #{userId}
</select>
</mapper>
JWT
JWT(Json Web Token)란?
JWT 생성하기 전에 간단하게 JWT에 대해 알아보자. Json Web Token의 줄임말로 웹 표준을 따르는, JSON 객체를 사용하여 정보를 전달하는 토큰이다. 필요한 모든 정보를 하나의 객체(토큰)에 담아서 전달하기 때문에 JWT 하나로 인증을 마칠 수 있다. 웹 표준을 따르기 때문에 대부분의 언어를 지원하며 '로그인'과정에서 많이 사용된다. 아래의 그림과 같이 Header.Payload.Signature 형태로 구성되어 있으며 마침표(.)를 이용하여 구분한다.
Header: 서명 시 사용하는 키, 사용할 타입, 서명 암호화 알고리즘에 대한 정보를 담는다.
Payload: 토큰에서 사용할 정보의 조각인 클레임(Claim)이 담겨있다. (등록된 클레임, 공개 클레임, 비공개 클레임으로 나뉜다)
Signature: 헤더에서 정의한 알고리즘 방식을 활용
사용하는 알고리즘의 종류가 많고 상이하기에 그 부분에 대해서 따로 공부가 필요할 듯하다. 추후에 공부하여 기록해 보겠다.
이 링크로 이동하면 JWT를 직접 생성해 볼 수 있다.
JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
jwt.io
JWT를 왜 사용할까?
HTTP는 기본적으로 state-less를 지향한다. state-less(무상태)란 서버-클라이언트 구조에서 서버가 클라이언트의 상태를 가지고 있지 않는 것을 의미하는데, 이는 state-full(세션) 방식보다 비교적 많은 양의 데이터가 반복적으로 전송되어 네트워크 성능이 저하될 수 있고, 데이터 노출로 인해 보안적 문제가 존재한다. 또, 세션은 서버의 세선 저장소에 인증 내용을 담고 있기에 공유되지 않는다. 즉 인가가 불가능한 것이다. 이를 보완하기 위해 JWT를 사용한다.
JWT 생성
1. JWTConfig 작성
여기서 주의할 점은 JWT의 버전이 업데이트되면서 Claims를 생성하여 작업하지 않는다. 공식 문서의 가이드를 참고하여 JwtBuilder를 활용한다.
package kr.co.vibevillage.jwt.config;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JWTConfig {
public String createJwt(String userId, String secretKey, Long expiredMs){
// JWT 버전 올라가면서 Claims 활용 x
// Claims claims = Jwts.claims();
// claims.put("userName", userName);
return Jwts.builder()
.claim("userId", userId)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
2. LoginController 코드 수정
로그인 성공 시JWTConfig 클래스 객체를 이용해서 토큰을 생성하는 createJwt 메서드를 호출한다. 그리고 생성된 토큰을 쿠키에 담아 응답에 더해준다. 쿠키에 담아주는 이유는 JWT가 쿠키에 저장되면 서버에 대한 모든 HTTP 요청에 자동으로 포함되어 지속적인 인증과 정보 교환이 가능해지기 때문이다. 반면에 쿠키는 localStorage에 저장할 수도 있는데, 이는 장단점을 비교해 보고 개발 환경에 맞게 선택하면 되겠다. 이에 대한 내용은 이 포스팅을 참고하여 공부했다. 일단은 기능 구현에 집중하고 나중에 refresh Token을 사용할지... 생성된 토큰을 Redis에 저장하여 활용할지 모르겠다. 이 또한 추후에 포스팅으로 작성해 보겠다.
package kr.co.vibevillage.user.controller;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import kr.co.vibevillage.jwt.config.JWTConfig;
import kr.co.vibevillage.user.model.service.LoginServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginServiceImpl loginService;
private final PasswordEncoder passwordEncoder;
// Jason Web Token 생성을 위한 객체 생성
private final JWTConfig jwt;
// 서명 키 값 가져오기 (application.properties에 작성해둠)
@Value("${jwt.secret}")
private String secretKey;
// 토큰 수명 가져오기 (application.properties에 작성해둠)
@Value("${jwt.expiration_time}")
private Long expiredMs;
@GetMapping("/login")
public String login(@RequestParam("userId") String userId, @RequestParam("userPassword") String userPassword, HttpServletResponse response) {
System.out.println("--------------------------logincontroller-------------------------");
String getPassword = loginService.login(userId, userPassword);
if(passwordEncoder.matches(userPassword, getPassword)){
// JWT 생성
String token = jwt.createJwt(userId, secretKey, expiredMs);
// JWT 담아서 쿠키 생성
Cookie cookie = new Cookie("JWT", token);
cookie.setHttpOnly(true);
cookie.setPath("/");
// 생성된 쿠키 응답에 더해주기
response.addCookie(cookie);
return "redirect:form";
} else {
return null;
}
}
}
이렇게 코드 작성을 마무리하고 토큰이 잘 생성되었는지 확인해 보자.
SpringApplication을 실행 > 로그인 > 개발자도구 > Application > Cookies
JWT라는 이름을 가진 토큰이 아주 예쁘게 잘 생성되어 있다. 이 토큰을 활용하여 인증을 하는 내용을 다음 포스팅에서 다뤄보겠다.
> 참고 자료 <
1. qkre.log
2. 푸르고 개발 블로그
3. 0307 kwon.log
'개발 > Team Project' 카테고리의 다른 글
ep.12 JWT에서 정보를 추출하여 로그인한 회원 확인하기 (0) | 2024.08.21 |
---|---|
ep.11 로그인 기능 구현 - Spring Security + JWT (0) | 2024.08.20 |
ep.09 비밀번호 암호화 (Bcrypt) (0) | 2024.08.13 |
ep08. 본인인증 (SpringBoot + Coolsms + Redis) (0) | 2024.08.09 |
ep07. 회원가입 기능 구현 - 중복검사 (0) | 2024.08.07 |