방법
방법1) 직접 UserDetailsService를 구현한다.
방법2) 직접 AuthenticationProvider를 구현한다.
이 게시글에서는 UserDetailsService를 구현하는 방법에 대해 다룬다.
//새 글
//@RequestMapping(value="/noticeReg.htm", method=RequestMethod.POST)
@PostMapping("/noticeReg.htm")
public String noticeReg(
NoticeVO notice
, HttpServletRequest request
, Principal principal//인증받은 로그인한 정보를 얻어오는 인터페이스 Principal
) throws Exception{
//책 pg 358 커맨드 객체 NoticeVO notice 사용
//1. 첨부파일 유무확인
CommonsMultipartFile multipartFile = notice.getFile();
//2. 첨부파일 저장
String uploadRealPath = null;
if(!multipartFile.isEmpty()) {
//HttpServletRequest request 매개변수 위에 추가해준다.
uploadRealPath = request.getServletContext().getRealPath("/customer/upload"); //upload 폴더에 저장
File saveDir = new File(uploadRealPath);
if(!saveDir.exists())saveDir.mkdirs(); //저장하고자 하는 경로가 없다면 새로 폴더를 만들겠다는 뜻
System.out.println("uploadRealPath:" + uploadRealPath);
String originalFilename = multipartFile.getOriginalFilename(); //서버에 올릴 때 원래 파일명
String filesystemName = getFileNameCheck(uploadRealPath, originalFilename); //getFileNameCheck()함수
//filesystemName은 물리적 이름(실제 업로드 되는 이름), originalFilename은 원래 사용자가 올릴 떄의 이름
File dest = new File(uploadRealPath, filesystemName);
multipartFile.transferTo(dest); //실제 서버에 파일이 업로드 된다.
notice.setFilesrc(filesystemName); //DB에 물리적인 이름으로 저장되도록 함
}//if
notice.setWriter(principal.getName());
@Data
public class AuthVO {
private String username;
private String authority;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberVO {
//member 테이블의 컬럼과 이름을 일치
//fields
private String id;
private String pwd;
private String name;
private String gender;
private String birth;
private String is_lunar;
private String cphone;
private String email;
private String habit;
private Date regdate;
//트랜젝션 처리 테스트 위해 point 컬럼 추가 -> 필드 추가, getter setter 추가
private int point;
private boolean enabled;
private List<AuthVO> authList; //회원이 어떤 권한을 가졌는지 list
}
public interface MemberMapper {
//회원정보 얻어오는 메서드
public MemberVO getMember(String id) throws ClassNotFoundException, SQLException;
//회원가입 메서드
public int insert(MemberVO member) throws ClassNotFoundException, SQLException;
//회원정보 + 권한정보 얻어오는 메서드
public MemberVO read(String userid);
}
resultMap 사용하기
- Map 형태로 데이터를 collection 해 담으면 편리하다.
- ★ResultMap을 사용해서 인증 정보를 한 번에 Map에 담는다.★
<resultMap type="org.doit.ik.domain.MemberVO" id="memberMap">
<id property="id" column="id" />
<result property="id" column="id" />
<result property="pwd" column="pwd" />
<result property="name" column="name" />
<result property="regdate" column="regdate" />
<collection property="authList" resultMap="authMap"></collection>
</resultMap>
<resultMap type="org.doit.ik.domain.AuthVO" id="authMap">
<result property="username" column="username" />
<result property="authority" column="authority" />
</resultMap>
<select id="read" resultMap="memberMap">
SELECT id, pwd, name, enabled, regdate, authority
FROM member m LEFT JOIN member_authorities auth ON m.id = auth.username
WHERE m.id = #{userid}
</select>
package org.doit.ik.security;
@Component("customUserDetailsService")
@Log4j
public class CustomUserDetailsService implements UserDetailsService{
@Setter(onMethod = @__({@Autowired}))
private MemberMapper memberMapper; //Mapper객체 얻어옴
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.warn("Load user by Username:" + username);
//VO 객체에는 회원정보 + 권한정보(authList)가 들어있다.
MemberVO vo = this.memberMapper.read(username);
log.warn("Queried by Member mapper:" + vo);
//리턴타입이 UserDetails 이기 때문에 VO 객체를 바로 넣을 수는 없다.
//User 인터페이스를 구현한 CustomUser를 만들어 변환해 넣어주자.
return vo == null? null : new CustomUser(vo);
//User라는 인터페이스를 상속받은 CustomUser 안에 vo를 넣어서 return 해 줌
//이걸 UserDetails로 return 하면 자동으로 업캐스팅 된다.
}
}
package org.doit.ik.security.damin;
//VO 객체를 담기 위해 만든 클래스이기 때문에 User를 extend함, 이것이 principal 객체와 같다고 생각할 수 있다.
@Getter
public class CustomUser extends User {
private static final long serialVersionUID = 8215844917794450806L;
private MemberVO member; // 회원+권한 정보 = principal.member
public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
//VO 객체: 회원정보 + 권한정보(authList)를 가짐
public CustomUser(MemberVO vo) {
super( //여기서 부모클래스는 User이다
vo.getId()
, vo.getPwd()
// 권한 정보 읽어와서 위의 private MemberVO member;에 넣어준다.
, vo.getAuthList().stream().map( auth->new SimpleGrantedAuthority( auth.getAuthority() ) ).collect( Collectors.toList() )
//원래 들어있는 것은 List<AuthVO>타입이었는데 이를 Collection<> 타입으로 바꾸기 위한 코드
//위의 생성자에서 Collection 타입을 매개변수로 달라고 했기에 변환해주는 것이다.
);
this.member = vo;
}
}
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<!-- JDBC 연결
<security:jdbc-user-service data-source-ref="dataSource"
users-by-username-query="SELECT id AS username ,pwd AS password ,enabled FROM MEMBER WHERE id= ? "
authorities-by-username-query="SELECT username AS userid, authority AS auth FROM member_authorities WHERE username = ? "
/> -->
<security:password-encoder ref="bCryptPasswordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<div>
<ol>
<li>principal : <sec:authentication property="principal"/></li>
<!-- CustomUser의 member 필드 -->
<li>MemberVO : <sec:authentication property="principal.member"/></li>
<li>사용자 ID : <sec:authentication property="principal.member.id"/></li>
<li>사용자 이름 : <sec:authentication property="principal.member.name"/></li>
<li>사용자 권한 목록 : <sec:authentication property="principal.member.authList"/></li>
</ol>
</div>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<div id="content">
<h2>공지사항</h2>
<h3 class="hidden">방문페이지위치</h3>
<ul id="breadscrumb" class="block_hlist">
<li>HOME</li>
<li>
고객센터
</li>
<li>
공지사항등록
</li>
</ul>
<form action="/customer/noticeReg.htm?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
<div id="notice-article-detail" class="article-detail margin-large" >
<dl class="article-detail-row">
<dt class="article-detail-title">
제목
</dt>
<dd class="article-detail-data">
<input name="title"/>
</dd>
</dl>
<dl class="article-detail-row">
<dt class="article-detail-title">
첨부파일
</dt>
<dd class="article-detail-data">
<input type="file" id="txtFile" name="file"/>
</dd>
</dl>
<div class="article-content" >
<textarea id="txtContent" class="txtContent" name="content"></textarea>
</div>
</div>
<p class="article-comment margin-small">
<input class="btn-save button" type="submit" value="저장" />
<a class="btn-cancel button" href="notice.htm">취소</a>
</p>
<!-- <input type="hidden" name="${ _csrf.parameterName }" value="${ _csrf.token }"> -->
</form>
<div>
<ol>
<li>principal : <sec:authentication property="principal"/></li>
<!-- CustomUser의 member 필드 -->
<li>MemberVO : <sec:authentication property="principal.member"/></li>
<li>사용자 ID : <sec:authentication property="principal.member.id"/></li>
<li>사용자 이름 : <sec:authentication property="principal.member.name"/></li>
<li>사용자 권한 목록 : <sec:authentication property="principal.member.authList"/></li>
</ol>
</div>
</div>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authorize access="isAuthenticated()">
<sec:authentication property="principal" var="pinfo"/>
<c:if test="${pinfo.member.id eq notice.writer}">
<a class="btn-edit button" href="noticeEdit.htm?seq=${notice.seq}">수정</a>
<a class="btn-del button" href="noticeDel.htm?seq=${ notice.seq }&filesrc=${ notice.filesrc }&${_csrf.parameterName}=${_csrf.token}">삭제</a>
</c:if>
</sec:authorize>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<div id="content">
<h2>공지사항</h2>
<h3 class="hidden">방문페이지위치</h3>
<ul id="breadscrumb" class="block_hlist">
<li id="home">
<a href="">HOME</a>
</li>
<li>
<a href="">고객센터</a>
</li>
<li>
<a href="">공지사항</a>
</li>
</ul>
<div id="notice-article-detail" class="article-detail margin-large" >
<dl class="article-detail-row">
<dt class="article-detail-title">
제목
</dt>
<dd class="article-detail-data">
${notice.title}
</dd>
</dl>
<dl class="article-detail-row">
<dt class="article-detail-title">
작성일
</dt>
<dd class="article-detail-data">
${notice.regdate}
</dd>
</dl>
<dl class="article-detail-row half-row">
<dt class="article-detail-title">
작성자
</dt>
<dd class="article-detail-data half-data" >
${notice.writer}
</dd>
</dl>
<dl class="article-detail-row half-row">
<dt class="article-detail-title">
조회수
</dt>
<dd class="article-detail-data half-data">
${notice.hit}
</dd>
</dl>
<dl class="article-detail-row">
<dt class="article-detail-title">
첨부파일
</dt>
<dd class="article-detail-data">
<%-- <a href="upload/${notice.filesrc}">${notice.filesrc}</a> --%>
<a href="download.htm?dir=customer/upload&file=${notice.filesrc}">${notice.filesrc}</a>
</dd>
</dl>
<div class="article-content" >
${notice.content}
</div>
</div>
<p class="article-comment margin-small">
<a class="btn-list button" href="notice.htm">목록</a>
<sec:authorize access="isAuthenticated()">
<sec:authentication property="principal" var="pinfo"/>
<c:if test="${pinfo.member.id eq notice.writer}">
<a class="btn-edit button" href="noticeEdit.htm?seq=${notice.seq}">수정</a>
<!-- <a class="btn-del button" href="noticeDel.htm?seq=${notice.seq}">삭제</a> -->
<a class="btn-del button" href="noticeDel.htm?seq=${ notice.seq }&filesrc=${ notice.filesrc }&${_csrf.parameterName}=${_csrf.token}">삭제</a>
</c:if>
</sec:authorize>
<script>
$(".btn-del.button").on("click", function(event) {
if(!confirm("정말 삭제할까요?")){
event.preventDefault();
}
});
</script>
</p>
<div class="margin-small" style="border-top: 1px solid #dfdfdf;">
<dl class="article-detail-row">
<dt class="article-detail-title">
▲ 다음글
</dt>
<dd class="article-detail-data">
다음 글이 없습니다.
</dd>
</dl>
<dl class="article-detail-row">
<dt class="article-detail-title">
▼ 이전글
</dt>
<dd class="article-detail-data">
제 12회 창업스쿨
</dd>
</dl>
</div>
</div>