1. 인증과 권한 1 : 230410
1) Session 개념
a. Session 로직
a) 조건 1
- 세션 / 쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함.
- if 너 로그인 했니?
- 아니요 -> 로그인하고 와
b) 조건 2
- if 너 로그인 햇니?
- 네 -> if(그럼 너 어드민 이니?)
- 아니요 -> 권한이 없다~ 얘.
b. Session-Id 개념
- 세션아이디는 서버자원을 사용하는 것이기 때문에 인증을 하면 그때 키(세션아이디, 세션키)가 부여받게 된다.**
package kr.co.rland.web.controller.admin;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.http.HttpServletRequest;
@Controller("adminHomeController")
@RequestMapping("/admin")
public class HomeController {
@GetMapping("index")
public String index(HttpServletRequest request) {
// **session id 부여**
// 아직 테스트만 했다. 중요하지 않다. sessionkey가 언제만들어지는지 확인!!
request.getSession().setAttribute("test", "hehe");
return "admin/index";
}
}
2) 로그인 기본 방식
a. 기능
- Member.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Member {
private Long id;
private String userName;
private String pwd;
private String email;
private Long roleId;
}
- UserController.java
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private MemberService memberService;
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@GetMapping("login")
public String login(String uid, String pwd){
// Member member= memberService.getByUseName(uid);
// 우리는 컨트롤러에서 입력만 받으면 된다.
// 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다.
// id를 가져와서 컨트롤러에서 처리하기 때문이다.
boolean isValid = memberService.isValidMember(uid, pwd);
return "user/login";
}
}
- MemberService.java
package kr.co.rland.web.service;
public interface MemberService {
// Member getByUseName(String uid);
boolean isValidMember(String uid, String pwd);
}
- DefaultMemberService.java
@Service
public class DefaultMemberService implements MemberService {
@Autowired
private MemberRepository repository;
@Override
public boolean isValidMember(String uid, String pwd) {
Member member = repository.findByUserName(uid);
return false;
}
}
- MemberRepository.java
@Mapper
public interface MemberRepository {
Member findByUserName(String uid);
}
2. 인증과 권한 2 : 230411
1) 로그인 방식 1 : 바닥부터 만들기
- UserController.java
package kr.co.rland.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import kr.co.rland.web.service.MemberService;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private MemberService memberService;
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@GetMapping("login")
public String login(){
return "user/login";
}
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@PostMapping("login")
public String login(String uid, String pwd){
// Member member= memberService.getByUseName(uid);
// 우리는 컨트롤러에서 입력만 받으면 된다.
// 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다.
// id를 가져와서 컨트롤러에서 처리하기 때문이다.
boolean isValid = memberService.isValidMember(uid, pwd);
System.out.println(isValid);
if(isValid)
return "redirect:index";
return "redirect:login";
}
}
- MemberService.java
package kr.co.rland.web.service;
public interface MemberService {
// Member getByUseName(String uid);
boolean isValidMember(String uid, String pwd);
}
- DefaultMemberService.java
package kr.co.rland.web.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import kr.co.rland.web.entity.Member;
import kr.co.rland.web.repository.MemberRepository;
@Service
public class DefaultMemberService implements MemberService {
@Autowired
private MemberRepository repository;
@Override
public boolean isValidMember(String uid, String pwd) {
Member member = repository.findByUserName(uid);
if(member == null)
return false;
// '=='가 아니라 '.getPwd().equals' 이다.
else if(!member.getPwd().equals(pwd))
return false;
return true;
}
}
- MemberRepository.java
package kr.co.rland.web.repository;
import org.apache.ibatis.annotations.Mapper;
import kr.co.rland.web.entity.Member;
@Mapper
public interface MemberRepository {
Member findByUserName(String uid);
}
- MemberRepositoryMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.rland.web.repository.MemberRepository">
<select id="findByUserName" resultType="Member">
select *
from member
where username=#{uid}
</select>
</mapper>
2) 로그인 실패 시, 에러 추가
${param.data}
로 값 넘겨주기- 타임리프의 편의 객체 이용
a. 실습 코드 :
- UserController.java
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private MemberService memberService;
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@GetMapping("login")
public String login(){
return "user/login";
}
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@PostMapping("login")
public String login(String uid,
String pwd,
HttpSession session){
// Member member= memberService.getByUseName(uid);
// 우리는 컨트롤러에서 입력만 받으면 된다.
// 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다.
// id를 가져와서 컨트롤러에서 처리하기 때문이다.
boolean isValid = memberService.isValidMember(uid, pwd);
System.out.println(isValid);
if(isValid) {
// session 케비넷을 사용하고 있는 User가 많다면,
// 과부하가 생긴다.
session.setAttribute("username", uid);
return "redirect:/index";
}
return "redirect:login?error=sdfsdfsdf";
}
}
- login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link href="/css/reset.css" type="text/css" rel="stylesheet" >
<link href="/css/style.css" type="text/css" rel="stylesheet" >
<link href="/css/layout.css" type="text/css" rel="stylesheet" >
<link href="/css/header.css" type="text/css" rel="stylesheet" >
<link href="/css/footer.css" type="text/css" rel="stylesheet" >
<link href="/css/buttons.css" type="text/css" rel="stylesheet" >
<link href="/css/icon.css" type="text/css" rel="stylesheet" >
<link href="/css/deco.css" type="text/css" rel="stylesheet" >
<link href="/css/index.css" type="text/css" rel="stylesheet">
<link href="/css/utils.css" type="text/css" rel="stylesheet" >
<style>
.btn{
color:black;
}
</style>
</head>
<body>
<header class="header">
<div>
<h1 class="header-title"><a href="/index.html"><img class="logo" src="../image/logo-w.png" alt="알랜드"></a></h1>
<ul class="main-menu d-none d-inline-flex-sm">
<li><a class="" href="/menu/list.html">카페메뉴</a></li>
<li><a class="" href="/notice/list.html">공지사항</a></li>
<li><a class="" href="/user/login.html">로그인</a></li>
</ul>
<div class="d-none-sm"><a class="icon icon-menu icon-white" href="?m=on">메뉴버튼</a></div>
</div>
</header>
<main>
<section>
<header class="header-default">
<h1 class="text-title1-h1">로그인</h1>
</header>
<section class="login">
<h1 class="d-none" th:text="${param.error}">일반 로그인</h1>
<div class="d-none"
th:class="${param.error} ? '' : 'd-none'";
style="color:red;
font-weight: bold;
text-align: center;
margin-bottom: 20px;">
아이디 또는 비밀번호가 일치하지 않습니다.
</div>
<form method="post">
<div class="d-flex align-items-center">
<label class="d-none">아이디</label><input name="uid" class="btn btn-cancel" type="text" placeholder="로그인 아이디를 입력하세요">
</div>
<div class="d-flex align-items-center">
<label class="d-none">비밀번호</label><input name="pwd" class="btn btn-cancel" type="password" placeholder="비밀번호">
</div>
<div class="d-flex align-items-center justify-content-center">
<input type="checkbox">
<label>로그인 저장</label>
</div>
<div class="d-flex align-items-center justify-content-center">
<input class="btn btn-default" type="submit" value="로그인">
<a class="btn btn-cancel" href="">취소하기</a>
</div>
</form>
</section>
<section class="register">
<h1 class="d-none">회원가입</h1>
<a href="signup.html">회원가입</a>
<a href="">아이디 찾기</a>
<a href="">비밀번호 찾기</a>
</section>
<section class="social-login">
<h1 class="d-none">소셜 로그인</h1>
<span>또는 다음으로 로그인</span>
<div>
<a class="icon icon-naver mx-2" href="">네이버 로그인</a>
<a class="icon icon-kakao mx-2" href="">카카오 로그인</a>
<a class="icon icon-youtube mx-2" href="http://localhost/login/oauth2/code/google">구글 로그인</a>
</div>
</section>
</section>
</main>
<footer class="footer">
<h2>알랜드(Rland)</h2>
<div>
copyright @ rland.co.kr 2022-2022 All Right Reservved. Contact admin@rland.co.kr for more information
</div>
</footer>
</body>
</html>
3) session 값 넘겨서 로그인 유지하기
- 쇼핑몰 예시처럼 로그인 안되면 index 페이지로 보내줄 것
a. 실습 코드 :
- admin/UserController.java
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private MemberService memberService;
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@GetMapping("login")
public String login(){
return "user/login";
}
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@PostMapping("login")
public String login(String uid,
String pwd,
String returnURL,
HttpSession session){
// Member member= memberService.getByUseName(uid);
// 우리는 컨트롤러에서 입력만 받으면 된다.
// 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다.
// id를 가져와서 컨트롤러에서 처리하기 때문이다.
boolean isValid = memberService.isValidMember(uid, pwd);
System.out.println(isValid);
if(isValid) {
// session 케비넷을 사용하고 있는 User가 많다면,
// 과부하가 생긴다.
// 하지만, 로그인하면 이렇게 세션키를 발급!
session.setAttribute("username", uid);
//
if(returnURL != null)
return "redirect:"+returnURL;
return "redirect:/index";
}
return "redirect:login?error=sdfsdfsdf";
}
}
- admin/MenuController.java
@Controller("adminMenuController")
@RequestMapping("/admin/menu/")
public class MenuController {
// 콩자루를 가져온다. 결합을 의미한다! 빈 객체 사용!
// 결합은 setInjection과 CompositionInjection이 있다.
// 이것을 실행하려면 Application.java에서 실행해주자!
// 객체가 같은 것이 2개가 있다면, DI하는데 문제가 있는 것이다. 그래서 @Qualifier로 구분해준다.
// setter injection은 실행하거나 실행되는 영역이 생긴다. 바인딩 되기 전에 다른 작업을 처리할 수 있다.
// 하지만, 코드 라인수가 가장 적은 '필드'에 @Autowired를 하여 DI를 한다.
@Autowired
private MenuService service;
// 400 에러는 데이터가 파라미터 인자가 없는 경우이다.
// 403 에러는 권한 관련 에러이다.
// 405 에러는 처리메서드가 post요청인데 get요청만 있는 경우
@RequestMapping("list")
public String list(
@RequestParam(name = "p", defaultValue = "1") int page,
@RequestParam(name = "c", required = false) Integer categoryId,
@RequestParam(name = "q", required = false) String query,
Model model,
HttpSession session
) throws UnsupportedEncodingException
{
// 세션 / 쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함.
// if 너 로그인 했니?(인증한 적이 없으면 로그인을 다시 하러 가야 한다.)
if(session.getAttribute("username")==null)
return "redirect:/user/login?returnURL=/admin/menu/list";
// 아니요 -> 로그인하고 와
// if 너 로그인 햇니?
// 네 -> if(그럼 너 어드민 이니?)
// 아니요 -> 권한이 없다~ 얘~
// **세션아이디는 서버자원을 사용하는 것이기 때문에 인증을 하면 그때 키(세션아이디, 세션키)가 부여받게 된다.**
List<MenuView> list = service.getViewList(page, categoryId, query);
model.addAttribute("list", list);
return "admin/menu/list";
}
@GetMapping("detail")
public String detail(int id, Model model) {
Menu menu = service.getById(id);
model.addAttribute("menu", menu);
return "admin/menu/detail";
}
// 등록폼을 주세요.
// @RequestMapping("reg")
// -> service() 함수와 같다. : Get/Post를 내가 다 처리하는 매핑
@GetMapping("reg")
public String reg() {
return "/WEB-INF/view/admin/menu/reg.jsp";
}
// 폼입력해서 제출이요.
@PostMapping("reg")
public String reg(String title) {
// 등록하고나서!
System.out.println("메뉴 등록 완료");
return "redirect:list";
// @Controller는 view단을 찾으므로 url을 입력해줘야한다.
// redirection은 reg라는 페이지에서 클라이언트가 list라는 url로 가라고 한다.
// 경로가 더 이상 없다면 현재 경로에서 찾는다.
// 그 요청은 jsp파일의 form 태그에서 method post를 설정해줘야 한다. Post 요청이라서!
}
}
4) 초기의 필터 만들기
package kr.co.rland.web.config;
import java.io.IOException;
import org.springframework.stereotype.Component;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class AuthFilter implements Filter {
// 인증을 위한 필터 만들기!
// 따로 설정을 안 하면, 수문장이라서 해당 서비스 내부로 들여보내질 않는다.
// 예전에는 이런 방식으로 필터를 만들었었다.
private static final String[] authUrls = {
"/admin/**","/member/**"
};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 사용자가 입력하는 uri와 url을 체킹한다. 에러가 발생했는지 알 수 있다.(로그인 실패 등등)
HttpServletRequest httpRequest = (HttpServletRequest) request;
String uri = httpRequest.getRequestURI();
System.out.println(uri);
String url = httpRequest.getRequestURI().toString();
System.out.println(url);
// FilterChain은 여러개 쓸 수 있어서 다음 Chain도 설정 해준다!
System.out.println("입구 필터가 실행되었습니다.");
chain.doFilter(request, response);
System.out.println("출구 필터가 실행되었습니다.");
// 이렇게 하면, 여러번 필터가 발생하는데 모든 파일들에 대해 필터 처리를 해준다.
}
}
5) Spring Security
- pom.xml 에 아래 라이브러리를 깔면,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
package kr.co.rland.web.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.web.SecurityFilterChain;
@Configuration
public class RlandSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests() // 권한 요청
.requestMatchers("/admin/**") // url에 대한 패턴 권한 부여
.hasAnyRole("ADMIN") // 어떤 사용자에 대한 권한 부여
// **가 재귀 경로이다. 하부 구조 전부를 말한다.
.requestMatchers("/member/**").hasAnyRole("ADMIN,MEMBER")
.anyRequest().permitAll(); // 권한 부여주기
// .hasAnyAutnullhority() : Role_""가 기본 권한 방식,
// .hasAnyRole(null) : 앞에 Role_를 붙여줘서 역할만 바로 쓰면 된다.
return null;
}
}
3. 스프링 시큐리티 적용 : 230412
1) 스프링 시큐리티 기본 이용
- 스프링 시큐리티가 기본으로 주는 로그인 폼이 있다.
a. 내 코드 :
- 스프링 시큐리티 이용!!(내 코드 에러남…)
package kr.co.rland.web.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.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
public class RlandSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests() // 권한 요청
.requestMatchers("/admin/**") // url에 대한 패턴 권한 부여
.hasAnyRole("ADMIN") // 어떤 사용자에 대한 권한 부여// **가 재귀 경로이다. 하부 구조 전부를 말한다.
.requestMatchers("/member/**")
.hasAnyRole("ADMIN,MEMBER")
.anyRequest().permitAll() // 권한 부여주기
.and()
.formLogin();
// .hasAnyAutnullhority() : Role_""가 기본 권한 방식,
// .hasAnyRole(null) : 앞에 Role_를 붙여줘서 역할만 바로 쓰면 된다.
// build를 해야지 실행된다!!
return http.build();
}
// SpringSecurity를 사용하기 위해서, PasswordEncoder가 필요하다!
// 요즘에는 Bcrypt 해시 함수를 이용하는 암호화 패스워드를 이용한다!
// 해시 함수는 식별자라고 하며 식별자를 의미한다.
// 해시 값으로 원문은 못만든다!!
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 사용자 데이터 서비스
// 1. 인메모리 서비스 : 어디에 있는 것이 아니라 메모리에만 사용자가 있어서
// 2. JDBC 서비스
// 3. LDAP 서비스
@Bean
public UserDetailsService userDetailsService(){
//import org.springframework.security.core.userdetails.UserDetails; 이용하기!
// UserDetails은 어디에 있는 것이 아니라
// 메모리에만 사용자가 있어서 인메모리 서비스 라고 부른다.
UserDetails newlec = User
.builder()
.username("newlec")
.password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
.roles("ADMIN","MEMBER")
.build();
UserDetails dragon = User
.builder()
.username("newlec")
.password("111")
.roles("ADMIN","MEMBER")
.build();
// ** Builder는 AllArg 생성자가 필요하다!!
// Builder는 원하는 인자만 따로 생성자를 만들 때, 사용한다.
// Member member = Member.builder()
// .id()
// .username("newlec")
// .build();
// Member member = Member.builder().id(1L).build();
// 이렇게 원하는 사용자를 계속 담는다.
return new InMemoryUserDetailsManager(newlec, dragon);
}
}
b. 스프링 시큐리티 : 수정한 코드 :!!
package kr.co.rland.web.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.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
public class RlandSecurityConfig {
@Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests()
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
.anyRequest().permitAll()
.and()
.formLogin();
// /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 사용자 데이터 서비스
//1. 인메모리 서비스
//2. JDBC 서비스
//3. LDAP 서비스
@Bean
public UserDetailsService userDetailsService(){
UserDetails newlec = User
.builder()
.username("newlec")
.password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
.roles("ADMIN","MEMBER")
.build();
UserDetails dragon = User
.builder()
.username("dragon")
.password("111")
.roles("MEMBER")
.build();
return new InMemoryUserDetailsManager(newlec, dragon);
}
}
2) 유저가 만든 로그인 페이지 이용하는 스프링 시큐리티
a. 유저가 만든 로그인 페이지 이용하는 방법
.and()
.formLogin()
.loginPage("/user/login");
b. 실습 코드
@Configuration
public class RlandSecurityConfig {
@Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests()
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/user/login");
// /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능
return http.build();
}
3) 크로스 사이트에서 원격요청 설정
@Configuration
public class RlandSecurityConfig {
@Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.cors().and() // 리소스 사이트를 의미한다.
.csrf().disable() // 크로스 사이트에서 원격요청을 검사하는 작업을 disable하는 것이다.
.authorizeHttpRequests()
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
.anyRequest().permitAll()
.and()
.formLogin() // 무조건 인증을 안하면 redirect해서 여기로 와서 로그인을 해야 한다.
.loginPage("/user/login") // 이렇게 설정하면 사용자가 만든 로그인 페이지를 사용하게 해준다.
.loginProcessingUrl("/user/login"); // 여기에는 사용자가 만든 POST 요청 URL을 적자!
// /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능
return http.build();
}
4) 주의 사항
-
스프링이 로그인 기능을 만들어 주었기 때문에 html의 input 태그의 name 속성을 username과 password로 바꿔줘야 한다.
-
로그인하면 어디로 가야하는가? 성공한 url 페이지
- defaultSuccessUrl 이용하기
@Configuration
public class RlandSecurityConfig {
@Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.cors().and() // 리소스 사이트를 의미한다.
.csrf().disable() // 크로스 사이트에서 원격요청을 검사하는 작업을 disable하는 것이다.
.authorizeHttpRequests()
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
.anyRequest().permitAll()
.and()
.formLogin() // 무조건 인증을 안하면 redirect해서 여기로 와서 로그인을 해야 한다.
.loginPage("/user/login") // 이렇게 설정하면 사용자가 만든 로그인 페이지를 사용하게 해준다.
.loginProcessingUrl("/user/login") // 여기에는 사용자가 만든 POST 요청 URL을 적자!
.defaultSuccessUrl("/admin/index"); // 로그인 성공 시, 성공한 url 이동 가능
// /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능
return http.build();
}
5) 예전 방식의 Logout 방식
- Httpsession 이용!
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private MemberService memberService;
// 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
@GetMapping("login")
public String login(){
return "user/login";
}
// 예전 방식의 logout!!(session $이용 )
@RequestMapping("logout")
public String logout(HttpSession session){
session.invalidate();
return "redirect:/index";
}
5) 스프링 시큐리티 방식의 Logout 방식
a. 기본 로그아웃 설정(Spring Security)
.and()
.logout()
.logoutUrl("/user/logout") // 사용자가 만든 로그아웃 화면 URL
.logoutSuccessUrl("index"); // 로그아웃 성공하면, 이동하는 URL 설정
b. 실습 코드 :
@Configuration
public class RlandSecurityConfig {
@Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.cors().and() // 리소스 사이트를 의미한다.
.csrf().disable() // 크로스 사이트에서 원격요청을 검사하는 작업을 disable하는 것이다.
.authorizeHttpRequests()
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
.anyRequest().permitAll()
.and()
.formLogin() // 무조건 인증을 안하면 redirect해서 여기로 와서 로그인을 해야 한다.
.loginPage("/user/login") // 이렇게 설정하면 사용자가 만든 로그인 페이지를 사용하게 해준다.
.loginProcessingUrl("/user/login") // 여기에는 사용자가 만든 POST 요청 URL을 적자!
.defaultSuccessUrl("/admin/index") // 로그인 성공 시, 성공한 url 이동 가능
.and()
.logout()
.logoutUrl("/user/logout") // 사용자가 만든 로그아웃 화면 URL
.logoutSuccessUrl("index"); // 로그아웃 성공하면, 이동하는 URL 설정
// /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능
return http.build();
}
6) 타임리프에서 Spring Security 적용!
-
타임리프에 태그 추가 :
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity"
-
isAnonymous() : 익명의 사용자만 보인다.
-
isAuthenticated() : 로그인한 인증된 사용자만 보인다.
- 실습 코드 :
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity"
>
<header class="header">
<div>
<h1 class="header-title"><a href="/index"><img class="logo" src="/image/logo-w.png" alt="알랜드"></a></h1>
<ul class="main-menu d-none d-inline-flex-sm">
<li><a class="" href="/menu/list">카페메뉴</a></li>
<li><a class="" href="/notice/list">공지사항</a></li>
<li sec:authorize="isAnonymous()"><a class="" href="/user/login">로그인</a></li>
<li sec:authorize="isAuthenticated()"><a class="" href="/user/logout">로그아웃</a></li>
</ul>
<div class="d-none-sm"><a class="icon icon-menu icon-white" href="?m=on">메뉴버튼</a></div>
</div>
</header>
</html>
4. 230413
1) JDBC 서비스
-
권한 테이블((Role))에서 회원이 관리자나 회원이 된다면, 회원 테이블(Member)/과 권한 테이블(Role)은 다대다 관계여야 한다.
-
현재 Rland의 경우에 회원이 권한이 1개 밖에 안된다.
-
그렇다면, 권한이 여러개면 어떻게 할까?
-
'pwd password' 이렇게 매핑 시킨다.(as 기능)
// DB의 칼럼명이 다른데 어떻게 매핑할까?
// 현재, username, password, enable라는 칼럼이 필요!
// 'pwd password' 이렇게 매핑 시킨다.(as 기능)
manager.setUsersByUsernameQuery("select username, pwd password, 1 enabled from member where username=?");
manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_MEMBER' autority from member where username=?");
2) 시큐리티로 유저정보 받기(준비 단계)
- MenuController.java
@Controller("adminMenuController")
@RequestMapping("/admin/menu")
public class MenuController {
@Autowired
private MenuService service;
@GetMapping("list")
public String list(
@RequestParam(name = "p", defaultValue = "1") int page,
@RequestParam(name="c", required = false) Integer categoryId,
@RequestParam(name="q", required = false) String query,
Model model,
HttpSession session
){
//세션/쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함
// if(너 로그인은 했니?)
// if(session.getAttribute("username")==null)
// return "redirect:/user/login?returnURL=/admin/menu/list";
//아니요 -> 로그인하구와
// if(너 로그인은 했니? )
//네->if (그럼 너 어드민이니?)
//아니요 -> 권한 없다 얘~
List<MenuView> list = service.getViewList(page,categoryId, query);
model.addAttribute("list", list);
return "admin/menu/list";
}
@GetMapping("detail")
public String detail(
int id,
Model model) {
Menu menu = service.getById(id);
model.addAttribute("menu", menu);
return "admin/menu/detail";
}
// /admin/menu/reg
// /admin/menu/reg.html
// /admin/menu/reg.jsp
// 등록폼을 주세요
//@RequestMapping("reg")//-> service():Get/Post를 내가 다 처리하는 매핑
@GetMapping("reg")
public String reg() {
return "/WEB-INF/view/admin/menu/reg.jsp";
}
// 폼입력해서 제출이요
//@RequestMapping("reg")
@PostMapping("reg")
public String reg(String title,
Authentication authentication) {
//방법3 : getPrincipal가 유저정보를 반환해준다!
UserDetails user = (UserDetails) authentication.getPrincipal();
System.out.println(user.getUsername());
//방법2
String userName= authentication.getName();
// authentication.getPrincipal();
System.out.println(userName);
// //방법1
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// String currentPrincipalName = authentication.getName();
// // 등록하고
// System.out.println("메뉴 등록 완료");
return "redirect:list";
}
}
- SecurityConfig.java
// https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html
@Configuration // 콩자루에 담아야할 내용들을 설정함
public class RlandSecurityConfig {
@Autowired
private DataSource dataSource;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 보안 설정을 위한.. 주소에 대한 필터링을 하는.. 너는 통과, 너는 안 돼
// 2가지 url 제외하고는 모두 허가함
// 로그인페이지로 보내는 작업을 내가 할 필요가 없음.
// 예외는 오류의 한 종류 - 구문오류, 논리오류, 예외오류(입력값이 잘못된 것)
http
.cors()
.and() // 리소스쉐어링
.csrf().disable() // 리퀘스트포절? 하는 작업을 디저블 하는거
.authorizeHttpRequests() // 권한을 요청
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN", "MEMBER") // admin은 member 페이지도 들어갈 수 있다고 가정함
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/user/login")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/admin/menu/list") // 로그인 성공 후에 기본 성공 URL을 설정
.and() // 한 묶
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/index");
return http.build(); // 설정을들 다 담고 한 번에 빌드하기가 트렌드
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 인증과 권한이 필요한 url에 대한 정보와 사용자에대한 정보가 필요함
// 사용자 데이터 서비스 --------------------------------------------------
// 2. JDBC 서비스
@Bean
public UserDetailsService jdbcUserDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.setUsersByUsernameQuery("select username, pwd password, 1 from member where username = ?"); // username, password, enabled
manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_ADMIN' autority from member where username = ?");
//manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_MEMBER' autority from member where username = ?");
return manager;
}
// 1. 인메모리 서비스
//@Bean
public UserDetailsService userDetailsService() {
// 세터보다 빌더가 편하다. 빌더 패턴
// 1. 인메모리 사용자 정보
// 관리자를 등록하는 유저, 슈퍼 관리자가 쓰거나 db를 쓰지 않는 경우에 인메모리 사용자 사용함
// 일반적으로 사용자 정보를 db에 넣고 사용함
UserDetails newlec = User
.builder()
.username("newlec")
//.password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
.password(passwordEncoder().encode("111")) // 스프링이 사용자가 입력한 값을 암호화할 때 사용
.roles("ADMIN", "MEMBER")
.build();
UserDetails dragon = User
.builder()
.username("dragon")
.password(passwordEncoder().encode("111"))
.roles("MEMBER")
.build();
// 예시
// Member member = Member
// .builder()
// .id(1L)
// .userName("newlec")
// .build();
return new InMemoryUserDetailsManager(newlec, dragon);
}
}
3) 커스터마이징한 사용자 정보를 DB에서 가져와서 스프링 시큐리티 사용
- RlandSecurityConfig.java
- 전역에서 설정 파일
// https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html
@Configuration // 콩자루에 담아야할 내용들을 설정함
public class RlandSecurityConfig {
@Autowired
private DataSource dataSource;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 보안 설정을 위한.. 주소에 대한 필터링을 하는.. 너는 통과, 너는 안 돼
// 2가지 url 제외하고는 모두 허가함
// 로그인페이지로 보내는 작업을 내가 할 필요가 없음.
// 예외는 오류의 한 종류 - 구문오류, 논리오류, 예외오류(입력값이 잘못된 것)
http
.cors()
.and() // 리소스쉐어링
.csrf().disable() // 리퀘스트포절? 하는 작업을 디저블 하는거
.authorizeHttpRequests() // 권한을 요청
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/member/**").hasAnyRole("ADMIN", "MEMBER") // admin은 member 페이지도 들어갈 수 있다고 가정함
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/user/login")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/admin/menu/list") // 로그인 성공 후에 기본 성공 URL을 설정
.and() // 한 묶
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/index");
return http.build(); // 설정을들 다 담고 한 번에 빌드하기가 트렌드
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 사용자 데이터 서비스 사용하기!!(사용하기 위해서 컨테이너에 @Bean를 사용하여 담아주기!)
// 여기서 담아주거나 RlandUserDetailsService.java에서 @Service로 컨테이너에 빈 객체로 담아준다!
// 3. Custom user Service
// @Bean
public UserDetailsService rlanDetailsService() {
return new RlandUserDetailsService();
}
// 인증과 권한이 필요한 url에 대한 정보와 사용자에대한 정보가 필요함
// 사용자 데이터 서비스 --------------------------------------------------
// 2. JDBC 서비스
// @Bean
public UserDetailsService jdbcUserDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
manager.setUsersByUsernameQuery("select username, pwd password, 1 from member where username = ?"); // username, password, enabled
manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_ADMIN' autority from member where username = ?");
//manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_MEMBER' autority from member where username = ?");
return manager;
}
// 1. 인메모리 서비스
//@Bean
public UserDetailsService userDetailsService() {
// 세터보다 빌더가 편하다. 빌더 패턴
// 1. 인메모리 사용자 정보
// 관리자를 등록하는 유저, 슈퍼 관리자가 쓰거나 db를 쓰지 않는 경우에 인메모리 사용자 사용함
// 일반적으로 사용자 정보를 db에 넣고 사용함
UserDetails newlec = User
.builder()
.username("newlec")
//.password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
.password(passwordEncoder().encode("111")) // 스프링이 사용자가 입력한 값을 암호화할 때 사용
.roles("ADMIN", "MEMBER")
.build();
UserDetails dragon = User
.builder()
.username("dragon")
.password(passwordEncoder().encode("111"))
.roles("MEMBER")
.build();
// 예시
// Member member = Member
// .builder()
// .id(1L)
// .userName("newlec")
// .build();
return new InMemoryUserDetailsManager(newlec, dragon);
}
}
- RlandUserDetails.java
- 담아줄 그릇 : entity 느낌
@ToString
@Setter
@Getter
public class RlandUserDetails implements UserDetails{
private Long id;
private String email;
private String username;
private String password;
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- MenuController.java
- view와 서버를 연결해주는 컨트롤러
- Principal가 유저 정보를 반환해준다.
@Controller("adminMenuController")
@RequestMapping("/admin/menu")
public class MenuController {
@Autowired
private MenuService service;
@GetMapping("list")
public String list(
@RequestParam(name = "p", defaultValue = "1") int page,
@RequestParam(name="c", required = false) Integer categoryId,
@RequestParam(name="q", required = false) String query,
Model model,
HttpSession session
){
//세션/쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함
// if(너 로그인은 했니?)
// if(session.getAttribute("username")==null)
// return "redirect:/user/login?returnURL=/admin/menu/list";
//아니요 -> 로그인하구와
// if(너 로그인은 했니? )
//네->if (그럼 너 어드민이니?)
//아니요 -> 권한 없다 얘~
List<MenuView> list = service.getViewList(page,categoryId, query);
model.addAttribute("list", list);
return "admin/menu/list";
}
@GetMapping("detail")
public String detail(
int id,
Model model) {
Menu menu = service.getById(id);
model.addAttribute("menu", menu);
return "admin/menu/detail";
}
// /admin/menu/reg
// /admin/menu/reg.html
// /admin/menu/reg.jsp
// 등록폼을 주세요
//@RequestMapping("reg")//-> service():Get/Post를 내가 다 처리하는 매핑
@GetMapping("reg")
public String reg() {
return "/WEB-INF/view/admin/menu/reg.jsp";
}
// 폼입력해서 제출이요
//@RequestMapping("reg")
@PostMapping("reg")
public String reg(String title,
Authentication authentication,
Principal principal
) {
// 방법 4:
System.out.println(principal.getName());
//방법3 : getPrincipal가 유저정보를 반환해준다!
UserDetails user = (UserDetails) authentication.getPrincipal();
System.out.println(user.getUsername());
//방법2
String userName= authentication.getName();
// authentication.getPrincipal();
System.out.println(userName);
// //방법1
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// String currentPrincipalName = authentication.getName();
// // 등록하고
// System.out.println("메뉴 등록 완료");
return "redirect:list";
}
}
- RlandUserDetailsService.java
- 서비스 구현 부이며, findByUserName에서 DB 값을 가져오며 SimpleGrantedAuthority는 GrantedAuthority 인터페이스의 구현체이다.
- 권한은 여러개가 있을 수 있어서 List로 받음.
// 중요!!**
// 이렇게 @Service로 빈 객체를 만들어서 사용할 수 있도록 컨테이너에 담아주거나
// RlandSecurityConfig.java에서 UserDetailsService 객체에서 빈 객체를 만들어서 사용할 수 있도록 컨테이너에 담아주거나
@Service
public class RlandUserDetailsService implements UserDetailsService {
@Autowired
private MemberRepository repository;
@Override
public UserDetails loadUserByUsername(String username) {
// RlandUserDetails 그릇에 담을 데이터 준비!
Member member = repository.findByUserName(username);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// 데이터 준비 되었으면 이제 RlandUserDetails 그릇 객체를 만들어서 데이터를 담아서 반환해주면 끝!
RlandUserDetails user = new RlandUserDetails();
user.setId(member.getId());
user.setUsername(member.getUserName());
user.setPassword(member.getPwd());
user.setEmail(member.getEmail());
user.setAuthorities(authorities);
return user;
}
}
4) 데이터 뽑아내기
-
authentication는 권한 준 아이디에서 데이터를 뽑아낸다.
-
authorize은 권한이 있는지 판단
<!DOCTYPE html>
<html
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="inc/layout"
>
<main layout:fragment="adminmain" class="main-padding-none">
<!-- 문서 안에 포함되어야 해서 스크립트 위치 주의! 그리고 defer 설정 중요!!-->
<script src="/js/admin/menu/list.js" defer="defer"></script>
<section id="main-section">
<header class="search-header"> <!-- authentication는 권한 준 아이디에서 데이터를 뽑아낸다. -->
<h1 class="text-title1-h1" sec:authorize="isAuthenticated()">알랜드 메뉴<span sec:authentication="principal.email" ></span></h1>
<form>
<input type="text">
<input type="submit" class="icon icon-find">
</form>
</header>