https://github.com/s2ljeun/nikee/tree/main/src/main/java/com/nikeedev/nikee/security
회원가입 - 비밀번호를 암호화해서 DB에 저장
로그인 - 사용자가 입력한 비밀번호를 암호화해서 DB의 비밀번호와 대조
로그인 실패 - ID와 PW가 DB에 저장된 내용과 틀리면 에러메시지를 표시
로그아웃
login 패키지 안에 SecurityConfig , LoginPwValidator, LoginSuccess/FailHandler 클래스를 구현. 여기에 로그인할 때 LoginController, 회원가입할 때 MemberController를 사용한다.
*처음 시큐리티를 적용하며 패키지명을 login이라고 했는데 config, security 등의 패키지명이 더 적절해보인다.
@SuppressWarnings("deprecation")
@Configuration // 스프링 환경 세팅을 돕는 어노테이션
@EnableWebSecurity // 스프링 시큐리티 설정할 클래스라고 알려주는 어노테이션
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginIdPwValidator loginIdPwValidator;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // POST방식 허용
.authorizeRequests()
.antMatchers("/", "/index", "/login", "/join").permitAll() // 이 URI는 누구든 접근가능
.antMatchers("/admin/**").hasRole("ADMIN") // ADMIN role만 접근 가능
.antMatchers("/member/**").hasRole("MEMBER") // ADMIN role만 접근 가능
.anyRequest().authenticated() // 어떤 URI로 접근하던 인증이 필요함
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/loginProc") // 이 URI 호출시 스프링 시큐리티로 폼 정보를 제출 / form의 action
.usernameParameter("id") // 폼 input name값: default - username
.passwordParameter("passwd") // 폼 input name값: default - password
.successHandler(loginSuccessHandler()) // 로그인 성공을 다룰 핸들러
.failureHandler(loginFailHandler()) // 로그인 실패를 다룰 핸들러
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/") // 로그아웃 성공시 이동할 URL
.logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc")); // 이 URI 호출시 로그아웃
}
//인증 예외 추가
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/js/**", "/resources/css/**", "/resources/img/**", "/resources/icon/**");
}
//입력한 ID/PW가 DB와 일치하는지 확인
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginIdPwValidator);
}
//로그인 성공 핸들러
@Bean
public LoginSuccessHandler loginSuccessHandler(){
return new LoginSuccessHandler();
}
//로그인 실패 핸들러
@Bean
public LoginFailHandler loginFailHandler(){
return new LoginFailHandler();
}
}
WebSecurityConfigurerAdapter를 상속받아 기본적인 security 설정을 한다.
form로그인의 post방식 제출을 허용하기 위해 .csrf().disable()을 해주고,
.authorizeRequests().antMatchers()로 각 URL에 접근가능한 권한을 설정해준다.
.formLogin()으로 폼 방식 로그인을 활성화하고, .loginPage()로 커스텀 로그인 페이지 매핑을 지정해준다. 그 외 form과 관련된 설정을 해준다.
로그인 성공, 실패를 다룰 핸들러를 .successHandler()와 .failureHandler()로 지정해주는데, 각 인자는 하단에서 Bean으로 주입하고 있다.
또한 configure 이름의 메소드를 override해서 유저가 입력한 정보와 DB정보가 일치하는지 확인하기 위한 loginIdPwValidator 클래스를 연결해준다.
@Service
public class LoginIdPwValidator implements UserDetailsService {
@Autowired
private MemberMapper memberMapper;
// DB의 pw(암호화된)와 유저가 입력한 pw를 암호화하여 자동으로 비교
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public UserDetails loadUserByUsername(String insertedId) throws UsernameNotFoundException {
// 사용자가 입력한 id가 인자로 들어옴 -> 유저id로 DTO꺼내오기
MemberDTO mdto = memberMapper.getMemberById(insertedId);
if (mdto == null) {
return null; // ID 혹은 PW가 잘못되었습니다.
}
String passwd = mdto.getMem_passwd();
String roles = mdto.getMem_role();
return User.builder()
.username(insertedId)
.password(passwd)
.roles(roles)
.build();
}
}
Bean으로 PasswordEncoder를 주입해주고 memberMapper를 autowired한다. loadUserByUsername메소드를 Override해 유저가 입력한 id로 dto가 존재하는지 판단하고 UserDetails 객체를 return한다. 패스워드가 일치하는지 여부는 PasswordEncoder가 자동으로 판단해준다.
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res,
Authentication auth) throws IOException, ServletException {
//로그인 성공시 기본 페이지 설정
String url = "/index";
//인증된 사용자의 user객체 추출
User user = (User) auth.getPrincipal();
//user객체에서 role 목록 추출
Collection<GrantedAuthority> authlist = user.getAuthorities();
Iterator<GrantedAuthority> authlist_it= authlist.iterator();
while(authlist_it.hasNext()) {
GrantedAuthority authority= authlist_it.next();
//설정되어 있는 권한 중 ROLE_ADMIN이 있다면
if(authority.getAuthority().equals("ROLE_ADMIN")) {
url="/admin";
}
}
res.sendRedirect(url);
req.getSession().setAttribute("userDetail", user);
}
}
로그인 성공 후 처리는 SimpleUrlAuthenticationSuccessHandler를 상속받아 만든다.
로그인 성공한(인증된) 사용자의 user객체를 auth.getPrincipal()로 추출해오고, 그 안에서 권한(USER_ROLE)을 확인하기 위해 authlist를 불러온다. 권한 중 "ADMIN"이 존재한다면 url을 어드민페이지로 매핑, 그 외에는 index페이지로 매핑한다.
*여기서 권한은 가입할 때 DB에 ADMIN, MEMBER 등으로 넣어주어야 한다.
*세션에 "userDetail"이라는 이름으로 user객체를 저장해 추후 로그인 한 상태인지 판단하는 데 사용한다. 로그아웃을 진행하면 세션에서 자동으로 삭제되는 것을 확인했다.
public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
String url = "/login?error";
res.sendRedirect(url);
}
}
@Controller
public class LoginController {
@GetMapping("/login")
public String login(HttpServletRequest req,
@RequestParam(value="error", required=false) String error) {
if(error != null) {
req.setAttribute("errorMsg", "아이디 또는 패스워드를 확인해주세요.");
}
return "login";
}
}
로그인 실패 후 처리는 SimpleUrlAuthenticationFailureHandler를 상속받아 만든다.
url만 지정하고 redirect 시켜주었다.
컨트롤러에서 "/login" 매핑을 받을 때 "error" 이름의 파라메터가 존재한다면 "아이디 또는 패스워드를 확인해주세요." 문구를 띄우도록 처리했다.
@Controller
public class MemberController {
@Autowired
private MemberMapper memberMapper;
@Autowired
private LoginIdPwValidator loginIdPwValidator;
@GetMapping("/join")
public String goJoin() {
return "join";
}
@PostMapping("/join")
public String joinOk(HttpServletRequest req, @ModelAttribute MemberDTO mdto) {
//패스워드 암호화
String encodedPasswd = loginIdPwValidator.passwordEncoder().encode(mdto.getMem_passwd());
mdto.setMem_passwd(encodedPasswd);
int res = memberMapper.insertMember(mdto);
if(res > 0) {
req.setAttribute("msg", "회원가입이 완료되었습니다.");
req.setAttribute("url", "/index");
}else {
req.setAttribute("msg", "회원가입에 실패했습니다.");
req.setAttribute("url", "/join");
}
return "message";
}
}
BCryptPasswordEncoder()가 사용자가 입력한 비밀번호와 DB의 비밀번호를 비교하려면 DB에 있는 비밀번호부터 암호화처리가 되어있어야 한다. 따라서 회원가입할 때 loginIdPwValidator의 passwordEncoder() 메소드를 사용해 DB에 저장한다. (여기서는 굳이 메소드를 불러오지 않고 new BCryptPasswordEncoder()를 생성해 처리해줘도 될 것 같다.)