2022. 12. 22. 14:48ㆍ[Spring]_/[Eclipse]
개요
Intellij를 사용하다가 다음 프로젝트가 Eclipse 환경으로 인해
Eclipse 에 spring 최신버전을 설치하였고
SpringSecurity 에서 제공하는
handler 들을 커스텀화 하여 적용하는 방법 서울
1단계에서는 로그인 과정 핸들러를 구현합니다.
환경
Eclipse 최신버전 2022-09 (4.25.0)
Tomcat - 10.0.23 v
maven Artifact ID : maven-archetype-webapp 1.4v
Spring 6.0.2 ver
Spring Security 6.0.0 ver
Jakarta 5.0.0 ver
특이사항
이전 포스트 에서 바뀐점이 있습니다.
Tomcat 10 버전 사용시 필요한 servlet 버전은 5 입니다.
이전까지는 Tomcat 에 내장된 jakarta 를 사용하였고
이번에 maven build 에 해당 servlet 이 없을 경우 오류가 발생합니다.
다음 포스트 참고
https://yn971106.tistory.com/167
또한 본문에 springSecurty 6.0.0 버전에 맞는 DTD도 기술하였습니다.
해당 포스트는 하위 포스트의 후속내용입니다.
https://yn971106.tistory.com/163
1. security.context 수정
기존의 DTD 의 경우 이전버전으로 6.0.0 에 맞는 DTD 작성
DTD 의 경우 다음 링크 참고
코드 + 주석 설명
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
<!-- http 요청시 처리 로직 -->
<http request-matcher="ant" auto-config="true">
<!-- url intercept 설정 -->
<intercept-url pattern="/login/**" access="permitAll" />
<intercept-url pattern="/menu1" access="hasRole('ROLE_ADMIN')" />
<intercept-url pattern="/**" access="permitAll" />
<!-- 로그인 페이지 설정 -->
<form-login login-page="/login/login"
username-parameter="username" password-parameter="password"
login-processing-url="/loginProcessing"
authentication-success-handler-ref="loginSuccessHandler"
authentication-failure-handler-ref="loginFailureHandler" />
<!-- 엑세스 거부시 설정 -->
<access-denied-handler ref="accessDeniedHandler" />
<!-- 로그아웃 설정 페이지 아직 미구현 -->
<logout logout-url="/logout" logout-success-url="/login/login"
invalidate-session="true" />
</http>
<!-- 엑세스 거부 프로세스 객체 (빈) 등록 -->
<!-- 구현체의 defaultDeniedUrl 에 value 의 값을 대입 -->
<b:bean id="accessDeniedHandler"
class="com.yoon.loginHelp.security.CustomAccessDeniedHandler">
<b:property name="defaultDeniedUrl" value="/login/login"></b:property>
</b:bean>
<!-- 로그인 성공시 작동할 객체(빈) 등록-->
<b:bean id="loginSuccessHandler"
class="com.yoon.loginHelp.security.LoginSuccessHandler">
</b:bean>
<!-- 로그인 실패시 작동할 객체(빈) 등록-->
<!-- 해당 클래스의(구현체의) defaultFailureUrl 에 value 값 대입-->
<b:bean id="loginFailureHandler"
class="com.yoon.loginHelp.security.LoginFailureHandler">
<b:property name="defaultFailureUrl" value="/login/error" />
</b:bean>
<!-- 로그인 프로세스-->
<!-- 구현체에 기존 security 에서 작동하는 메소드 오버라이드 하여 커스텀화 가능-->
<authentication-manager>
<authentication-provider
ref="customAuthenticationProvider" /> <!-- 로그인 관련 로직 구현 -->
</authentication-manager>
<!-- 로그인 구현체에 등록한 객체의 빈 등록 -->
<!-- 로그인 관련 로직 구현 -->
<b:bean id="customAuthenticationProvider"
class="com.yoon.loginHelp.security.CustomAuthenticationProvider" />
</b:beans>
등록해야 하는 빈 객체
- loginSuccessHandler : 로그인 성공시 핸들러
- loginFailureHandler : 로그인 실패시 핸들러
- accessDeniedHandler : 접근 거부 ( 예시 permitall 이 아닌 곳 직접 url 치고 들어올 경우)
- CustomAuthenticationProvider : 로그인 하는 과정 직접 오버라이드 하여 구현 < NOW
2 -1 CustomAuthenticationProvider.java작성
<!-- 로그인 프로세스-->
<!-- 구현체에 기존 security 에서 작동하는 메소드 오버라이드 하여 커스텀화 가능-->
<authentication-manager>
<authentication-provider
ref="customAuthenticationProvider" /> <!-- 로그인 관련 로직 구현 -->
</authentication-manager>
<!-- 로그인 구현체에 등록한 객체의 빈 등록 -->
<!-- 로그인 관련 로직 구현 -->
<b:bean id="customAuthenticationProvider"
class="com.yoon.loginHelp.security.CustomAuthenticationProvider" />
securityContext 에 작성한 내용으로
class 경로와 똑같은 위치에
CustomAuthenticationProvider.java 를 생성합니다.
CustomAuthenticationProvider.java 소스코드
package com.yoon.loginHelp.security;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.yoon.user.service.UserService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
public class CustomAuthenticationProvider implements AuthenticationProvider{
final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private UserService userService;
@Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
System.out.println("고객번호/비밀번호. {}/{}" +username +":"+password);
Map<String,String> param = new HashMap<>();
param.put("username", username);
param.put("password", password);
try {
List<Map<String,String>> userInfo = userService.getUserInfo(param);
if(userInfo.get(0) != null) {
System.out.println("success login");
System.out.println(userInfo.get(0).toString());
List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");
System.out.println(roles.toString());
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(username,password,roles);
System.out.println(result.toString());
result.setDetails(new CustomUserDetails(userInfo.get(0),roles));
System.out.println(result);
return result;
}else {
throw new BadCredentialsException("미가입 사용자");
}
}catch (Exception e) {
throw new BadCredentialsException(e.toString(), e);
}
}
}
AuthenticationProvider 를 implements 로 상속받습니다.
AuthenticationProvider 에 구현된 2가지 메소드를 오버라이드 한 뒤
authenticate 메소드에 코드를 작성합니다.
Authentication 객체는 spring Security 에서 인증정보의 객체입니다.
해당 인터페이스의
getPrincipal()
getCredentials()
를 이용하여 로그인한 username 과 password 를 받습니다.
이를 Map 형식을 바꾸어 service 에 param을 전달합니다.
List<Map<String,String>> userInfo = userService.getUserInfo(param);
2- 2 DB 조회 로직 (Mapper 가 연동되어 있어야 합니다. ( 이전포스팅 참고 )
service 는 다음과 같습니다.
package com.yoon.user.service;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
public interface UserService {
public List<Map<String, String>> getUserInfo(Map<String, String> mapData) throws SQLException;
}
serviceImpl 은 다음과 같습니다.
package com.yoon.user.service.impl;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.yoon.mapper.UserMapper;
import com.yoon.user.service.UserService;
import jakarta.annotation.Resource;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public List<Map<String, String>> getUserInfo(Map<String,String>mapData) throws SQLException {
// TODO Auto-generated method stub
return userMapper.getUserInfo(mapData);
}
}
mapper interface 는 다음과 같습니다.
package com.yoon.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
public List<Map<String, String>> getUserInfo(Map<String, String> mapData);
}
UserMapper.xml 은 다음과 같습니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yoon.mapper.UserMapper">
<select id="getUserInfo" resultType="HashMap">
select
*
from user_info_manager
where
USER_ID = #{username}
AND USER_PW = #{password}
</select>
</mapper>
2-3 다시 CustomAuthenticationProvider.java 로 돌아와서
try 내부 로직은 원하시는대로 작성할 수 있습니다.
만약 컬럼중 admin 혹은 user를 구분하는 값이 있다면
그 컬럼의 유무를 기점으로 roles 을 지정하여
UsernamePasswordAuthenticationToken 를 생성 할 수 있습니다.
저는 예시로 모든 값에 ROLE_ADMIN 이라는 역활을 부여하였습니다.
List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN");
이를 반환할 객체에 담고
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(username,password,roles);
여기 까지는 username, password , role 까지 토큰에 저장되고.
만약 추가적인 정보를 저장해서 전달하고 싶을 경우
setDetails() 를 활용합니다.
result.setDetails(new CustomUserDetails(userInfo.get(0),roles));
2- 4 여기서 CusomerUserDetails 형식의 모델 객체로 만들어서 저장하게 됩니다.
해당 클래스는 다음과 같습니다.
CusomerUserDetails .java 소스코드
package com.yoon.loginHelp.security;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.sound.midi.Soundbank;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import jakarta.annotation.Resource;
public class CustomUserDetails extends User {
/**
*
*/
private static final long serialVersionUID = 1593214568512L; // ?
private String user_id;
private String user_pw;
private String user_nm;
private String user_phone;
private String user_typ;
public CustomUserDetails(String username, String password,Collection<? extends GrantedAuthority> authorities) {
super(username, password,authorities);
// TODO Auto-generated constructor stub
};
public CustomUserDetails(Map<String, String> result ,Collection<? extends GrantedAuthority> authorities ) {
super(result.get("USER_ID"),result.get("USER_PW"),authorities);
System.out.println("tttt");
System.out.println(result);
this.setUser_id(result.get("USER_ID").toString());
this.setUser_pw(result.get("USER_PW").toString());
this.setUser_nm(result.get("USER_NM").toString());
this.setUser_phone(result.get("USER_PHONE").toString());
this.setUser_typ(result.get("USER_TYP").toString());
}
public String getUser_id() {
return user_id;
}
public void setUser_id(String user_id) {
this.user_id = user_id;
}
public String getUser_pw() {
return user_pw;
}
public void setUser_pw(String user_pw) {
this.user_pw = user_pw;
}
public String getUser_nm() {
return user_nm;
}
public void setUser_nm(String user_nm) {
this.user_nm = user_nm;
}
public String getUser_phone() {
return user_phone;
}
public void setUser_phone(String user_phone) {
this.user_phone = user_phone;
}
public String getUser_typ() {
return user_typ;
}
public void setUser_typ(String user_typ) {
this.user_typ = user_typ;
}
}
해당 클래스는 User 를 상속받아서 구현하였습니다.
Getter 와 Setter 가 구현되어 있습니다.
해당 모델의 생성자는 다음과 같습니다.
public CustomUserDetails(Map<String, String> result ,Collection<? extends GrantedAuthority> authorities ) {
super(result.get("USER_ID"),result.get("USER_PW"),authorities);
System.out.println("tttt");
System.out.println(result);
this.setUser_id(result.get("USER_ID").toString());
this.setUser_pw(result.get("USER_PW").toString());
this.setUser_nm(result.get("USER_NM").toString());
this.setUser_phone(result.get("USER_PHONE").toString());
this.setUser_typ(result.get("USER_TYP").toString());
}
여기서 map 형식의 result 와 Authority 의 콜렉션을 받도록 하였습니다.
이와 같이
UsernamePasswordAuthenticationToken 의 detail 항목에는 모델의 객체가 들어가 됩니다.
정리
만약 Mapper 에서 ( 유저가 있는지 확인하는 쿼리 ) 값이 있어
UsernamePasswordAuthenticationToken 가 리턴 된 경우 -> loginSuccessHandler 호출
만약 Mapper 에서 ( 유저가 있는지 확인하는 쿼리 ) 값이 없어
BadCredentialsException 가 일어날 경우 -> loginFailureHandler 호출
하게 됩니다.
3 Front (jsp)
login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link type="text/css" rel="stylesheet" href="/static/assets/css/login.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://kit.fontawesome.com/53a8c415f1.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="wrap">
<div class="login">
<h2>Log-in</h2>
<div class="login_sns">
<li><a href=""><i class="fab fa-instagram"></i></a></li>
<li><a href=""><i class="fab fa-facebook-f"></i></a></li>
<li><a href=""><i class="fab fa-twitter"></i></a></li>
</div>
<form action="/loginProcessing" method="POST">
<div class="login_id">
<h4>E-mail</h4>
<input type="text" name="username" id="username" />
</div>
<div class="login_pw">
<h4>Password</h4>
<input type="password" name="password" id="password" />
</div>
<div class="submit">
<input type="submit" value="로그인"/>
</div>
</form>
</div>
</div>
<script>
</script>
중요한 것은 이부분입니다.
<form action="/loginProcessing" method="POST">
폼태그 action 을
login-processing-url 과 동일하게 해야 springSecurity 가 인식하게 됩니다.
결과
감사합니다.
'[Spring]_ > [Eclipse]' 카테고리의 다른 글
[Eclipse]_Spring Security Handler 구현 3단계 (로그인 실패 시) (0) | 2022.12.20 |
---|---|
[Spring]_Oracle 연결 & Mapper 연동 ( Error 정리) (0) | 2022.12.15 |
[eclipse]_spring_security 설치 (0) | 2022.12.12 |
[eclipse]_Spring 설치 및 구현 (0) | 2022.12.09 |
[Eclipse]_Spring 최신 버전 servlet 오류(feat.jakarta error) (0) | 2022.12.09 |