http://localhost:9000 ⟶ 요청
http://localhostL9000/login ⟵ 리다이렉트 응답
⟶ 로그인 요청
http://localhost:9000 ⟵ 리다이렉트 응답
Get /login ⟶ 로그인 폼이 응답된다
Post /login ⟶ 로그인 프로세스가 진행된다
Get /logout ⟶ 로그아웃 프로세스가 진행된다
spring07.config 패키지 생성
@Configuration : 설정에 관련된 bean 이라고 알려준다@EnableWebSecurity : Spring Security 를 설정하기 위한 어노테이션@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// security 를 통과하는 white list
String[] whiteList = {"/", "/user/loginform", "/user/login-fail", "/user/expired"};
httpSecurity
.csrf(csrf->csrf.disable()) // csrf 검증 사용하지 않기
.authorizeHttpRequests(config->
config
.requestMatchers(whiteList).permitAll() // whiteList 는 무조건 받아 들이고
.anyRequest().authenticated() // 다른 모든 요청은 인증을 진행한다
)
.formLogin(config->
config
.loginPage("/user/required-loginform")
.loginProcessingUrl("/user/login")
.usernameParameter("userName")
.passwordParameter("password")
.successForwardUrl("/user/login-success")
.failureForwardUrl("/user/login-fail")
.permitAll()
)
.logout(config->
config
.logoutUrl("/user/logout")
.logoutSuccessUrl("/")
.permitAll()
)
.exceptionHandling(config->
config.accessDeniedPage("/user/denied")
)
.sessionManagement(config->
config
.maximumSessions(1)
.expiredUrl("/user/expired")
);
return httpSecurity.build();
}
}
➜ 해당 요청을 처리할 controller 와 view page 는 우리가 만들어야 한다
loginForm & required_loginform & loginSuccess loginFail 메소드 생성@Controller
public class UserController {
@GetMapping("/user/loginform")
public String loginForm() {
// templates/user/loginform.html 페이지로 forward 이동해서 응답
return "user/loginform";
}
// 로그인이 필요한 요청경로를 로그인하지 않은 상태로 요청하면 리다이렉트 되는 경로
@GetMapping("/user/required-loginform")
public String required_loginform() {
return "user/required-loginform";
}
// POST 방식 /user/login 요청 후 로그인 성공인 경우 forward 이동될 url
@PostMapping("/user/login-success")
public String loginSuccess() {
return "user/login-success";
}
// 로그인 폼을 제출(post) 한 로그인 프로세스 중에 forward 되는 경로이기 때문에 @PostMapping 임에 주의!
@PostMapping("/user/login-fail")
public String loginFail() {
// 로그인 실패임을 알릴 페이지
return "user/login-fail";
}
}
<ul>
<li><a th:href="@{/study}">STUDY</a></li>
</ul>
study 메소드 생성@GetMapping("/study")
@ResponseBody
public String study() {
return "let's study";
}
<h3>로그인이 필요 합니다</h3>
<form th:action="@{/user/login}" method="post">
<input type="text" name="userName" placeholder="사용자명..."/>
<input type="password" name="password" placeholder="비밀번호..."/>
<button type="submit">로그인</button>
</form>
<p>로그인 되었습니다</p>
<a th:href="@{/}">인덱스로</a>
<script>
alert("아이디 혹은 비밀번호가 틀려요");
location.href="[[@{/user/loginform}]]";
</script>
STUDY 링크를 클릭하면 해당 페이지로 이동

⭐ "study config" 는 화이트리스트에 존재하지 않기에
인증한 사용자만 접근 가능하여 로그인 폼으로 리다이렉트 된다
SecurityConfig 에서 .formLogin(config-> 안의 코드 분석
.loginPage("/user/required-loginform")`
.loginProcessingUrl("/user/login")`
<input type="text" name="userName">.usernameParameter("userName")
<input type="text" name="password">.passwordParameter("password")
.successForwardUrl("/user/login-success")
.failureForwardUrl("/user/login-fail")
.permitAll()
로그인 결과 확인
⭐ /user/login-success 가 아닌 /user/login 으로 forward 이동
요청 처리는 /user/login-success 에서 했다

sec:authorize="isAuthenticated()"
로그인된 사용자만 보이게 한다
sec:authorize="!isAuthenticated()"
로그인 안 한 사용자만 보이게 한다
sec:authentication="name"
로그인된 사용자의 이름=출력
[[${#authentication.principal.username}]]
로그인한 사용자 객체의 username 속성 출력
<div sec:authorize="isAuthenticated()">
<strong sec:authentication="name"></strong> 님 로그인중
<br />
<strong>[[${#authentication.principal.username}]]</strong> 님 로그인중
<br />
<a th:href="@{/user/logout}">로그아웃</a>
</div>
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/user/loginform}">로그인 하러 가기</a>
</div>
<h3>로그인 폼</h3>
<form th:action="@{/user/login}" method="post">
<input type="text" name="userName" placeholder="사용자명"/>
<input type="password" name="password" placeholder="비밀번호"/>
<button type="submit">로그인</button>
</form>
// 비밀번호를 암호화 해주는 객체를 bean 으로 만든다
@Bean
PasswordEncoder passwordEncoder() {
// 여기서 리턴해주는 객체도 bean 으로 된다
return new BCryptPasswordEncoder();
}
// 인증 메니저 객체를 bean 으로 만든다 (Spring Security 가 자동 로그인 처리할때도 사용되는 객체)
@Bean
AuthenticationManager authenticationManager(HttpSecurity http,
BCryptPasswordEncoder encoder, UserDetailsService service) throws Exception{
// 적절한 설정을한 인증 메니저 객체를 리턴해주면 bean 이 되어서 Spring Security 가 사용한다
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(service)
.passwordEncoder(encoder)
.and()
.build();
}
spring07.service 패키지 생성
Spring Security 가 로그인 처리시 호출하는 메소드를 가지고 있는 서비스 클래스 정의하기@Service // bean 으로 만들기 위한 어노테이션
public class CustomUserDetailService implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
spring07.dto 패키지 생성
@AllArgsConstructor@NoArgsConstructor : 빈 생성자@Data : setter & getter@Builder
@AllArgsConstructor // 모든 필드값을 전달받는 생성자 (Builder 를 사용하려면 필요)
@NoArgsConstructor // 빈 생성자
@Data // setter & getter
public class UserDto {
private long num;
private String userName;
private String password;
private String email;
private String role;
private String profileImage;
private String createdAt;
private String updatedAt;
}
loadUserByUsername 메소드 안에 코드 추가@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 원래는 DB 에서 dao 를 이용해 username 에 해당하는 사용자정보(UserDto) 를 얻어와야 한다
// 지금은 sample 데이터를 만들어서 사용하기
// 실제 DB 에는 ROLE_XXX 형식으로 저장이 되어 있어야한다
String role="";
if(username.equals("kimgura")) {
role="ROLE_USER";
}else if(username.equals("batman")) {
role="ROLE_STAFF";
}else if(username.equals("superman")){
role="ROLE_ADMIN";
}
// 비밀번호를 암호화 하기 위한 객체
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// DB 에는 암호화된 비밀번호가 저장되어 있다고 가정하자 (암호화 하기 이전의 비밀번호가 1234 라고 가정)
String encodedPwd = encoder.encode("1234");
// Sample UserDto 객체 만들기(원래는 DB 에서 읽어온 데이터)
UserDto dto=UserDto.builder()
.userName(username)
.password(encodedPwd)
.email("aaa@naver.com")
.role(role)
.build();
// 권한 목록을 List 에 담아서 (지금은 1개 이지만)
List<GrantedAuthority> authList = new ArrayList<>();
authList.add(new SimpleGrantedAuthority(dto.getRole()));
// UserDetails 객체를 생성해서
UserDetails ud = new User(dto.getUserName(), dto.getPassword(), authList);
// 리턴해준다
return ud;
}
SecurityConfig 에 requestMatchers 코드 추가
역할에 따라서 요청 가능한 경로를 구분
.authorizeHttpRequests(config->
config
.requestMatchers(whiteList).permitAll() // whiteList 는 무조건 받아 들이고
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/staff/**").hasAnyRole("ADMIN", "STAFF")
.anyRequest().authenticated() // 다른 모든 요청은 인증을 진행한다
)
userDenied & userList & userManage 메소드 추가// 권한 부족 시 or 403 인 경우
@RequestMapping("/user/denied")
public String userDenied() { // GET, POST 등 모두 가능
return "user/denied";
}
// ROLL_STAFF, ROLL_ADMIN 만 요청 가능
@GetMapping("/staff/user/list")
public String userList() {
return "user/list";
}
// ROLL_ADMIN 만 요청 가능
@GetMapping("/admin/user/manage")
public String userManage() {
return "user/manage";
}
<li><a th:href="@{/staff/user/list}">회원 목록(STAFF, ADMIN 전용)</a></li>
<li><a th:href="@{/admin/user/manage}">회원 관리(ADMIN 전용)</a></li>
SecurityConfig 에서 .logout(config-> 안의 코드 분석
.logoutUrl("/user/logout")
.logoutSuccessUrl("/")
SecurityConfig 에서 .exceptionHandling(config-> 안의 코드 분석
config.accessDeniedPage("/user/denied")
SecurityConfig 에서 .sessionManagement(config-> 안의 코드 분석
.maximumSessions(1)
.expiredUrl("/user/expired")
CustomUserDetailService 클래스 코드 분석
클라이언트가 로그인을 시도하면 이 메소드를 spring security 가 호출한다
메소드를 호출하면서 클라이언트가 입력한 userName 을 전달해준다
우리는 전달된 userName 을 이용해서 DB 에서 해당 유저의 정보를 select 해야 한다
만일 select 된 유저 정보가 없으면 예외를 발생시키면 되고
select 된 정보가 있다면 UserDetails type 객체에 select 된 정보 (비밀번호, 권한 등)를 담아서 리턴한다
이 메소드에서 리턴한 UserDetails 객체를 이용해서 spring security 가 로그인 처리를 대신해 준다
@Service
public class CustomUserDetailService implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String role="";
if(username.equals("kimgura")) {
role="ROLE_USER";
}else if(username.equals("batman")) {
role="ROLE_STAFF";
}else if(username.equals("superman")){
role="ROLE_ADMIN";
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPwd = encoder.encode("1234");
UserDto dto=UserDto.builder()
.userName(username)
.password(encodedPwd)
.email("aaa@naver.com")
.role(role)
.build();
List<GrantedAuthority> authList = new ArrayList<>();
authList.add(new SimpleGrantedAuthority(dto.getRole()));
UserDetails ud = new User(dto.getUserName(), dto.getPassword(), authList);
return ud;
}
}
AuthSuccessHandler 클래스 생성
로그인 성공 후에 호출될 메소드를 가지고 있는 클래스 정의하기
public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
// 1. 요청 캐쉬 객체를 직접 생성해서
private RequestCache requestCache = new HttpSessionRequestCache();
// 2. 생성자에서 부모 객체에 전달
public AuthSuccessHandler() {
super.setRequestCache(requestCache);
}
// 입력한 username 와 password 가 일치해서 로그인이 성공하면 이 메소드가 호출된다
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
// 세션 유지 시간 설정
HttpSession session=request.getSession();
session.setMaxInactiveInterval(60);//초단위로 설정
// Authentication 객체의 메소드를 이용해서 지금 로그인된 사용자에 대한 자세한 정보를 얻어낼수 있다
String userName = authentication.getName();
System.out.println("로그인된 사용자 : "+userName);
// 3. 로그인 성공이후 미리 저장된 요청이 있었는지 읽어와서
SavedRequest cashed = requestCache.getRequest(request, response);
//4. 만일 미리 저장된 요청이 없다면 (로그인 하지 않은 상태로 인증이 필요한 경로를 요청하지 않았다면)
if(cashed==null) {
// 5. 로그인 환영 페이지로 foward 이동 (POST 방식 요청임에 주의!!!)
RequestDispatcher rd = request.getRequestDispatcher("/user/login-success");
rd.forward(request, response);
} else {
// 6. 원래 가려던 목적지 경로로 리다일렉트 이동 시킨다 (GET 방식 요청 파라미터도 자동으로 같이 가지고 간다)
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
// .successForwardUrl("/user/login-success") : 주석 처리
// 로그인 성공 시 동작하는 핸들러 객체 등록하기
.successHandler(new AuthSuccessHandler())
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
Thymeleaf에서 Spring Security 태그 사용을 위한 xmlns 설정

xmlns:sec="http://www.thymeleaf.org/extras/spring-security" ➜ OK
study 메소드에 코드 추가// 컨트롤러나 서비스에서 로그인된 userName 을 알고 싶을 때
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println("로그인된 userName : "+userName);
Spring08_Example 프로젝트 생성
패키지 생성
1. spring08_controller
2. spring08_dto
3. spring08_repository
4. spring08_config
5. spring08_service
server.port=9000
server.servlet.context-path=/spring08
# oracle DataSource Setting ( Connection Pool )
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=scott
spring.datasource.password=TIGER
# mybatis 에서 사용하는 xml 문서가 어디에 있는지 알려주기
# classpath: 은 resources 폴더를 가리킨다
mybatis.mapper-locations=classpath:mapper/*.xml
# type 에 별칭을 붙인 클래스를 찾아서 로딩하기 위해
# com.example.spring04. 하위 패키지에서 모든 @Alias 가 붙은 클래스를 찾아서 사용할 준비를 한다
mybatis.type-aliases-package=com.example.spring08.**
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
List<String> notice = List.of("공지사항 입니다", "공지1", "공지2");
// 응답에 필요한 정보를 Model 객체에 담고
model.addAttribute("notice", notice);
// Thymeleaf 템플릿 페이지에서 응답 하기
return "home";
}
}
<div class="container">
<h1>인덱스 페이지</h1>
<h2>공지사항</h2>
<ul>
<li th:each="tmp : ${notice}" th:text="${tmp}"></li>
</ul>
</div>
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserDto {
private long num;
private String userName;
private String password;
private String email;
private String role;
private String profileImage;
private String createdAt;
private String updatedAt;
}
public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
private RequestCache requestCache = new HttpSessionRequestCache();
public AuthSuccessHandler() {
super.setRequestCache(requestCache);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
HttpSession session=request.getSession();
session.setMaxInactiveInterval(60);
String userName = authentication.getName();
System.out.println("로그인된 사용자 : "+userName);
SavedRequest cashed = requestCache.getRequest(request, response);
if(cashed==null) {
RequestDispatcher rd = request.getRequestDispatcher("/user/login-success");
rd.forward(request, response);
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
String[] whiteList = {"/", "/user/loginform", "/user/login-fail", "/user/expired"};
httpSecurity
.csrf(csrf->csrf.disable())
.authorizeHttpRequests(config->
config
.requestMatchers(whiteList).permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/staff/**").hasAnyRole("ADMIN", "STAFF")
.anyRequest().authenticated()
)
.formLogin(config->
config
.loginPage("/user/required-loginform")
.loginProcessingUrl("/user/login")
.usernameParameter("userName")
.passwordParameter("password")
.successHandler(new AuthSuccessHandler())
.failureForwardUrl("/user/login-fail")
.permitAll()
)
.logout(config->
config
.logoutUrl("/user/logout")
.logoutSuccessUrl("/")
.permitAll()
)
.exceptionHandling(config->
config.accessDeniedPage("/user/denied")
)
.sessionManagement(config->
config
.maximumSessions(1)
.expiredUrl("/user/expired")
);
return httpSecurity.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
AuthenticationManager authenticationManager(HttpSecurity http,
BCryptPasswordEncoder encoder, UserDetailsService service) throws Exception{
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(service)
.passwordEncoder(encoder)
.and()
.build();
}
}
@Controller
public class UserController {
@RequestMapping("/user/denied")
public String userDenied() {
return "user/denied";
}
@GetMapping("/user/loginform")
public String loginForm() {
return "user/loginform";
}
@GetMapping("/user/required-loginform")
public String required_loginform() {
return "user/required-loginform";
}
@PostMapping("/user/login-success")
public String loginSuccess() {
return "user/login-success";
}
@PostMapping("/user/login-fail")
public String loginFail() {
return "user/login-fail";
}
}
<div sec:authorize="isAuthenticated()">
<strong sec:authentication="name"></strong>님 로그인중
<a th:href="@{/user/logout}">로그아웃</a>
</div>
<div sec:authorize="!isAuthenticated()">
로그인중X
<a th:href="@{/user/loginform}">로그인 하러 가기</a>
<a th:href="@{/user/signup-form}">회원가입</a>
</div>
<mapper namespace="user">
<!-- 자주 사용하는 칼럼의 이름을 미리 정의해 놓고 참조해서 사용하기 -->
<sql id="columnName">
num, userName, password, email, profileImage, role, updatedAt, createdAt
</sql>
<update id="update" parameterType="com.example.spring08.dto.UserDto">
UPDATE users
SET email=#{email}, updatedAt=SYSDATE
<if test="profileImage != null">, profileImage=#{profileImage}</if>
WHERE userName=#{userName}
</update>
<update id="updatePassword" parameterType="com.example.spring08.dto.UserDto">
UPDATE users
SET password=#{newPassword}
WHERE userName=#{userName}
</update>
<insert id="insert" parameterType="com.example.spring08.dto.UserDto">
INSERT INTO users
(num, userName, password, email)
VALUES(users_seq.NEXTVAL, #{userName}, #{password}, #{email})
</insert>
<select id="getByNum" parameterType="long" resultType="com.example.spring08.dto.UserDto">
SELECT <include refid="columnName"/>
FROM users
WHERE num=#{num}
</select>
<select id="getByUserName" parameterType="string" resultType="com.example.spring08.dto.UserDto">
SELECT <include refid="columnName"/>
FROM users
WHERE userName=#{userName}
</select>
</mapper>
public interface UserDao {
public void insert(UserDto dto);
public int update(UserDto dto);
public int updatePassword(UserDto dto);
public UserDto getByNum(long num);
public UserDto getByUserName(String userName);
}
@Repository 어노테이션을 붙여서 bean 생성SqlException 이 발생하면 자동으로 DataAccessException 으로 변환해서 발생시켜 준다@Repository
@RequiredArgsConstructor
public class UserDaoImpl implements UserDao{
// lombok 에서 제공해주는 @RequiredArgsConstructor 어노테이션을 이용해서 의존 객체를 생성자로 주입받도록 한다
private final SqlSession session;
@Override
public void insert(UserDto dto) {
session.insert("user.insert", dto);
}
@Override
public int update(UserDto dto) {
return session.update("user.update", dto);
}
@Override
public int updatePassword(UserDto dto) {
return session.update("user.updatePassword", dto);
}
@Override
public UserDto getByNum(long num) {
UserDto dto = session.selectOne("user.getByNum", num);
return dto;
}
@Override
public UserDto getByUserName(String userName) {
UserDto dto = session.selectOne("user.getByUserName", userName);
return dto;
}
}
@Service // bean 으로 만들기 위한 어노테이션
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService{
// UserDao 객체 주입 받기
private final UserDao dao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// DB 에서 userName 에 해당하는 정보가 있는지 select 해준다
UserDto dto = dao.getByUserName(username);
// 만일 존재 하지 않는 사용자라면
if(dto == null) {
throw new UsernameNotFoundException("존재하지 않은 사용자 입니다");
}
// 권한 목록을 List 에 담아서 (지금은 1개 이지만)
List<GrantedAuthority> authList = new ArrayList<>();
authList.add(new SimpleGrantedAuthority(dto.getRole()));
// UserDetails 객체를 생성해서
UserDetails ud = new User(dto.getUserName(), dto.getPassword(), authList);
// 리턴해준다
return ud;
}
}
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
"/user/expired", "/user/signup-form"};
UserController 에 signupForm 메소드 생성
회원가입 폼 요청 처리
@GetMapping("/user/signup-form")
public String signupForm() {
return "user/signup-form";
}
signup-form.html 생성
novalidate : 브라우저 기본 폼 검사 동작을 꺼주는 속성
<div class="container">
<h1>회원가입 폼 양식</h1>
<form th:action="@{/user/signup}" method="post" novalidate>
<div>
<label for="userName">아이디</label>
<input type="text" name="userName" id="userName"/>
</div>
<div>
<label for="password">비밀번호</label>
<input type="text" name="password" id="password"/>
</div>
<div>
<label for="email">이메일</label>
<input type="email" name="email" id="email"/>
</div>
<button type="submit">가입</button>
</form>
</div>
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
"/user/expired", "/user/signup-form", "/user/signup"};
UserDto 에 signup 메소드 생성
회원가입 요청 처리
@PostMapping("/user/signup")
public String signup(UserDto dto) {
return "user/signup";
}
public interface UserService {
public void createUser(UserDto dto);
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{
private final UserDao dao;
// 사용자를 추가하는 메소드
@Override
public void createUser(UserDto dto) {
dao.insert(dto);
}
}
signup 메소드 수정 및 의존 코드 추가@RequiredArgsConstructor
private final UserService service;
// 회원가입 요청 처리
@PostMapping("/user/signup")
public String signup(UserDto dto) {
// UserService 객체를 이용해서 사용자 정보를 추가
service.createUser(dto);
return "user/signup";
}
UserServiceImpl 비밀번호 암호화 코드 추가
비밀번호를 암호화 하기 위한 객체도 spring bean container 로 주입 받는다
private final PasswordEncoder encoder;
// 사용자를 추가하는 메소드
@Override
public void createUser(UserDto dto) {
// 날 것의 비밀번호를 암호화해서
String encodedPwd = encoder.encode(dto.getPassword());
// dto 에 다시 담는다
dto.setPassword(encodedPwd);
// DB 에 저장
dao.insert(dto);
}
<div class="container">
<strong th:text="${param.userName}"></strong>님 가입되었습니다
<a th:href="@{/user/loginform}">로그인</a>
</div>