회고록(TIL&WIL)

TIL 2023.03.22 Spring Security, React-Redux, Scheduling

만들어나가자 2023. 3. 22. 19:20

Security를 적용한 Web MVC

ServletConfig 에서 Controller를 대신 작성

package com.bit.boot12.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.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class ServletConfig implements WebMvcConfigurer{
	
	@Bean
	PasswordEncoder getPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/").setViewName("index");
//		registry.addViewController("/login").setViewName("login");
		registry.addViewController("/join").setViewName("join");
//		registry.addViewController("/dept").setViewName("dept/index");
		
	}
}

SecurityConfig

package com.bit.boot12.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.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	@Bean
	SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((requests)-> requests
				.antMatchers("/","/join").permitAll() 
				.anyRequest().authenticated()
			)
			.formLogin((form)->form.loginPage("/login").permitAll())
			.logout((logout)->logout.permitAll());
		http.csrf().disable(); // csrf 체크를 해야하는데 테스트를 위해서 비사용 처리
		return http.build();
	}
}

UserDetailsImpl

package com.bit.boot12.service;

import java.time.LocalDate;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import lombok.Getter;

@Getter
public class UserDetailsImpl extends User {

	private LocalDate hiredate;
	private int mgr;
	private String job;
	
	public UserDetailsImpl(String username, String password, Collection<? extends GrantedAuthority> authorities, String job, LocalDate hiredate, int mgr) {
		super(username, password, authorities);
		this.job = job;
		this.hiredate = hiredate;
		this.mgr = mgr;
	}

}

UserDetailServiceImpl

package com.bit.boot12.service;

import java.util.List;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.bit.boot12.domain.EmpRepo;
import com.bit.boot12.domain.EmpVo;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class UserDetailServiceImpl implements UserDetailsService {
	
	final EmpRepo empRepo;
	final PasswordEncoder encoder;

	@Override
	public UserDetails loadUserByUsername(String ename) throws UsernameNotFoundException {
		EmpVo bean = empRepo.findByEname(ename).getEntity();
//		return User.builder()
//				.username(bean.getEname())
//				.password(encoder.encode(String.valueOf(bean.getEmpno())))
//				.authorities("USER")
//				.build();
		return new UserDetailsImpl(bean.getEname(),
				encoder.encode(String.valueOf(bean.getEmpno())),
				List.of(new SimpleGrantedAuthority("USER")),
				bean.getJob(),
				bean.getHiredate(),
				bean.getMgr()
				);
	}

}

HomeController

package com.bit.boot12;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.bit.boot12.service.UserDetailsImpl;

@Controller
public class HomeController {
	@GetMapping("/login")
	public void login() {}
	
	@GetMapping("/dept")
	public String list(Authentication authentication, @AuthenticationPrincipal UserDetails userDetails) {
		System.out.println(userDetails);
		System.out.println(userDetails.getUsername());
		System.out.println(((UserDetailsImpl)userDetails).getJob());
		
		System.out.println(authentication.getPrincipal());
		System.out.println(((UserDetailsImpl)authentication.getPrincipal()).getUsername());
		System.out.println(((UserDetailsImpl)authentication.getPrincipal()).getJob());
		
		SecurityContext context = SecurityContextHolder.getContext();
		String name = context.getAuthentication().getName();
		System.out.println(name);
		return "dept/index";
	}
}

thymeleaf - index.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<meta name="description" content="">
	<meta name="author" content="">
	<title>Please sign in</title>
	<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet"
		integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
	<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet"
		crossorigin="anonymous" />
</head>

<body>
	<div class="container">
		<nav>
			<a href="/" th:href="@{/}" class="btn btn-default" role="button">Home</a>
			<a href="/" th:href="@{/dept}" class="btn btn-default" role="button">List</a>
			<a sec:authorize="{!isAuthenticated()}" href="/" th:href="@{/login}" class="btn btn-default" role="button">Login</a>
			<a sec:authorize="{!isAuthenticated()}" href="/" th:href="@{/join}" class="btn btn-default" role="button">Join</a>
			<a sec:authorize="{isAuthenticated()}" href="/" th:href="@{/logout}" class="btn btn-default" role="button">Logout</a>
		</nav>
		<h1>list page</h1>
		Logged user: <span sec:authentication="name">Bob</span>
	</div>
</body>

</html>

react-redux

프로젝트 생성 및 redux 설치

npx create-react app .
npm i redux react-redux

App3.js

import { Provider, useDispatch, useSelector } from 'react-redux';
import Comp1 from './components/Component1';
import Comp2 from './components/Component2';
import Comp3 from './components/Component3';
import Comp4 from './components/Component4';
import Comp5 from './components/Component5';
import { store } from './components/store';


export default function App() {
  return (
    <Provider store={store}>
        <Comp3/>
        <Comp4/>
        <Comp5/>
    </Provider>
  )
}

store.js

import { createStore } from 'redux';

function reducer (prev={val:1}, action) {
    console.log(prev, action)
    if(action.type === 'up'){
        return {...prev, val:prev.val+1};
    };
    if(action.type === 'upup'){
        return {...prev, val:prev.val+action.su};
    };
    return {...prev};
}
const store = createStore(reducer);

export {store, reducer};

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App3';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

Component1.js

import { useDispatch } from "react-redux";

export default function Comp1() {
    const dispatch = useDispatch();
    return <div><button onClick={e=>dispatch({type:'up'})}>클릭+1</button></div>
}

Component2.js

import { useSelector } from "react-redux";

export default function Comp2() {
    const su = useSelector(state=>state.val);
    return <div><h1>{su}</h1></div>
}

Component3.js

import { useDispatch } from "react-redux";

export default function Comp3() {
    const dispatch = useDispatch();
    return <div><button onClick={e=>dispatch({type:'upup', su:10})}>클릭+10</button></div>
}

Component4.js

import Comp1 from "./Component1";

export default () => <><Comp1/></>

Component5.js

import Comp2 from "./Component2";

export default () => <><Comp2/></>

Spring Scheduling

의존성 추가

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.1.2</version>
    <scope>test</scope>
</dependency>

@EnableScheduling

package com.bit.boot14;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Boot14Application {

	public static void main(String[] args) {
		SpringApplication.run(Boot14Application.class, args);
	}
}

ScheduleTasks

package com.bit.boot14.scheduleingtasks;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduledTasks {

	private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

	private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

	@Scheduled(fixedRate = 5000)
	public void reportCurrentTime() {
		log.debug("The time is now {}", dateFormat.format(new Date()));
	}
}