3Tier Model
public class Sample {
private String url;
private String username;
private String password;
public Sample(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
}
// 객체의 생성과 초기화
Sample sample = new Sample("jdbc:oracle:thin:@localhost:1521:xe", "hong", "abcd1234");
생성자 메소드를 이용하는 객체의 초기화
- 객체를 초기화하는 방법이 다양한 경우, 해당 경우의 수만큼 생성자 메소드를 중복정의해야한다.
초기화하는 과정에서 전달되는 값을 서로 다르지만, 타입이 동일한 경우 생성자 메소드 중복정의로 해결할 수 없다.
예시 )
public Sample(String url, String username) { ... }
public Sample(String username, String password) { ... }
* 생성자 메소드를 이용하는 객체의 초기화
- 객체를 초기화하는 방법이 다양한 경우, 해당 경우의 수만큼 생성자 메소드를 중복정의해야한다.
초기화하는 과정에서 전달되는 값을 서로 다르지만, 타입이 동일한 경우 생성자 메소드 중복정의로 해결할 수 없다.
예시 )
public Sample(String url, String username) { ... }
public Sample(String username, String password) { ... }
Setter 메소드를 이용하는 객체의 초기화
Setter 메소드가 public 메소드이기 때문에 객체의 생성 직후 Setter 메소드로 초기화가 완료된 이후에도
Setter 메소드를 실행해서 객체의 값을 변경할 위험성이 존재한다.
public class Sample {
private String url;
private String username;
private String password;
public static class SampleBuilder {
private String url;
private String username;
private String password;
public SampleBuilder url(String url) {
this.url = url;
return this;
}
public SampleBuilder username(String usename) {
this.username = username;
return this;
}
public SampleBuilder password(String password) {
this.password = password;
return this;
}
public Sample build() {
Sample sample = new Sample();
sample.url = url;
sample.username = username;
sample.password = password;
return sample;
}
}
}
// 객체의 생성과 초기화
SampleBuilder builder = new Sample.SampleBuilder();
Sample sample = builder.url("jdbc:oracle:thin:@localhost:1521:xe");
.username("hong");
.password("abcd1234");
.build();
Name : springboot-security
type : Maven
packaging : War
Java cersion : 11
Language : Java
Group : com.example
Artifact : springboot-security
Version : 0.0.1-SNAPSHOT
Description : spring-boot and spring-security
Package : com.example
Spring Boot Version : 2.7.8
Spring web
Spring Security
Lombok
Oracle Driver
Mybatis Framework
Validation
Spring Boot DevTools
spring boot version 변경 : 2.6.7
<!--
jsp를 지원하는 내장형 톰캣 라이브러리 의존성 추가
-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--
jstl 태그 라이브러리 의존성 추가
-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
주석처리
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
-->
create table spring_users (
user_id varchar2(100) primary key,
user_password char(64) not null,
user_name varchar2(100) not null,
user_email varchar2(255) not null unique,
user_tel varchar2(20) not null,
user_photo varchar2(100) default 'default.png',
user_deleted char(1) default 'N',
user_created_date default sysdate,
user_updated_date default sysdate
);
create table spring_user_roles (
user_id varchar2(100) not null references spring_users (user_id),
user_role_name varchar2(20) not null,
primary key (user_id, user_role_name)
);
VO 클래스
User.java, UserRole.java
mybatis mapper 인터페이스
UserMapper.java, UserRoleMapper.java
mybatis mapper 파일
users.xml, user-role.xml
* 사용자정보 추가, 사용자정보 조회관련 기능 정의.
// 아이디, 비밀번호, 이름을 표현하는 클래스
public class LoginUser {
String id;
String encryptPassword;
String name;
public LoginUser(String id, String encryptPassword, String name) {
this.id = id;
this.encryptPassword = encryptPassword;
this.password = password;
}
// Getter 메소드
}
// UserDetails 인터페이스를 구현한 클래스.
// username(사용자 식별정보), password(암호화된 비밀번호), authorities(보유권한 정보)와 기타 정보를 제공한다.
public class CustomUserDetails extends LoginUser implements UserDetails {
private Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(String id, String encryptPassword, String name, Collection<? extends GrantedAuthority> authorities) {
super(id, encryptPassword, name);
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return getEncryptPassword();
}
@Override
public String getUsername() {
return getId();
}
그 외 UserDetails 인터페이스의 추상메소드들은 전부 true값을 반환하도록 재정의한다.
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
private static final String DELETED_USER_STATUS = "Y";
// 사용자정보와 사용자권한 정보를 조회하기 위해서 UserMapper와 UserRoleMapper를 주입받는다.
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
// 전달받은 사용자식별정보로 사용자정보를 조회해서 사용자정의 UserDetails 객체를 반환한다.
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
// 사용자아이디는 인증작업을 수행하는 AuthenticationProvider로부터 전달받은 것이다.
// 사용자아이디로 사용자 정보를 조회한다.
User user = userMapper.getUserById(userId);
// 사용자정보가 존재하지 않으면 예외를 던진다.
if (user == null) {
throw new UsernameNotFoundException("사용자 정보가 존재하지 않습니다.");
}
if ("Y".equals(user.getDeleted())) {
throw new UsernameNotFoundException("탈퇴한 사용자입니다.");
}
// 사용자의 권한정보를 조회한다.
List<UserRole> userRoles = userRoleMapper.getUserRolesByUserId(userId);
// 조회된 권한정보로 GrantedAuthority객체를 생성한다.
Collection<? extends GrantedAuthority> authorities = this.getAuthorities(userRoles);
return new CustomUserDetails(
user.getId(), // 사용자 아이디
user.getEncryptPassword(), // 암호화된 사용자 비밀번호
user.getName(), // 사용자이름
authorities); // 사용자가 보유한 권한정보
}
// 사용자 권한정보 목록을 전달받아서 GrantedAuthority객체의 집합으로 반환한다.
private Collection<? extends GrantedAuthority> getAuthorities(List<UserRole> userRoles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (UserRole userRole : userRoles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(userRole.getRoleName());
authorities.add(authority);
}
return authorities;
}
}
@SpringBootApplication
public class SpringbootSecurityApplication {
public static void main(String[] args) {
// 부트스트래핑 클래스를 로드해서 스프링 부트 어플리케이션을 실행시킨다.
SpringApplication.run(SpringbootSecurityApplication.class, args);
}
// 비밀번호를 암호화하는 비밀번호 인코드 객체를 스프링 컨테이너에 등록시킨다.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService; // 사용자식별정보로 사용자정보를 반환하는 기능을 가진 객체
@Autowired
private PasswordEncoder passwordEncoder; // 비밀번호 암호화 기능을 제공하는 객체
/*
* HTTP 요청에 대한 인증/인가 관련 설정정보를 정의한다. <br />
* 1. HTPP 요청에 대한 인가 정책(권한체크 정책)을 설정한다.
* 2. 인증 정책(로그인 정책)을 설정한다.
* 3. 로그아웃 정책을 설정한다.
*
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF 토큰 사용여부 설정
.csrf() // CSRF(사이트 간 요청위조) 방지를 위한 csrf토큰 사용여부 설정, CsrfConfigurer 객체를 반환한다.
.disable() // csrf 토큰 사용을 비활성화한다. (기본값은 csrf 토큰을 사용한다.)
// 1. 인가정책 설정 시작
.authorizeHttpRequests() // 모든 요청에 대해서 아래에 설정된 인가정책을 적용하도록 한다.
.antMatchers("/", "/register", "/registered", "/login").permitAll() // 제시된 요청은 접근을 허용한다.
.antMatchers("/post/**").hasAnyRole("GUEST", "USER")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // 위에서 제시된 요청 외에 모든 요청도 반드시 인증이 필요하다.
// 1. 인가정책 설정 종료
.and()
// 2. 인증정책 설정 시작
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("id")
.passwordParameter("password")
.defaultSuccessUrl("/")
.failureUrl("/login?error=fail")
// 2. 인증정책 설정 종료
.and()
// 3. 로그아웃정책 설정 시작
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
// 3. 로그아웃정책 설정 종료
.and()
// 4. 예외처리정책 설정 시작
.exceptionHandling()
.accessDeniedPage("/access-denied");
// 4. 예외처리정책 설정 종료
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/favicon.ico");
}
// 사용자정의 UserDetailsService 객체와 이 어플리케이션에서 사용하는 비밀번호 인코더를 AuthenticationManager에 등록시킨다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
}
@GetMapping("/register") // 위에서 /register는 permitAll 설정함.
public String registerform() {
return "register-form";
}
@PostMapping("/register")
public String register(UserRegisterForm userRegisterForm) {
userService.registerUser(userRegisterForm);
return "redirect:registered";
}
@Service
@Transactional
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private PasswordEncoder passwordEncoder;
public void registerUser(UserRegisterForm userRegisterForm) {
User savedUser = userMapper.getUserById(userRegisterForm.getId());
// 아이디, 이메일 중복여부 체크
if (savedUser != null) {
throw new ApplicationException("["+userRegisterForm.getId()+"] 사용할 수 없는 아이디입니다.");
}
savedUser = userMapper.getUserByEmail(userRegisterForm.getEmail());
if (savedUser != null) {
throw new ApplicationException("["+userRegisterForm.getEmail()+"] 사용할 수 없는 이메일입니다.");
}
// User객체를 생성하고, user객체에 사용자정보를 복사한다.
User user = new User();
BeanUtils.copyProperties(userRegisterForm, user);
// 비밀번호를 암호화한다.
user.setEncryptPassword(passwordEncoder.encode(userRegisterForm.getPassword()));
// 사용자정보를 저장한다.
userMapper.insertUser(user);
// 사용자 보유 권한 정보를 저장한다.
List<String> roleNames = userRegisterForm.getRoleName();
for (String roleName : roleNames) {
UserRole userRole = new UserRole(userRegisterForm.getId(), roleName);
userRoleMapper.insertUserRole(userRole);
}
}
@GetMapping("/login")
public String loginform() {
return "login-form";
}
// 로그아웃정책 설정 시작
.logout() // 로그아웃을 요청하는 URI는 "/logout"이다.
.logoutUrl("/logout")
.logoutSuccessUrl("/")
// 로그아웃정책 설정 종료
@AuthenticationPrincipal // SecurityContext에 저장된 Authentication 객체의 principal 정보를 가져오는 어노테이션이다.
@Retention(RUNTIME)
public @interface AuthenticatedUser {
}
예시
@GetMapping("/user/info")
public String info(@AuthenticatedUser LoginUser loginUser, Model model) {
User user = userService.getUserDetail(loginUser.getId());
model.addAttribute("user", user);
return "user/detail";
}
<!-- spring security tag library -->
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authorize />
사용자 인증 여부 정보를 제공한다.
사용자 보유 권한 정보를 체크한다.
<sec:authentication />
인증된 사용자 정보를 제공한다.