* 시큐리티 인증 절차
* 시큐리티 흐름
* 로그인 과정
로그인 폼 요청 경로는 따로,
로그인 처리는 한 곳에서 ("/login")
0. 로그인폼
- action="/login"
- 히든필드에 userType 담아서 보내기
- error=fail 일 때 문구 넣기
* user의 login-form
* employee의 login-form
1. security.vo
* LoginUser 객체
package com.example.security.vo;
import com.example.vo.User;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoginUser {
private String id;
private String nickname;
private String providerType;
private String email;
public LoginUser() {}
public LoginUser(User user) {
this.id = user.getId();
this.nickname = user.getName();
this.providerType = user.getProviderType();
this.email = user.getEmail();
}
}
* CustomUserDetails 객체
- CustomUserDetailsService 객체에서 로그인한 사용자 아이디로 사용자 정보를 조회하고, 그 정보를 담아서 반환하는 객체
package com.example.security.vo;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails extends LoginUser implements UserDetails{
private static final long serialVersionUID = -692518506074118282L;
// db에서 조회한 비밀번호
private final String encryptPassword;
private final Collection<? extends GrantedAuthority> authorities;
// 로그인한 id로 사용자/직원 정보를 조회해서 UserDetails 객체에 로그인한 사용자의 아이디, 비밀번호, 이름, 권한정보를 저장
public CustomUserDetails(String id, String encryptPassword, String name, Collection<? extends GrantedAuthority> authorities) {
setId(id);
setName(name);
this.encryptPassword = encryptPassword;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return encryptPassword;
}
@Override
public String getUsername() {
return getId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2. security.service
- CustomUserDetailsService와 CustomEmployeeDetailsService로 구분!!
* CustomUserDetailsService (사용자 DB 조회)
- User의 Role은 "ROLE_USER" 1개
package com.example.security.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.example.mapper.UserMapper;
import com.example.mapper.UserRoleMapper;
import com.example.security.vo.CustomUserDetails;
import com.example.vo.User;
import com.example.vo.UserRole;
@Service
public class CustomUserDetailsService implements UserDetailsService{
@Autowired
UserMapper userMapper;
@Autowired
UserRoleMapper userRoleMapper;
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
User user = userMapper.getUserById(userId);
if(user == null) {
throw new UsernameNotFoundException("사용자 정보가 존재하지 않습니다.");
}
if("Y".equals(user.getDeleted())) {
throw new UsernameNotFoundException("탈퇴한 사용자입니다.");
}
UserRole userRole = userRoleMapper.getUserRoleByNo(user.getNo());
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(userRole.getRoleName()));
return new CustomUserDetails(
user.getId(),
user.getEncryptPassword(),
user.getName(),
authorities);
}
}
* CustomEmployeeDetailsServices (관리자 DB 조회)
- Employee의 권한은 "ROLE_EMP", "ROLE_ADMIN"
package com.example.security.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.example.mapper.EmployeeMapper;
import com.example.mapper.EmployeeRoleMapper;
import com.example.security.vo.CustomUserDetails;
import com.example.vo.Employee;
import com.example.vo.EmployeeRole;
@Service
public class CustomEmployeeDetailsService implements UserDetailsService{
@Autowired
EmployeeMapper employeeMapper;
@Autowired
EmployeeRoleMapper employeeRoleMapper;
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
// 로그인하면서 전달받은 직원 아이디로 직원 정보 조회
Employee employee = employeeMapper.getEmployeeById(userId);
if(employee == null) {
throw new UsernameNotFoundException("직원 정보가 존재하지 않습니다.");
}
// 직원의 권한정보 조회
List<EmployeeRole> empRoles = employeeRoleMapper.getEmployeeRolesByEmployeeId(employee.getId());
// 직원의 권한정보 목록을 전달받아서 GrantedAutority 객체의 집합으로 반환
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for(EmployeeRole empRole : empRoles) {
authorities.add(new SimpleGrantedAuthority(empRole.getRoleName()));
}
return new CustomUserDetails(
employee.getId(),
employee.getPassword(),
employee.getName(),
authorities);
}
}
3. security
- 로그인 단계에서 userType이 추가됐기 때문에 원래 시큐리티를 상속받아서 Custom으로 다시 만들어줌
* CustomAuthenticationToken
package com.example.security;
import java.util.Collection;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
public class CustomAuthenticationToken extends UsernamePasswordAuthenticationToken{
private static final long serialVersionUID = 893311451286366129L;
private String userType;
public CustomAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public CustomAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
}
* CustomAuthenticationFilter
package com.example.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if(!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String id = request.getParameter("id");
String password = request.getParameter("password");
String userType = request.getParameter("userType");
System.out.println("id: " + id);
System.out.println("password: " + password);
System.out.println("userType: " + userType);
CustomAuthenticationToken authenticationToken = new CustomAuthenticationToken(id, password);
authenticationToken.setUserType(userType);
return this.getAuthenticationManager().authenticate(authenticationToken);
}
}
* CustomAuthenticationProvider
- 실질적인 인증 처리 담당
- map 객체와 비밀번호 인코더 2가지를 전달받음
- 로그인 전의 Authentication 객체에 아이디, 비번 등의 정보를 담고
로그인 후 Authentication 객체를 새로 만들어서 거기에 userType담아서 Authentication 객체 반환
- CustomAuthenticationToken은 Authentication 인터페이스를 구현하고 있는 구현객체 !!! 그래서 Authentication 타입으로 캐스팅 할 수 있음
package com.example.security;
import java.util.Map;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
// 실제 인증 처리 (인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환)
// 사용자 타입이 추가됐기 때문에 Custom 정의
public class CustomAuthenticationProvider implements AuthenticationProvider {
// detailsServiceMap을 SecurityConfig로부터 전달받음 -> {"user", CustomUserDetailsService, "employee", CustomEmployeeDetailsService}
private Map<String, UserDetailsService> detailsServiceMap;
private PasswordEncoder passwordEncoder;
public CustomAuthenticationProvider(Map<String, UserDetailsService> detailsServiceMap, PasswordEncoder passwordEncoder) {
this.detailsServiceMap = detailsServiceMap;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 로그인 전의 Authentication 객체에 로그인 정보 담기
CustomAuthenticationToken customAuthentication = (CustomAuthenticationToken) authentication;
// 로그인 창에서 입력하는 아이디, 비밀번호, 유저타입(히든필드) 저장
String username = customAuthentication.getName();
String password = customAuthentication.getCredentials().toString();
String userType = customAuthentication.getUserType();
// userType에 해당하는 CustomXXXDetailsService 객체를 Map 객체에서 꺼낸다.
UserDetailsService detailsService = detailsServiceMap.get(userType);
// userType에 해당하는 CustomXXXDetailsService 객체를 이용해서 인증에 필요한 사용자정보(UserDetails를 구현한 CustomUserDetails 객체)를 획득한다.
UserDetails userDetails = detailsService.loadUserByUsername(username);
// 비밀번호를 비교해서 사용자 인증을 수행한다.
if(!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
}
// 인증이 완료되면 새로운 Authentication 객체 생성해서 사용자정보, 비밀번호, 권한정보를 담는다
customAuthentication = new CustomAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
// 사용자 타입을 담는다
customAuthentication.setUserType(userType);
// 반환된 Authentication 객체는 SecurityContext 에 저장되고, SecurityContext 는 HttpSession에 저장된다.
return customAuthentication;
}
@Override
public boolean supports(Class<?> authentication) {
return CustomAuthenticationToken.class.isAssignableFrom(authentication);
}
}
* SecurityConfig
- 커스텀한 필터 추가
- 커스텀한 provider를 빈에 등록
- 로그인 성공 후 이동할 경로 각자 설정 (사용자 홈 / 관리자 홈)
package com.example.security;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationProvider;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import com.example.security.service.CustomEmployeeDetailsService;
import com.example.security.service.CustomUserDetailsService;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
CustomUserDetailsService userDetailsService;
@Autowired
CustomEmployeeDetailsService employeeDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// 인가정책 설정
.authorizeRequests()
// 다음 페이지에 모두 접근 가능
.antMatchers("/").permitAll()
.antMatchers("/user/register", "/user/registered").permitAll()
.antMatchers("/user/login", "/emp/login").permitAll()
// 인증된 사람만 다음 기능 가능
.antMatchers("/logout").authenticated()
// 특정 권한을 가진 사람만 다음 페이지 접근 가능
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/emp/**").hasAnyRole("EMP", "ADMIN")
.antMatchers("/admin/**").hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and()
// 인증정책 설정
.formLogin() // 인증방식은 폼인증 방식 사용
.loginProcessingUrl("/login") // 로그인 처리를 요청하는 URI
.and()
// 로그아웃 정책 설정
.logout()
.logoutUrl("/logout") // 로그아웃 처리를 요청하는 URI
.logoutSuccessUrl("/") // 사용자 로그아웃 성공 시 재요청할 URI
.and()
.addFilter(authenticationFilter())
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.headers().frameOptions().disable();
}
// 보안정책을 적용하지 않을 URI 설정 (이미지, 스타일시트, 자바스크립트 소스와 같은 정적 콘텐츠는 인증/인가 작업을 수행하지 않도록 설정)
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/favicon.ico");
}
// 사용자정의 UserDetailsService 객체(customUserDetailsService)와 비밀번호 암호화객체를 AuthenticationManagerBuilder에 등록
// -> 기본제공하는거 말고 얘네를 써라
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendRedirect("/access-denied");
}
};
}
@Bean
public CustomAuthenticationFilter authenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
filter.setAuthenticationFailureHandler(authenticationFailureHandler());
return filter;
}
@Bean
public AuthenticationProvider authenticationProvider() {
// 여기서 map에 담아서 CustomAuthenticationProvider에서 값 꺼내서 사용
Map<String, UserDetailsService> map = new HashMap<>();
map.put("사용자", userDetailsService);
map.put("관리자", employeeDetailsService);
return new CustomAuthenticationProvider(map, passwordEncoder);
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
CustomAuthenticationToken customAuthenticationToken = (CustomAuthenticationToken) authentication;
String userType = customAuthenticationToken.getUserType();
if("사용자".equals(userType)) {
response.sendRedirect("/");
} else if("관리자".equals(userType)) {
response.sendRedirect("/emp/home");
}
}
};
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String userType = request.getParameter("userType");
if("사용자".equals(userType)) {
response.sendRedirect("/user/login?error=fail");
} else if("관리자".equals(userType)) {
response.sendRedirect("/emp/login?error=fail");
}
}
};
}
}
'수업내용 > 프로젝트' 카테고리의 다른 글
[Spring] String joiner 요일 리스트를 테이블 한 칸에 표시하기 (0) | 2023.02.17 |
---|---|
[Spring/Security] .csrf().disable() (0) | 2023.02.15 |
[Spring] 반환경로가 redirect일 때, Model 객체에 담은 값을 View로 전달하는 법 (0) | 2023.02.10 |
[Spring boot] 프로필 사진 업로드, 미리보기 (0) | 2023.02.07 |
프로젝트 참고 (달력, 차트, 지도, 우편번호API) (0) | 2023.01.13 |