본문 바로가기

회고록(TIL&WIL)

TIL 2023.03.21 암호화, JWT, Spring Security

암호화 encryption

암호화 인증 모듈을 쉽게쓰기 위한 코덱 의존성 추가

<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
</dependency>

Spring Security crpto 사용을 위한 의존성 추가

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
    <version>6.0.2</version>
</dependency>

SevletConfig - BCrypt를 이용하여 Encoding하기 위해 @Bean으로 객체 생성

package com.bit.boot09.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableWebMvc
public class ServletConfig extends WebMvcConfigurerAdapter{
	@Bean
	PasswordEncoder getBCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

CryptService

package com.bit.boot09.service;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class CryptService {
	
	// 앞서 생성한 Bcrypt 객체 주입
	final PasswordEncoder passwordEncoder;
	
//	@Value("${secure.key}") properties에 저장해둔 것을 불러올 수 있다.
	String key="oingisprettyintheworld1234567890";
    private String iv = "0123456789abcdef"; // 16byte

	// 암호화
	// 단방향 - 복호화(decoding) 불가능 - 패스워드 => MD5 SHA256
	public String createMd5Encrypt(String msg) {
		return DigestUtils.md5Hex(msg);
	}
	public String createSha256Encrypt(String msg) {
		return DigestUtils.sha256Hex(msg);
	}
	public String createSha512Encrypt(String msg) {
		return DigestUtils.sha3_512Hex(msg);
	}
	
	// 양방향 - 복호화 가능 단, 키를 이용해 인증해야만 가능 =>
	SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
	public String createAESEncrypt(String msg) throws Exception {
		Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
	     c.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes()));
	     byte[] encrypted = c.doFinal(msg.getBytes("UTF-8"));
	     String enStr = new String(Base64.encodeBase64(encrypted));
	     return enStr;
	}
	public String createAESDecrypt(String msg) throws Exception {
		Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
	      c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes()));
	      byte[] byteStr = Base64.decodeBase64(msg.getBytes());
	      return new String(c.doFinal(byteStr), "UTF-8");
	}
	
	// spring security crpto
	public String springEcrypt(String msg) {
		return passwordEncoder.encode(msg);
	}
	public boolean isMatches(String msg, String result) {
		return passwordEncoder.matches(msg, result);
	}
}

CryptServiceTest

package com.bit.boot09.service;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class CryptServiceTest {
	String msg;
	@Autowired
	CryptService cryptService;

	@BeforeEach
	void setUp() throws Exception {
		msg = "abcd1234";
	}

	@Test
	void test() {
		System.out.println(cryptService.createMd5Encrypt(msg));
		System.out.println(cryptService.createSha256Encrypt(msg));
		System.out.println(cryptService.createSha512Encrypt(msg));
		
	}
	
	@Test
	void test02() throws Exception {
		String result1 = cryptService.createAESEncrypt(msg);
		System.out.println(result1);
		String result2 = cryptService.createAESDecrypt(result1);
		System.out.println(result2);
	}
	
	@Test
	void test03() {
		String result = cryptService.springEcrypt(msg);
		System.out.println(result);
		System.out.println(cryptService.isMatches(msg,result));
	}

}

Token

JWT

의존성 추가

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.4</version>
</dependency>

jwtService

package com.bit.boot09.service;

import org.springframework.stereotype.Service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;

@Service
public class JwtService {
	String secretKey = "abcdefg";
	
	// Token 인증 -> 완료되면 user 값 리턴
	public String verify(String token) {
		try {
		    Algorithm algorithm = Algorithm.HMAC256(secretKey);
		    JWTVerifier verifier = JWT.require(algorithm)
		        .build(); //Reusable verifier instance
		    DecodedJWT jwt = verifier.verify(token);
		    return jwt.getClaim("user").asString();
		} catch (JWTVerificationException exception){
		    //Invalid signature/claims
		}
		return null;
	}
	
	// access token 생성
	public String createHs256() {
		try {
		    Algorithm algorithm = Algorithm.HMAC256(secretKey);
		    String token = JWT.create()
		        .withIssuer("auth0")
		        .withClaim("user", "user02")
		        .sign(algorithm);
		    return token;
		} catch (JWTCreationException exception){
			return null;
		}
	}
}

HomeController

package com.bit.boot09.comtroller;

import java.net.HttpCookie;
import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.bit.boot09.model.ResponseDeptVo;
import com.bit.boot09.service.JwtService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class HomeController {
	
	final JwtService jwtService;
	
	@GetMapping
	public String index() {
		return "index";
	}
	
	@GetMapping("/join")
	public String join() {
		return jwtService.createHs256();
	}
	@GetMapping("/join2")
	public String join2(HttpServletResponse resp) {
		// JWT token을 생성하여 cookie에 저장
		String value = "Bearer "+jwtService.createHs256();
		value = Base64.encodeBase64String(value.getBytes());
		Cookie cookie = new Cookie("authorization", value);
		resp.addCookie(cookie);
		return "<h1>쿠키에 등록되었습니다.</h1>";
	}
	
	@GetMapping("/cookie")
	public String cookie(HttpServletRequest req) {
		// 쿠키에 저장된 token 불러오기
		Cookie cookie = req.getCookies()[0];
		String value = cookie.getValue();
		value = new String(Base64.decodeBase64(value));
		return value;
		
	}
	
	@GetMapping("/api/dept")
	public List<ResponseDeptVo> list() {
		return List.of(ResponseDeptVo.builder().deptno(1111).dname("tester1").loc("test").build(),
				ResponseDeptVo.builder().deptno(2222).dname("tester2").loc("test").build(),
				ResponseDeptVo.builder().deptno(3333).dname("tester3").loc("test").build(),
				ResponseDeptVo.builder().deptno(4444).dname("tester4").loc("test").build());
	}
	
}

ServletConfig

package com.bit.boot09.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.bit.boot09.comtroller.DeptControllerIntercepter;
import com.bit.boot09.service.JwtService;

import lombok.AllArgsConstructor;

@Configuration
@EnableWebMvc
@AllArgsConstructor
public class ServletConfig extends WebMvcConfigurerAdapter{
	
	final JwtService jwtService;
	
	@Bean
	PasswordEncoder getBCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	// 컨트롤러 호출 전에 실행되는 DeptControllerInterceptor 추가
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new DeptControllerIntercepter(jwtService));
	}
}

DeptControllerInterceptor

package com.bit.boot09.comtroller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;

import com.bit.boot09.service.JwtService;

public class DeptControllerIntercepter implements HandlerInterceptor{
	
	private JwtService jwtService;

	public DeptControllerIntercepter(JwtService jwtService) {
		this.jwtService = jwtService;
	}

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// 특정 URI에서 token 검증됨
		if(request.getRequestURI().toString().startsWith("/api/")) {
//			System.out.println("HandlerInterceptor...");
			// Authorization에 값이 들어 가 있는지
			String authorization = request.getHeader("authorization");
			if(authorization == null) {
				response.sendRedirect("/login");
				return false;
			}
			// Bearer [token Value] 형태로 저장되어있기 때문에 스플릿
			String token = authorization.split(" ")[1];
			// JWT token이 유효한 지 검사
			String user = jwtService.verify(token);
			System.out.println(user);
			if(user==null) {
				response.sendRedirect("/login");
				return false;
			}
		}
		return true;
	}
}

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>

SecurityConfig

package com.bit.boot10.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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	
	@Bean
	SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((requests)-> requests
				.antMatchers("/","/ex01", "login").permitAll() // 매치되는 uri는 무조건 허용
				.anyRequest().authenticated() // 외의 request는 로그인 필요
			)
			.formLogin((form)->form.loginPage("/login").permitAll()) // 로그인 페이지 지정 + 무조건 허용
			.logout((logout)->logout.logoutSuccessUrl("/").permitAll()); // 로그아웃 후 이동할 url 지정 + 무조건 허용
		http.csrf().disable(); // csrf 체크를 해야하는데 테스트를 위해서 비사용 처리
		return http.build();
	}
	
//	@Bean
//	UserDetailsService userDetailsService() {
//		UserDetails user =
//				User.withDefaultPasswordEncoder()
//					.username("user01")
//					.password("1234")
//					.roles("USER")
//					.build();
//		UserDetails user2 =
//				User.withDefaultPasswordEncoder()
//					.username("user02")
//					.password("4567")
//					.roles("USER")
//					.build();
//		UserDetails user3 =
//				User.withDefaultPasswordEncoder()
//					.username("user03")
//					.password("3456")
//					.roles("USER")
//					.build();
//		return new InMemoryUserDetailsManager(user, user2, user3);
//	}
}

Controller

package com.bit.boot10.controller;

import java.util.Arrays;

import javax.servlet.http.HttpSession;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
	
	@GetMapping("/")
	public String index() {
		return "index";
	}
	@GetMapping("/ex01")
	public String ex01() {
		return "ex01";
	}
	@GetMapping("/ex02")
	public String ex02(HttpSession session, @AuthenticationPrincipal User user) {
		// @AutenticationPrincipal 을 이용해서 로그인한 유저의 정보 불러올 수 있다.
		System.out.println(Arrays.toString(session.getValueNames()));
		System.out.println(user.getUsername());
		return "ex02";
	}
	@GetMapping("/ex03")
	public String ex03() {
		return "ex03";
	}
	@GetMapping("/login")
	public String login() {
		return "login";
	}
}

UserDetailServiceImpl

package com.bit.boot10.service;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserDetailServiceImpl implements UserDetailsService{
	
//	DeptMapper deptMapper;
	final PasswordEncoder passwordEncoder;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//		JPA		// DeptVo bean = findByDname(username);
//		Mybatis	// DeptVo bean = deptMapper.selectOne(username);
		return User.builder()
				.username("scott")
				.password(passwordEncoder.encode("tiger"))
				.roles("USER,ADMIN").build();
	}

}