스프링 시큐리티 개요
1. spring-security.jar을 추가했다
2. 모든 페이지에 접근이 불가능해졌다
3. 권한이 있어야 페이지 출력이 가능하다
4. 내가 로그인 화면을 구성하지 않았음에도 로그인 화면으로 유도된다
5. FilterChain 제공된다
6. SecurityConfig.java 추가
-> 더 이상 스프링에서 제공하는 페이지로 가지 않아도 된다
7. 사용자 정의 시큐리티 코딩 전개하기
-> 스프링 시큐리티는 스프링 시큐리티 세션을 들고 있다
서버 세션 영역 안에 시큐리티가 관리하는 세션이 따로 존재함
HttpSession session =request.getSession() - 기존의 세션
시큐리티 세션에는 무조건 Authentication 객체만 들어갈 수 있다
Authentication이 시큐리티 세션 안에 들어있다는 것은 로그인된 상태라는 의미임
Authentication에는 2개의 타입이 들어감
UserDetails -> PrincipalUserDetails
OAuth2User
문제제기
세션이 두 개 타입이라서 컨트롤러에서 처리가 복잡해짐 일반 개발자 정의 로그인에서는 UserDetails타입으로 Authentication 객체가 만들어지고 구글로그인처럼 OAuth로그인 시에는 OAuth2User타입으로 Authentication객체가 생성됨
해결방법
PrincipalDetails에 UserDetails, OAuth2User를 implements 한다
<스프링 시큐리티 복습 완료 - HomeController.java>
package com.example.demo.controller;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
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.ResponseBody;
import com.example.demo.auth.PrincipalDetails;
import com.example.demo.logic.UserLogic;
import com.example.demo.model.User;
@Controller
public class HomeController {
Logger logger = LogManager.getLogger(HomeController.class);
@Autowired
private UserLogic userLogic = null;
@Autowired
public BCryptPasswordEncoder bCryptPasswordEncoder = null;
// user-123 admin-123
@GetMapping("/")
public String index(Authentication authentication, HttpSession session) {
logger.info("index");
PrincipalDetails principalDetails = null;
if(authentication !=null) {
principalDetails = (PrincipalDetails)authentication.getPrincipal();
logger.info(principalDetails.getUser());
logger.info(principalDetails.getUser().getId());
logger.info(principalDetails.getUser().getEmail());
logger.info(principalDetails.getUser().getCreateDate());
logger.info(principalDetails.getUser().getUsername());
logger.info(principalDetails.getUser().getPassword());
logger.info(principalDetails.getUser().getRole());
session.setAttribute("email", principalDetails.getUser().getEmail());
session.setAttribute("role", principalDetails.getUser().getRole());
}
return "forward:index.jsp";
}
// 로그인 화면
@GetMapping("/loginForm")
public String loginForm() {
logger.info("loginForm");
return "forward:loginForm.jsp";
}
// 로그인 에러 화면
@GetMapping("/login-error")
public String loginError() {
logger.info("loginError");
return "forward:loginError.jsp";
}
// 회원가입 화면
@GetMapping("/joinForm")
public String joinForm() {
logger.info("joinForm");
return "forward:joinForm.jsp";
}
// 회원가입
@PostMapping("/join")
public String join(User user) {
logger.info("join");
int result = 0; // 등록 성공 유무 담기
user.setRole("ROLE_USER");
// 패스워드 암호화처리
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
result = userLogic.memberInsert(user);
return "forward:loginForm.jsp";
}
// 유저 화면
@GetMapping("/user")
public String userPage() {
logger.info("userPage");
return "forward:user-index.jsp";
}
// 관리자 화면
@GetMapping("/admin")
public String adminPage() {
logger.info("adminPage");
return "forward:admin-index.jsp";
}
// @ResponseBody주면 text로 출력됨
@GetMapping("/auth")
public @ResponseBody Authentication auth() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
<스프링 시큐리티 복습 완료 - SecurityConfig.java>
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
//import com.example.demo.oauth.PrincipalOauth2UserService;
// 1-1에서틑 SecurityConfig 파일을 생성하지 않았다 -> 스프링시큐리티에서 제공하는 인증화면으로 처리 진행됨
// 이 클래스를 정의하지 않으면 스프링 시큐리티가 제공하는 DefaultLoginPageGeneratingFilter가
// 제어권을 가져가서 개발자가 만든 로그인 화면을 만날 수 없다
// 결론: 내가 주도하는 인증처리를 위해서는 반드시 선언해야함
@EnableWebSecurity(debug=true) // 요청이 지나가는 필터정보 확인 가능
@EnableGlobalMethodSecurity(securedEnabled=true ,prePostEnabled=true) // 권한을 체크하겠다는 설정 추가
public class SecurityConfig {
// 암호화가 안된 비밀번호로는 로그인이 안됨
// 패스워드를 암호화하기위한 코드 추가
// Bean어노테이션을 적으면 해당 메소드의 리턴되는 오브젝트를 IoC로 등록해줌
// @Autowired
// private PrincipalOauth2UserService principalOauth2UserService;
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
// 상수로 처리시에 주석을 반드시 풀 것
/*
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();
}
*/
// 만약 관리자 계정이면 하위인 유저 계정에 접근할 수 있도록 처리하려면 아래 코드를 추가함
// 유저는 관리자에 접근이 안되지만 관리자는 유저페이지도, 관리자페이지 접근 가늘하도록 설정
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
AuthenticationManagerBuilder auth =
http.getSharedObject(AuthenticationManagerBuilder.class);
/*
auth.inMemoryAuthentication()
// 아래 메소드가 데프리케이트 대상인 이유는 테스트용도로만 사용하라는 경고의 의미
// 보안상 안전하지 않으니 쓰지말것을 당부 - 그러나 지원은 끊지 않음
.withUser(User.withDefaultPasswordEncoder()
.username("user")
.password("123")
.roles("USER")
).withUser(User.withDefaultPasswordEncoder()
.username("admin")
.password("123")
.roles("ADMIN")
);
*/
http.csrf().disable(); // csrf filter 비활성화하기
http.authorizeRequests() // http요청으로 들어오는 모든 것에대해서 매칭하기
// localhost:5000/user로 요청하면 403발동 - 403접근권한이 없을때
.antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있는 주소
// manager나 admin 권한이 있는 사람만 접근가능, securedEnabled=true속성추가해야함
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
// admin 권한이 있는 사람만 접근가능
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll() // 위 세가지가 아닌 경우는 모두 허용함
.and()
.formLogin()
.loginPage("/loginForm") // 2차 단위테스트 - 권한에따라서 페이지 제어
.loginProcessingUrl("/login") // 이 URL이 요청되면 시큐리티가 인터셉트해서 대신 로그인 진행
.failureUrl("/login-error")
.defaultSuccessUrl("/")
.and()
// 구글 로그인 기능 구현 추가
.oauth2Login()
.loginPage("/loginForm");
// 구글 로그인 후 후처리 필요함
// 1. 인증받기 -> 2. 엑세스 토큰(권한) -> 3. 사용자 프로필정보 가져오기 -> 4. 그 정보로 회원가입 처리가능(여기선 미적용)
// 구글 로그인이 성공되면 코드를 받는게 아니라 엑세스 토큰과 사용자에대한 프로필정보를 한번에 받아옴
// .userInfoEndpoint()
// .userService(principalOauth2UserService); // 구글 로그인 후처리 클래스 추가
// 현재 페이지에서 로그아웃을 눌럿을 때 로그인 페이지가 아니라 메인 페이지에 있도록 해줌
//.logout(logout -> logout.logoutSuccessUrl("/"))
// 403번 접근제한 예외 발생시 이동할 페이지 요청URL 작성하기
//.exceptionHandling(exception -> exception.accessDeniedPage("/access-denied"));
return http.build();
}// end of filterChain
}
/*
테스트 시나리오
localhost:5000 요청은 권한과 상관없이 열림
localhost:5000/user
localhost:5000/admin
localhost:5000/manager
로그인 페이지로 이동함
첫번째 - user롤일때
localhost:5000/user 출력되고
localhost:5000/admin 403발동
localhost:5000/manager 403발동
두번째 - admin 로그인했을때
lcalhost:5000/user 출력
localhost:5000/admin 출력
localhost:5000/manager 출력
두번째 - manager 로그인했을때
lcalhost:5000/user 403발동
localhost:5000/admin 403발동
localhost:5000/manager 출력
*/
<스프링 시큐리티 복습 완료 - PrincipalDetails.java>
package com.example.demo.auth;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.example.demo.model.User;
import lombok.Data;
//여기에서 쥔 값을 세션에 담아야 함
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
private static final long serialVersionUID = 1L;
private User user;
// 구글 로그인시 구글 서버에서 넣어주는 정보가 Map형태인데 그것을 받을 변수 선언
private Map<String, Object> attributes;
//일반 로그인시 사용하는 생성자
public PrincipalDetails(User user) {
this.user = user;
}
//OAuth로그인시 사용하는 생성자
// attributes를 통해서 User를 생성해준다
// 2번째 파라미터 자리는 구글 인증 정보를 담음
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
//해당 User권한을 리턴하는 곳
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
//데이터베이스와 매칭이 안되면 로그인 실패 페이지 호출됨
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
//강제는 아니나 사용자가 세션에 담고 싶은 다른 컬럼정보도 등록이 가능함
public String getRole() {
return user.getRole();
}
public String getCreateDate() {
return user.getCreateDate();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
}
<스프링 시큐리티 복습 완료 - PrincipalOauth2UserService.java>
package com.example.demo.oauth;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import com.example.demo.auth.PrincipalDetails;
import com.example.demo.dao.UserDao;
import com.example.demo.model.User;
//구글로그인버튼 -> 구글로그인창 -> 로그인을 완료 -> 코드를 리턴
//AccessToken요청 -> loadUser호출 -> 구글 프로필정보 받음
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
Logger logger = LogManager.getLogger(PrincipalOauth2UserService.class);
@Autowired
private UserDao userDao;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oAuth2User.getAttribute("sub");
String email = oAuth2User.getAttribute("email");
String role = "ROLE_USER";
logger.info(userRequest);
logger.info("구글발급해준 토큰 : " + userRequest.getAccessToken().getTokenValue());
logger.info("provider : " + userRequest.getClientRegistration().getRegistrationId());
logger.info("providerId : " + oAuth2User.getAttribute("sub"));
String username = provider + "_" + providerId;
User user = userDao.login(username); // 회원가입이 되었는지 유무 체크하기위해 추가함
// 구글 로그인이 처음인 경우
if(user == null) {
user = User.builder()
.username(username)
.password("123")
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userDao.memberInsert(user);
}
return new PrincipalDetails(user, oAuth2User.getAttributes());
}
}
<스프링 시큐리티 복습 완료 - User.java>
package com.example.demo.model;
import java.sql.Timestamp;
import lombok.Builder;
import lombok.Data;
@Data // getter, setter생성해줌
public class User {
private int id;
private String username;
private String password;
private String email;
private String role; // ROLE_USER, ROLE_MANAGER, ROLE_ADMIN
private String createDate;
private String provider;
private String providerId;
// 회원가입에 사용할 생성자 추가
// @Builder하면 안됨! - 들고온 정보를 모두 초기화하기에! 문법상 써놓은 생성자임!
public User() {}
// UserRepository의 findByUsername으로 찾아낸 정보로 초기화가됨
@Builder
public User(String username, String password, String email
, String role, String createDate, String provider, String providerId) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.createDate = createDate;
this.provider = provider;
this.providerId = providerId;
}
}
<스프링 시큐리티 복습 완료 - member.xml>
<?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="com.example.demo">
<!-- 회원 정보 입력 -->
<insert id="memberInsert" parameterType="map">
INSERT INTO member202210(
id
, username
, password
, email
, role
, createDate
, provider
, providerId
)
VALUES (SEQ_MEMBER202210_ID.nextval
<if test="username != null">
, #{username}
</if>
<if test="password != null">
, #{password}
</if>
<if test="email != null">
, #{email}
</if>
<if test="role != null">
, #{role}
</if>
, to_char(sysdate, 'YYYY-MM-DD')
<if test="provider != null">
, #{provider}
</if>
<if test="providerId != null">
, #{providerId}
</if>
)
</insert>
<!-- 로그인 -->
<select id="login" parameterType="string" resultType="com.example.demo.model.User">
SELECT id, username, password, role, email, createdate
FROM member202210
WHERE username = #{value}
</select>
<!-- 목록 출력 -->
<select id="memberList" parameterType="map" resultType="map">
select mem_uid, mem_name, mem_email
,mem_no, mem_nickname, mem_status, mem_auth
from member230324
<where>
<if test='mem_uid!=null and mem_uid.length()>0'>
AND mem_uid = #{mem_uid}
</if>
<!--
<input type=text id="mem_nickname" value=""/>
항상 무조건 빈문자열이다. 폼전송하면 무조건 빈문자열이 있는 상태이다
너가 아무것도 입력하지 않아도 null에 걸리지 않는다
잡아내려면 문자열이 > 0 까지를 비교해야 잡아낼수 있다
-->
<if test='MEM_NICKNAME!=null and MEM_NICKNAME.length()>0'>
AND mem_nickname = #{MEM_NICKNAME}
</if>
<if test='mem_name!=null and mem_name.length()>0'>
AND mem_name = #{mem_name}
</if>
<if test='mem_tel!=null and mem_tel.length()>0'>
AND mem_tel = #{mem_tel}
</if>
</where>
</select>
</mapper>
<스프링 시큐리티 복습 완료 - index.jsp>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JSP</title>
<%@ include file="../common/bootstrap_common.jsp" %>
<link rel="stylesheet" type="text/css" href="/css/common.css">
<script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>
<script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=e83164cf4f1a17d846fcb5c4a6c4c365"></script>
<script type="text/javascript">
let map;
let message;
let container;
const positions = [
{
content: '<div>터짐블로그</div>',
latlng: new kakao.maps.LatLng(33.450701, 126.570667)
}
]
// 현재 위치 버튼 클릭시 호출되는 메소드
const geoloc = () => {
if(navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(pos) {
const lat = pos.coords.latitude; // 위도
const lon = pos.coords.longitude; // 경도
const locPosition = new kakao.maps.LatLng(lat, lon);
map = new kakao.maps.Map(container, options);
message = '<div style="padding: 5px">현재위치</div>';
const marker = new kakao.maps.Marker({
map: map,
position: locPosition,
})
const iwContent = message, iwRemoveable = true;
const infowindow = new kakao.maps.InfoWindow({
content: iwContent,
removeable: iwRemoveable
})
infowindow.open(map, marker)
map.setCenter(locPosition);
})
} else {
console.log('해당 브라우저는 지원하지 않습니다.')
}
}
</script>
</head>
<body>
<div class='divContainer'>
<%@ include file="../include/header.jsp" %>
</div>
<div class='divHeader'>
</div>
<div class='divForm'>
<h2>이벤트존</h2>
<hr style="height: 2px" />
<h2>추천상품존</h2>
<hr style="height: 2px" />
<div class='divMapParent'>
<div id="map" class='divMap' style="width:500px;height:400px;"></div>
<div style="margin-bottom: 5px"></div>
<input class='btnGeoLoc' type="button" onclick="geoloc()" value='현재위치'>
<script type="text/javascript">
container = document.querySelector("#map");
const options = {
center: positions[0].latlng,
level: 3
}
map = new kakao.maps.Map(container, options)
for(let i=0; i<positions.length; i++) {
// 마커를 생성하기
const marker = new kakao.maps.Marker({
map: map, // 마커를 표시할 지도
position: positions[i].latlng
})
// 마커에 표시할 infowindow 생성
const infowindow = new kakao.maps.InfoWindow({
content: positions[i].content // 인포윈도우에 표시할 내용
});
// 즉시실행함수
(function(marker, infowindow) {
// 마커에 mouseover이벤트 등록하고 마우스 오버됐을때 인포윈도우 표시 처리
kakao.maps.event.addListener(marker, 'mouseover', function() {
infowindow.open(map, marker)
})
// 마커에 mouseout이벤트 등록하고 마우스가 아웃됐을때 인포윈도우 닫기 처리
kakao.maps.event.addListener(marker, 'mouseout', function() {
infowindow.close()
})
})(marker, infowindow)
}
</script>
</div>
</div>
</body>
</html>
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day105 (0) | 2023.04.28 |
---|---|
국비 지원 개발자 과정_Day104 (0) | 2023.04.27 |
국비 지원 개발자 과정_Day102 (0) | 2023.04.25 |
국비 지원 개발자 과정_Day101 (0) | 2023.04.25 |
국비 지원 개발자 과정_Day100 (0) | 2023.04.21 |
댓글