20220922_thu
: 웹사이트에 접근 가능여부(로그인)
: 웹사이트에 접근은 가능하나, 권한의 여부(admin_관리자)
단, 실제로 프로젝트에서 시큐리티 기능 구현시, 로그인인증과정이 복잡하고 번거롭기 때문에 가장 마지막단계에서 구현하는 것이 좋다.
server.port= 8081
spring.thymeleaf.cache= false
- porm.xml
- 해당 파일 아무곳이나 커서를 두고 마우스 오른쪽 버튼 클릭하면 spring > Add starter 클릭한다.
- security 중 Spring Security 기능 추가 선택한다.
- 해당 파일이 열리는 것을 확인
<dependency>
태그를 옮겨준다<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>Kh.study</groupId>
<artifactId>SecurityTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SecurityTest</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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>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.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
주의사항
※ 주의 ※ porm파일을 건드릴 때는 다시 프로젝트를 런하고 페이지 실행해야 오류가 안날 수 있다.
차이점
로그인: 아이디와 비밀번호
: 아이디는 소문자로 user를 입력한다.
: 프로젝트 run하면 콘솔창에 비밀번호가 뜬다.
Using generated security password: 4c305450-0f60-4f06-9660-0d4adeb9f6e5
로그인 하면 기본경로인 index.html 페이지가 뜬다.
: 기본경로 localhost:8081/index
security.authorizeHttpRequests().anyRequest().authenticated();
http://localhost:8081/login?error
drop TABLE SECURITY_MEMBER;
CREATE TABLE SECURITY_MEMBER(
MEMBER_ID VARCHAR2(50) PRIMARY KEY
, MEMBER_PW VARCHAR2(100)
, MEMBER_NAME VARCHAR2(50)
, ROLE VARCHAR2(100) -- 관리자권한
);
INSERT INTO SECURITY_MEMBER VALUES('member','member123','일반회원','ROLE_MEMBER');
INSERT INTO SECURITY_MEMBER VALUES('manager','manager123','매니저회원','ROLE_MANAGER');
INSERT INTO SECURITY_MEMBER VALUES('admin','admin123','관리자회원','ROLE_ADMIN');
-- 삽입 후 항상 커밋!!
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>Kh.study</groupId>
<artifactId>SecurityTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SecurityTest</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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>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.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Q.Config를 왜 사용하는가?
A. password를 안전하게 보관해야한다. 코드에 password를 써 두면 git push하는 순간 비밀번호가 외부에 노출된다. API secret key나 각종 access token들도 마찬가지로 겪는 문제이다. 이런 key를 담은 config file들은 암호화하여 push하는 등의 방법이 있기때문이다.
@Configuration
:해당클래스가 설정파일임을 스프링에게 인지시키는 어노테이션
@EnableWebSecurity
:해당 클래스로부터 만들어진 객체가 security 설정 파일임을 인지시켜주는 어노테이션
@bean
bean 객체 어노테이션
- 매개변수로 들어온 httpSecurity 객체를 사용해서
- 인증 및 인가에 대한 제어를 구성할 수 있다.
- @Bean//(객체) : 자동으로 객체만들어주는 어노테이션
- SecurityFilterChain securityFilterChain = ""; 필요할 때마다 자동으로 호출해준다.
csrf()
security.csrf().disable();
:사이트 간의 요청 위조를 방지하는 시큐리티의 기본기능 비활성화해준다.
(스프링 보안공격 방지)
: 이를 하지 않으면, 스프링보안정책문제로 페이지 오류발생으로
package kh.study.security.config;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//--------- @bean 만들기 전 예시-------------------------------------------------------//
// @Bean// 객체만들어주세요
// String string = "java"; -> 자동으로 만들어준다.
// 메소드를 만들면, 스프링이 필요할때 알아서 호출해준다.
// 단, 리턴되는 자료형에서 소문자로 만들어 만들어준다.String - > string
// public String aaa(String name) {
// return name;
// }
// public void bbb() {
// String name = aaa("java");
// System.out.println(name);
// }
//-------------------------------------------------------------------------------------//
@Bean
public SecurityFilterChain filterChain(HttpSecurity security) throws Exception {
//-------------------------------------------------------------------------//
// throws Exception : //
// 해석) 해당 메소드 예외발생시 예외처리안하고 전가시킨다. //
//-------------------------------------------------------------------------//
//-------------------------------<방법 1_기본 >----------------------------//
// ① security.authorizeRequests().antMatchers("/index").permitAll(); //
// 해석) 로그인 없이도 누구에게나 허용하도록하겠다. //
// authorizeHttpRequests() : 권한(인증,인가) 에 대한 설정을 시작하겠다. //
// antMatchers("/index") : /index 요청에 대한 설정을 하겠다. //
// permitAll() : 인증,인가 절차없이 누구에게나 허용하겠다. //
//-------------------------------------------------------------------------//
//-------------------------------------------------------------------------//
// ② security.authorizeRequests().antMatchers("/member").authenticated(); //
// 해석) /member 요청경로도 인증을 받아야만 접근가능하다. //
//-------------------------------------------------------------------------//
//-------------------------------------------------------------------------//
// ③ security.authorizeRequests().anyRequest().authenticated(); //
// 해석) 모든 요청은 로그인(인증)을 하지 않으면 접근 거부한다. //
// anyRequest() : 어떤 요청이 들어와도 //
// authenticated() : 인증(로그인)을 받아야 접근가능하다. //
//-------------------------------------------------------------------------//
//----------------------------------------------------------------------------//
// ④ security.authorizeRequests().antMatchers("/manager").hasRole("MANAGER") //
// ⑤ security.authorizeRequests().antMatchers("/admin").hasRole("ADMIN") //
// hasRole() : //
// 해석) //
// /manager 라는 요청으로 MANAGER라는 인가가 있어야 한다. //
// /admin 라는 요청으로 ADMIN라는 인가가 있어야 한다. //
//----------------------------------------------------------------------------//
//-------------------------------<방법 1_기본 >-------------------------------//
//-------------------------------<방법 2_요약> -------------------------------//
// 한줄로 연결하여 요약이 가능한 이유)
// : 리턴타입이 동일하기 때문에 연결하여 사용 가능하다
security.authorizeRequests()
.antMatchers("/index").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/member").authenticated()
.antMatchers("/manager").hasRole("MANAGER")
.antMatchers("/admin").hasRole("ADMIN")
//.anyRequest().authenticated()
.and().formLogin().loginPage("/login")
.defaultSuccessUrl("/index")
.failureUrl("/loginFail");
//----------------------------------------------------------------------------------------------//
// 1) 인증이 되지 않았을 경우 스프링이 기본 제공하는 login 페이지로 이동
// .and().formLogin();
//----------------------------------------------------------------------------------------------//
// 2) 인증이 되지 않았을 경우 /login 페이지로 이동
// (주의사항) 단, 로그인 페이지를 가는데 전체허용 permitAll()을 하지않은채로 페이지이동하면,
// 로그인하러가는 페이지이동을 하는데에도 로그인인증을 받아야하는게 무한반복 호출하여 오류가 발생한다.
// .and().formLogin().loginPage("/login");
//----------------------------------------------------------------------------------------------//
//-------------------------------------------------------------------------//
// security.formLogin(); //
// 해석) 인증이 되지 않을 경우, 자동으로 로그인 페이지로 이동한다. //
// 로그인 인증방법) //
// 콘솔창 가장 마지막 비밀번호 값을 입력하고 아이디도 user입력 시 가능하다.//
//-------------------------------------------------------------------------//
//.defaultSuccessUrl("/index") : 로그인 성공시 이동경로
//.failureUrl("/loginFail"); : 로그인 실패시 이동경로
//-------------------------------<방법 2_요약> -------------------------------------------------//
//------------------ < manager 와 admin 로그인 할 때, 비교 >-----------------------------------------------------------//
// 공통점) 둘 다 페이지 이동시, 로그인 인증을 해야 페이지 창이 뜬다.
// 차이점) admin 은 접근권한을 가진 인가와 인증 둘다 가능하고, manager는 인증만 가능하다.
// admin으로 로그인 후, manager 로그인 가능하지만,
// manager 로그인 후, admin 페이지로 이동시, 로그인은 가능하지만(인증) 접근권한이 없어서 오류페이지가 뜬다.
//------------------------------------------------------------------------------------------------------------------//
//---------------------------------------------------------------------------//
security.exceptionHandling().accessDeniedPage("/accessDenied");
// 위의 manager 페이지에서 admin 페이지 이동시,
// 권한 문제로 접근거부 될 때 기본적으로 이동할 페이지를 지정 할 수 있다.
// exceptionHandling().accessDeniedPage("/페이지명")
// : 페이지명으로 이동하면서 오류페이지가 아닌 접근페이지로 이동 가능하다.
//---------------------------------------------------------------------------//
//---------------------------------------------------------------------------//
security.logout().invalidateHttpSession(true)
.logoutSuccessUrl("/index");
// 로그아웃하면 세션에 저장된 데이터를 자동 삭제한다.
// 로그아웃하면 자동으로 /index 페이지로 이동한다.
//---------------------------------------------------------------------------//
//---------------------------------------------------------------------------//
security.csrf().disable();
// 사이트 간의 요청 위조를 방지하는 시큐리티의 기본기능 비활성화해준다.
// 스프링 보안공격 방지
// : 이를 하지 않으면, 스프링보안정책문제로 페이지 오류발생으로
//---------------------------------------------------------------------------//
return security.build();
}
////////////// [ 인증 및 인가 테스트를 위한 계정 생성 메소드 ] ///////////////////////////////////////////
// @Bean
// public InMemoryUserDetailsManager userDetailService() {
// List<UserDetails> userList = new ArrayList<>();
//
// //----------리스트 데이터 추가하기 : 방법 .add()-------------//
//
// userList.add(User.withUsername("manager")
// .password("{noop}manager123")
// .roles("MANAGER")
// .build());
//
// userList.add(User.withUsername("admin")
// .password("{noop}admin123")
// .roles("ADMIN")
// .build());
// //------------------------------------------------------------//
//
// //-------------------------------------------------------------------------//
// // Q. manager 도 로그인 후 admin 페이지 접근가능하도록 하려면? ------------//
// // "메소드 오버로딩" 사용하면 가능하다.
// // 메소드명이 동일하면 중복되어 사용 불가능하지만,
// // 매소드명이 다르거나 매개변수가 다르면 사용가능하다.
// // roles(" "," ",...," ") : 무한으로 매개변수가 들어갈 수 있다.
// //-------------------------------------------------------------------------//
// // userList.add(User.withUsername("admin")
// // .password("{noop}admin123")
// // .roles("ADMIN","MANAGER")
// // .build());
// //-------------------------------------------------------------------------//
//
// return new InMemoryUserDetailsManager(userList);
//}
//------------------ 위의 manager 와 admin 로그인의 비교 ---------------------------------------------------------//
// 공통점) 둘 다 페이지 이동시, 로그인 인증을 해야 페이지 창이 뜬다.
// 차이점) admin 은 접근권한을 가진 인가와 인증 둘다 가능하고, manager는 인증만 가능하다.
// admin으로 로그인 후, manager 로그인 가능하지만,
// manager 로그인 후, admin 페이지로 이동시, 로그인은 가능하지만(인증) 접근권한이 없어서 오류페이지가 뜬다.
//----------------------------------------------------------------------------------------------------------------//
}
package kh.study.security.controller;
@Controller
public class SecurityController {
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/home")
public String home() {
return "home";
}
@GetMapping("/manager")
public String manager() {
return "manager";
}
@GetMapping("/admin")
public String admin() {
return "admin";
}
@GetMapping("/accessDenied")
public String accessDenied() {
return "accessDenied";
}
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/loginFail")
public String loginFail(Model model) {
//---------------------------------------------------------------------------//
model.addAttribute("failMsg", "아이디 혹은 비밀번호가 잘못 입력되었습니다.");
// login 페이지에, 뒤의 문자열로 failMsg값을 던져준다.
// 로그인 실패시, div영역으로 failMsg값이 화면에 나타난다
//---------------------------------------------------------------------------//
return "login";
}
}
package kh.study.security.service;
public interface MemberService {
//로그인
MemberVO login(String memberId);
}
package kh.study.security.service;
@Service("/memberService")
public class MemberServiceImpl implements MemberService {
@Autowired
private SqlSessionTemplate sqlSession;
//로그인
@Override
public MemberVO login(String memberId) {
return sqlSession.selectOne("memberMapper.login",memberId);
}
}
- UserDetailsService
: 로그인 메소드가 정의되어 있는 인터페이스
(스프링에서 만들어준 서비스)
package kh.study.security.service;
@Service("userDetailsService")
public class UserDtailsServiceImpl implements UserDetailsService {
@Resource(name = "memberService")
private MemberService memberService;
//해당 메소드가 로그인 시 자동으로 실행된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//---------- 우리가 만든 로그인쿼리 실행 --------//
MemberVO loginInfo = memberService.login(username);
// UserDetails : 리턴값 자료형_인터페이스
return null;
}
}
package kh.study.security.vo;
@Setter
@Getter
@ToString
public class MemberVO {
private String memberId;
private String memberPw;
private String memberName;
private String role;
}
- 스프링 시큐리티 로그인 조회
:스프링 시큐리티에서는 조건절에 비밀번호 사용하는 것을 허용하지 않는다!
:단, 비밀번호도 같이 조회해야 한다.
비밀번호가 조회가 되지만, 암호화 된채로 비밀번호 데이터를 가져온다
그래서 암호화된 비밀번호를 디코딩(복호화) 한 뒤,
조회된 비밀번호를 비교하여 맞으면 로그인을 시켜준다.
<?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="memberMapper">
<resultMap type="kh.study.security.vo.MemberVO" id="member">
<id column="MEMBER_ID" property="memberId"/>
<result column="MEMBER_PW" property="memberPw"/>
<result column="MEMBER_NAME" property="memberName"/>
<result column="ROLE" property="role"/>
</resultMap>
<select id="login" resultMap="member">
SELECT MEMBER_ID
, MEMBER_NAME
, ROLE
, MEMBER_PW
FROM SECURITY_MEMBER
WHERE MEMBER_ID = #{memberId}
</select>
</mapper>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
index페이지입니다
</body>
</html>
- 로그아웃 기능
시큐리티를 활용해서 로그아웃을 진행하려면
post방식으로 /logout 으로 요청을 보내주기만 하면된다.
컨트롤러에 굳이 /logout 요청을 받는 post 방식메소드를 만들 필요가 없다.(자동생성)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>홈 화면</h1>
<form action="/logout" method="post">
<input type="submit" value="로그아웃">
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
admin 페이지입니다.
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
manager 페이지입니다.
</body>
</html>
로그인 기능
- [ 스프링 시큐리티 로그인 할 때 ]
: form태그 name값 아이디/비밀번호 표기법
아이디 : name="username"
비밀번호 : name="password"- 시큐리티를 활용해서 로그인 진행하려면
post방식으로 /login으로 요청을 보내주기만 하면된다.
컨트롤러에 굳이 /login 요청을 받는 post방식 메소드를 만들 필요가 없다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="/login" method="post">
id :<input type="text" name="username"><br>
pw :<input type="password" name="password"><br>
<div th:text="${failMsg}"></div>
<input type="submit" value="로그인">
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
accessDenied
</body>
</html>