스프링 시큐리티를 사용하여 로그인 기능을 구현해보기 전, 이전 포스팅을 보고 온다면 더 이해가 쉬울 것이다.
2021.08.21 - [WEB developer/Spring] - [Spring] 스프링 시큐리티 이해하기
먼저 스프링 시큐리티 기본 설정을 해야할 텐데, 아래 포스팅의 기본 설정들이 되어있다는 가정 하에 진행된다.
2021.08.20 - [WEB developer/Spring] - [Spring] 기본 설정 + Spring Security 설정
1. 데이터베이스 모델링, 테이블 생성, 데이터 추가
drop table if exists member_role;
drop table if exists member;
-- -----------------------------------------------------
-- Table `member`
-- -----------------------------------------------------
CREATE TABLE `member` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'member id',
`name` VARCHAR(255) NOT NULL COMMENT 'member name',
`password` VARCHAR(255) NOT NULL COMMENT '암호회된 password',
`email` VARCHAR(255) NOT NULL UNIQUE COMMENT 'login id, email',
`create_date` DATETIME NULL DEFAULT NULL COMMENT '등록일',
`modify_date` DATETIME NULL DEFAULT NULL COMMENT '수정일',
PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- -----------------------------------------------------
-- Table `member_role`
-- -----------------------------------------------------
CREATE TABLE `member_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'role id',
`member_id` INT(11) NOT NULL COMMENT 'member id fk',
`role_name` VARCHAR(100) NOT NULL COMMENT 'role 이름 ROLE_ 로 시작하는 값이어야 한다.',
PRIMARY KEY (`id`),
FOREIGN KEY (`member_id`)
REFERENCES `member` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into member (id, name, password, email, create_date, modify_date) values ( 1, '김동준', '$2a$10$G/ADAGLU3vKBd62E6GbrgetQpEKu2ukKgiDR5TWHYwrem0cSv6Z8m', 'carami@example.com', now(), now());
insert into member (id, name, password, email, create_date, modify_date) values ( 2, '배다연', '$2a$10$G/ADAGLU3vKBd62E6GbrgetQpEKu2ukKgiDR5TWHYwrem0cSv6Z8m', 'toto@example.com', now(), now());
insert into member_role (id, member_id, role_name) values (1, 1, 'ROLE_USER');
insert into member_role (id, member_id, role_name) values (2, 1, 'ROLE_ADMIN');
insert into member_role (id, member_id, role_name) values (3, 2, 'ROLE_USER');
2. 데이터베이스로부터 읽어들이기 위한 DTO와 DAO작성
Member.java
import java.util.Date;
public class Member {
private Long id;
private String name;
private String password;
private String email;
private Date createDate;
private Date modifyDate;
public Member() {
createDate = new Date();
modifyDate = new Date();
}
public Member(Long id, String name, String password, String email) {
this();
this.name = name;
this.password = password;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Date getModifyDate() {
return modifyDate;
}
public void setModifyDate(Date modifyDate) {
this.modifyDate = modifyDate;
}
}
email정보와 일치하는 회원 정보를 읽는 Sql 클래스 작성
MemberDaoSqls.java
public class MemberDaoSqls {
public static final String SELECT_ALL_BY_EMAIL = "SELECT id, name, password, email, create_date, modify_date FROM member WHERE email = :email";
}
MemberDao클래스 작성
MemberDao.java
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.edwith.webbe.securityexam.dto.Member;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class MemberDao {
private NamedParameterJdbcTemplate jdbc;
// BeanPropertyRowMapper는 Role클래스의 프로퍼티를 보고 자동으로 칼럼과 맵핑해주는 RowMapper객체를 생성한다.
// roleId 프로퍼티는 role_id 칼럼과 맵핑이 된다.
// 만약 프로퍼티와 칼럼의 규칙이 맞아 떨어지지 않는다면 직접 RowMapper객체를 생성해야 한다.
// 생성하는 방법은 아래의 rowMapper2를 참고한다.
private RowMapper<Member> rowMapper = BeanPropertyRowMapper.newInstance(Member.class);
public MemberDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
}
public Member getMemberByEmail(String email) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("email", email);
return jdbc.queryForObject(MemberDaoSqls.SELECT_ALL_BY_EMAIL, map, rowMapper);
}
}
회원의 권한(Role)정보를 저장하기 위한 MemberRole DTO클래스 작성
MemberRole.java
public class MemberRole {
private Long id;
private Long memberId;
private String roleName;
public MemberRole() {
}
public MemberRole(Long memberId, String roleName) {
this.memberId = memberId;
this.roleName = roleName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
email에 해당하는 권한 정보를 읽어들이기 위해서 member 테이블과 member_role 테이블을 JOIN하여 결과를 얻는 Sql을 가진 MemberRoleDaoSqls 클래스 작성
MemberRoleDaoSqls.java
public class MemberRoleDaoSqls {
public static final String SELECT_ALL_BY_EMAIL =
"SELECT mr.id, mr.member_id, mr.role_name "
+ "FROM member_role mr "
+ "JOIN member m ON mr.member_id = m.id "
+ "WHERE m.email = :email";
}
권한 정보를 읽어들이는 MemberRoleDao 클래스 작성
MemberRoleDao.java
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.edwith.webbe.securityexam.dto.MemberRole;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class MemberRoleDao {
private NamedParameterJdbcTemplate jdbc;
// BeanPropertyRowMapper는 Role클래스의 프로퍼티를 보고 자동으로 칼럼과 맵핑해주는 RowMapper객체를 생성한다.
// roleId 프로퍼티는 role_id 칼럼과 맵핑이 된다.
// 만약 프로퍼티와 칼럼의 규칙이 맞아 떨어지지 않는다면 직접 RowMapper객체를 생성해야 한다.
// 생성하는 방법은 아래의 rowMapper2를 참고한다.
private RowMapper<MemberRole> rowMapper = BeanPropertyRowMapper.newInstance(MemberRole.class);
public MemberRoleDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
}
public List<MemberRole> getRolesByEmail(String email){
Map<String, Object> map = new HashMap<String, Object>();
map.put("email", email);
return jdbc.query(MemberRoleDaoSqls.SELECT_ALL_BY_EMAIL, map, rowMapper);
}
}
데이터베이스 접속, MemberDao, MemberRoleDao클래스가 알맞게 동작하는지 확인하기 위해 테스트 클래스를 작성한다.
MemberDaoTest.java
import java.sql.Connection;
import javax.sql.DataSource;
import org.edwith.webbe.securityexam.config.ApplicationConfig;
import org.edwith.webbe.securityexam.dto.Member;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ApplicationConfig.class})
public class MemberDaoTest {
@Autowired
DataSource dataSource;
@Autowired
MemberDao memberDao;
@Autowired
MemberRoleDao memberRoleDao;
@Test
public void configTest() throws Exception{
// 아무 작업도 하지 않는다. 실행이 잘된다는 것은 Spring 설정이 잘 되어 있다는 것을 의미한다.
}
@Test
public void connectionTest() throws Exception{
Connection connection = dataSource.getConnection();
Assert.assertNotNull(connection);
}
@Test
public void getUser() throws Exception{
Member member = memberDao.getMemberByEmail("carami@example.com");
Assert.assertNotNull(member);
Assert.assertEquals("김동준", member.getName());
}
}
3. Login/Logout 처리를 위한 클래스 작성하기
1. 먼저 로그인 아이디와 암호 정보를 갖고 있는 UserEntity 클래스를 생성한다.
UserEntity.java
public class UserEntity {
private String loginUserId;
private String password;
public UserEntity(String loginUserId, String password) {
this.loginUserId = loginUserId;
this.password = password;
}
public String getLoginUserId() {
return loginUserId;
}
public void setLoginUserId(String loginUserId) {
this.loginUserId = loginUserId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2. 로그인 아이디와 권한(Role) 정보를 갖는 UserRoleEntity 클래스를 생성한다.
UserRoleEntity.java
public class UserRoleEntity {
private String userLoginId;
private String roleName;
public UserRoleEntity(String userLoginId, String roleName) {
this.userLoginId = userLoginId;
this.roleName = roleName;
}
public String getUserLoginId() {
return userLoginId;
}
public void setUserLoginId(String userLoginId) {
this.userLoginId = userLoginId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
3. 로그인한 사용자의 ID를 파라미터로 받아들여서 UserEntity와 List<UserRoleEntity>를 리턴하는 메소드를 가지고 있는 UserDbService 인터페이스를 작성한다.
로그인 정보가 저장된 데이터베이스의 구조는 프로젝트마다 다르기 때문에 해당 인터페이스를 구현해야한다.
UserDbService.java
import java.util.List;
public interface UserDbService {
public UserEntity getUser(String loginUserId);
public List<UserRoleEntity> getUserRoles(String loginUserId);
}
4. 데이터베이스에서 읽어 들인 로그인 정보는 UserDetails인터페이스를 구현하고 있는 객체에 저장되어야 한다. UserDetails를 구현하고 있는 CustomUserDetails 클래스를 생성한다.
CustomUserDetails.java
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails implements UserDetails{
private String username;
private String password;
private boolean isEnabled;
private boolean isAccountNonExpired;
private boolean isAccountNonLocked;
private boolean isCredentialsNonExpired;
private Collection<?extends GrantedAuthority>authorities;
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
public void setAccountNonExpired(boolean accountNonExpired) {
isAccountNonExpired = accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
public void setAccountNonLocked(boolean accountNonLocked) {
isAccountNonLocked = accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
isCredentialsNonExpired = credentialsNonExpired;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
5. UserDetailsService 인터페이스를 구현하는 CustomUserDetailsService를 생성한다.
CustomUserDetailsService.java
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
// CustomUserDbService는 인터페이스다. 해당 인터페이스를 구현하고 있는 객체가 Bean으로 등록되어 있어야 한다.
@Autowired
UserDbService userDbService;
@Override
public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
// loginId에 해당하는 정보를 데이터베이스에서 읽어 CustomUser객체에 저장한다.
UserEntity customUser = userDbService.getUser(loginId);
if(customUser == null)
throw new UsernameNotFoundException("사용자가 입력한 아이디에 해당하는 사용자를 찾을 수 없습니다.");
// 해당 정보를 CustomUserDetails객체에 저장한다.
CustomUserDetails customUserDetails = new CustomUserDetails();
customUserDetails.setUsername(customUser.getLoginUserId());
customUserDetails.setPassword(customUser.getPassword());
List<UserRoleEntity> customRoles = userDbService.getUserRoles(loginId);
//로그인한 사용자의 권한 정보를 GrantedAuthority를 구현하고 있는 SimpleGrantedAuthority객체에 담아 리스트에 추가한다.
//MemberRole 이름은 "ROLE_"로 시작되어야 한다.
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if(customRoles != null) {
for(UserRoleEntity customRole : customRoles) {
authorities.add(new SimpleGrantedAuthority(customRole.getRoleName()));
}
}
//CustomUserDetails 객체에 권한 목록 (authorities)를 설정한다.
customUserDetails.setAuthorities(authorities);
customUserDetails.setEnabled(true);
customUserDetails.setAccountNonExpired(true);
customUserDetails.setAccountNonLocked(true);
customUserDetails.setCredentialsNonExpired(true);
return customUserDetails;
}
}
6. UserDbService 인터페이스를 상속받는 MemberService 인터페이스를 작성한다.
UserDbService는 스프링 시큐리티에서 필요로 하는 정보를 갖고 오는 메소드를 갖고 있다. MemberService는 회원과 관련된 모든 정보를 처리하는 서비스이다(회원 등록등..).
MemberService.java
import org.edwith.webbe.securityexam.service.security.UserDbService;
public interface MemberService extends UserDbService{
}
7. MemberServiceImpl클래스는 MemberService인터페이스를 구현한다.
MemberService인터페이스를 구현한다는 것은 UserDbService 역시 구현해야한다는 것이다.
MemberServiceImpl.java
import java.util.ArrayList;
import java.util.List;
import org.edwith.webbe.securityexam.dao.MemberDao;
import org.edwith.webbe.securityexam.dao.MemberRoleDao;
import org.edwith.webbe.securityexam.dto.Member;import org.edwith.webbe.securityexam.dto.MemberRole;
import org.edwith.webbe.securityexam.service.security.UserEntity;
import org.edwith.webbe.securityexam.service.security.UserRoleEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MemberServiceImpl implements MemberService {
// 생성자에 위해 주입되는 객체이고, 해당 객체를 초기화할 필요가 이후에 없기 때문에 final로 선언하였다.
// final로 선언하고 초기화를 안한 필드는 생성자에서 초기화를 해준다.
private MemberDao memberDao;
private MemberRoleDao memberRoleDao;
// @Service가 붙은 객체는 스프링이 자동으로 Bean으로 생성하는데
// 기본생성자가 없고 아래와 같이 인자를 받는 생성자만 있을 경우 자동으로 관련된 타입이 Bean으로 있을 경우 주입해서 사용하게 된다.
public MemberServiceImpl(MemberDao memberDao, MemberRoleDao memberRoleDao) {
this.memberDao = memberDao;
this.memberRoleDao = memberRoleDao;
}
@Override
@Transactional
public UserEntity getUser(String loginUserId) {
Member member = memberDao.getMemberByEmail(loginUserId);
return new UserEntity(member.getEmail(), member.getPassword());
}
@Override
@Transactional
public List<UserRoleEntity> getUserRoles(String loginUserId) {
List<MemberRole> memberRoles = memberRoleDao.getRolesByEmail(loginUserId);
List<UserRoleEntity> list = new ArrayList<>();
for(MemberRole memberRole : memberRoles) {
list.add(new UserRoleEntity(loginUserId, memberRole.getRoleName()));
}
return list;
}
}
4. 로그인, 로그아웃 처리를 위한 설정 수정
기존의 SecurityConfig.java 파일을 아래와 같이 수정
SecurityConfig.java
import org.edwith.webbe.securityexam.service.security.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.authentication.AuthenticationManagerBeanDefinitionParser;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
// /webjars/** 경로에 대한 요청은 인증/인가 처리하지 않도록 무시합니다.
@Override
public void configure(WebSecurity web) throws Exception{
web.ignoring().antMatchers("/webjars/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(customUserDetailsService);
}
// /와 /main에 대한 요청은 누구나 할 수 있지만, 그 외의 요청은 모두 인증 후 접근 가능합니다
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/main","/members/loginerror","/members/joinform","/members/join","/members/welcome").permitAll()
.antMatchers("/securepage", "/members/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/members/loginform")
.usernameParameter("userId")
.passwordParameter("password")
.loginProcessingUrl("/authenticate")
.failureForwardUrl("/members/loginerror?login_error=1")
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/");
}
//패스워드 인코더를 빈으로 등록합니다.
//암호를 인코딩하거나, 인코딩된 암호와 사용자가 입력한 암호가 같은지 확인할 때 사용합니다.
@Bean
public PasswordEncoder endcoder() {
return new BCryptPasswordEncoder();
}
}
각 부분에 대한 설명
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
WebSecurityConfigurerAdapter가 갖고 있는 void configure(AuthenticationManagerBuilder auth)를 오버라이딩 하고 있다. 해당 메소드를 오버라이딩 한 후 UserDetailService 인터페이스를 구현하고 있는 객체(customUserDetailService)를 auth.userDetailService() 메소드의 인자로 전달하고 있다.
이렇게 설정된 객체는 아이디/암호를 입력받아 로그인을 처리하는 AuthenticationFilter에서 사용하게 된다.
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/main","/members/loginerror","/members/joinform","/members/join","/members/welcome").permitAll()
.antMatchers("/securepage", "/members/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/members/loginform")
.usernameParameter("userId")
.passwordParameter("password")
.loginProcessingUrl("/authenticate")
.failureForwardUrl("/members/loginerror?login_error=1")
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/");
}
"/securepage", "/members/**"는 로그인도 되어 있어야 하고 "USER"권한도 가지고 있어야 접근할 수 있도록 설정하고 있다.
formLogin()
- loginPage : login form 페이지 URL
- usernameParameter : form id의 name 속성값
- passwordParameter : form pw의 name 속성값
- loginProcessingUrl : form action 값 (security를 이용해 인증처리)
- failureForwardUrl : 로그인 처리가 실패하게 되면 해당 url로 포워딩
- defaultSuccessUrl : 로그인 처리가 성공하게 되면 해당 url로 리다이렉트
- permitAll : 해당 로그인 폼은 누구나 접근 가능
logout()
- logoutUrl : 로그아웃 처리할 URL (security가 알아서 만들기 때문에, 이 경로로 요청만 하면된다)
- logoutSuccessUrl : 로그아웃 성공 시 해당 url로 리다이렉트
5. 로그인 처리를 위한 컨트롤러와 뷰 작성하기
MemberController.java
import org.edwith.webbe.securityexam.service.MemberService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping(path = "/members")
public class MemberController {
//스프링 컨테이너가 생성자를 통해 자동으로 주입한다.
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/loginform")
public String loginform() {
return "members/loginform";
}
@RequestMapping("/loginerror")
public String loginerror(@RequestParam("login_error") String loginError) {
return "members/loginerror";
}
}
아이디와 암호를 입력 받는 뷰 작성
members/loginform.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<div>
<div>
<form method="post" action="/securityexam/authenticate">
<div>
<label>ID</label>
<input type="text" name="userId">
</div>
<div>
<label>암호</label>
<input type="password" name="password">
</div>
<div>
<label></label>
<input type="submit" value="로그인">
</div>
</form>
</div>
</div>
</body>
</html>
로그인 오류가 발생할 경우 보여줄 화면 작성
members/loginerror.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 오류</title>
</head>
<body>
<h1>로그인 오류가 발생했습니다. id나 암호를 다시 입력해주세요.</h1>
<a href="/securityexam/members/loginform">login</a>
</body>
</html>
잘 작동하는지 확인해보자.
1. 서버 실행
2. 로그인 페이지 이동 (http://localhost:8080/securityexam/members/loginform)
3. 로그인
url(http://localhost:8080/securityexam/securepage) 접근 가능
4. 로그아웃 (http://localhost:8080/securityexam/logout)
참고 : 부스트코스 웹 백엔드
'Web developer > Spring' 카테고리의 다른 글
[JPA] Entity select만 했는데 update쿼리가 실행될 때 (0) | 2023.01.09 |
---|---|
[Spring] Spring Security ACL Tutorial (1) | 2022.11.27 |
[Spring] 스프링 시큐리티 이해하기 (0) | 2021.08.21 |
[Spring] 기본 설정 + Spring Security 설정 (0) | 2021.08.20 |
[Spring] MockMVC Test (0) | 2021.08.02 |
댓글