Spring

[Spring] passwordEncoder, 로그인/로그아웃/자동로그인

아잠만_ 2024. 8. 12. 16:27

Encoder

비밀번호를 passwordEncoder된 비밀번호로 관리

passwordEncoder.encode(비밀번호) 를 해당 비밀번호에 넣어줘야함

로그인 시 부호화(encode)된 비밀번호를 java에서 비교해서 로그인 결과를 확인한다

 

(부호화된 비밀번호를 우리가 복호화할 수는 없음)

SQL

MEMBER 테이블에 enabled 추가

CREATE TABLE MEMBER_AUTH (
  MEM_ID VARCHAR2(15) NOT NULL,
 AUTH VARCHAR2(150) NOT NULL,
    CONSTRAINT MEMBER_AUTH_PK PRIMARY KEY(MEM_ID, AUTH),
    CONSTRAINT FK_MEM_AUTH  FOREIGN KEY (MEM_ID)
                REFERENCES MEMBER(MEM_ID)
);
INSERT INTO MEMBER_AUTH(MEM_ID, AUTH)
SELECT MEM_ID, 'ROLE_MEMBER' FROM MEMBER;

INSERT INTO MEMBER_AUTH(MEM_ID, AUTH)
VALUES('a001', 'ROLE_ADMIN');

COMMIT;

passwordEncoder

WEB-INF/spring/security-context.xml

spring 프레임워크있는 password인코더를 이용해 인코더를 한다

org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xmlns:security="http://www.springframework.org/schema/security"
   xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
      http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 접근 거부 클래스 객체 생성 -->
	<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
	<!-- 로그인 성공 클래스 객체 생성 -->
	<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>		
	<!-- password Encoder -->
	<bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean>
	<!-- 프레임워크에서 지원하는 Encoder -->
	<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
	
	<security:http>
		<security:intercept-url pattern="/board/list" access="permitAll"/>
		<security:intercept-url pattern="/board/regist" access="hasRole('ROLE_MEMBER')"/>
		<security:intercept-url pattern="/notice/list" access="permitAll"/>
		<security:intercept-url pattern="/notice/regist" access="hasRole('ROLE_ADMIN')"/>
		<!-- 폼 기반 인증 기능을 사용 -->
		<!-- 사용자가 정의한 로그인 페이지의 URI를 지정함
		사용자가 정의한 class를 로그인 성공 처리자로 지정 (authentication-success-handler-ref) -->
		<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess"/>
		
		<!-- //// 방법 1. URI로 요청하는 방법 error-page //// -->
		<!-- 접근 거부 처리자(HTTP 상태 403 - 금지됨 : 권한 없음) 
		로그인은 됐지만 요청URI에 대한 권한이 없다면 /accessError 요청 URI로 자동 재요청됨
		-->
<!-- 		<security:access-denied-handler error-page="/accessError"/> -->
		<!-- //// 방법 2. 등록한 사용자 정의 class를 접근 거부 처리자로 지정 ref //// -->
		<security:access-denied-handler ref="customAccessDenied"/>
	</security:http>
	
	<!-- 스프링 시큐리니티 5부터 기본적으로 PasswordEncoder를 지정해야 하는데,
      그 이유는 사용자 테이블(USERS)에 비밀번호를 암호화하여 저장해야 하므로..
      우리는 우선 비밀번호를 암호화 처리 하지 않았으므로
      암호화 하지 않는 PasswordEncoder를 직접 구현하여 지정하기로 함
      noop : no option password
    -->
	<security:authentication-manager>
		<security:authentication-provider>
<!--  			<security:user-service> -->
 			<!-- name 아이디, authorities 권한 --> 
<!-- 				<security:user name="member" password="{noop}java" authorities="ROLE_MEMBER"/> -->
<!-- 				<security:user name="admin" password="{noop}java" authorities="ROLE_MEMBER, ROLE_ADMIN"/> -->
<!-- 			</security:user-service> -->
			<!-- 데이터 소스를 지정함 -->
			<security:jdbc-user-service data-source-ref="dataSource"/> <!-- root-context에 있는 bean id -->
			<!-- 사용자가 정의한 비밀번호 암호화 처리기를 지정함 -->
<!-- 			<security:password-encoder ref="customPasswordEncoder"/> -->
		<!-- 비밀번호 암호화 처리기 지정 -->
		<security:password-encoder ref="passwordEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

LoginController.java (encode 테스트)

package kr.or.ddit.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
public class LoginController {

	@Autowired
	PasswordEncoder passwordEncoder;
	
	@GetMapping("/login")
	public String loginForm() {
		// 암호화 ( asdfasdf 라는 문자열을 암호화)
		String pw = "asdfasdf";
		String encodePw = this.passwordEncoder.encode(pw);
		log.info("encodedPw : "+encodePw); //$2a$10$H/tmoD9L561PbEZl39Ocf.A3e7XaUoHVjk3pyJGch1/ARfcmy1xXS
		return "loginForm";
	}
}

예제 - 설정

WEB_INF/spring/security-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xmlns:security="http://www.springframework.org/schema/security"
   xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
      http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- 접근 거부 클래스 객체 생성 -->
	<bean id="customAccessDenied" class="kr.or.ddit.security.CustomAccessDeniedHandler"></bean>
	<!-- 로그인 성공 클래스 객체 생성 -->
	<bean id="customLoginSuccess" class="kr.or.ddit.security.CustomLoginSuccessHandler"></bean>		
	<!-- password Encoder -->
	<bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean>
	<!-- 프레임워크에서 지원하는 Encoder -->
	<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
	<!-- 스프링 시큐리티의 UserDetailsService를 구현한 클래스를 빈으로 등록 -->
	<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>
    
	<security:http>
		<security:intercept-url pattern="/board/list" access="permitAll"/>
		<security:intercept-url pattern="/board/regist" access="hasRole('ROLE_MEMBER')"/>
		<security:intercept-url pattern="/notice/list" access="permitAll"/>
		<security:intercept-url pattern="/notice/regist" access="hasRole('ROLE_ADMIN')"/>
		<!-- 폼 기반 인증 기능을 사용 -->
		<!-- 사용자가 정의한 로그인 페이지의 URI를 지정함
		사용자가 정의한 class를 로그인 성공 처리자로 지정 (authentication-success-handler-ref) -->
		<security:form-login login-page="/login" authentication-success-handler-ref="customLoginSuccess"/>

		<security:access-denied-handler ref="customAccessDenied"/>
	</security:http>

	<security:authentication-manager>
		<security:authentication-provider user-service-ref="customUserDetailsService">
		<!-- 비밀번호 암호화 처리기 지정 -->
		<security:password-encoder ref="passwordEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

MemberVO.java (enabled 추가)

package kr.or.ddit.vo;

import java.util.Date;
import java.util.List;

import org.springframework.web.multipart.MultipartFile;

import lombok.Data;

@Data
public class MemberVO {
	private String memId;
	private String memPass;
	private String memName;
	private String memRegno1;
	private String memRegno2;
	private Date memBir;
	private String memBirStr;
	private String memZip;
	private String memAdd1;
	private String memAdd2;
	private String memHometel;
	private String memComtel;
	private String memHp;
	private String memMail;
	private String memJob;
	private String memLike;
	private String memMemorial;
	private Date memMemorialday;
	private int memMileage;
	private String memDelete;
	private String enabled;
	private MultipartFile fileImage;
	private long fileGroupNo;
	
	private List<MemberAuthVO> memberAuthVoList;
	
	private FileGroupVO fgvo;
}

MemberAuthVO.java

package kr.or.ddit.vo;

import lombok.Data;

@Data
public class MemberAuthVO {
	private String memId;
	private String auth;
}

member_SQL.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="kr.or.ddit.mapper.MemberMapper">
	<!-- 로그인 -->
	<select id="detail" parameterType="String" resultMap="memberMap">
		select A.MEM_ID, A.MEM_PASS, A.MEM_NAME, A.MEM_REGNO1, A.MEM_REGNO2, 
		A.MEM_BIR, A.MEM_ZIP, A.MEM_ADD1, A.MEM_ADD2, A.MEM_HOMETEL, 
		A.MEM_COMTEL, A.MEM_HP, A.MEM_MAIL, A.MEM_JOB, A.MEM_LIKE, 
		A.MEM_MEMORIAL, A.MEM_MEMORIALDAY, A.MEM_MILEAGE, A.MEM_DELETE, 
		A.FILE_GROUP_NO, A.ENABLED, B.AUTH
		from member a, MEMBER_AUTH B
		where a.mem_id=#{memId}
		and a.mem_id=b.mem_id
	</select>
    
	<resultMap type="memVo" id="memberMap">
		<result property="memId" column="MEM_ID"/>
		<result property="memPass" column="MEM_PASS"/>
		<result property="memName" column="MEM_NAME"/>
		<result property="memRegno1" column="MEM_REGNO1"/>
		<result property="memRegno2" column="MEM_REGNO2"/>
		<result property="memBir" column="MEM_BIR"/>
		<result property="memZip" column="MEM_ZIP"/>
		<result property="memAdd1" column="MEM_ADD1"/>
		<result property="memAdd2" column="MEM_ADD2"/>
		<result property="memHometel" column="MEM_HOMETEL"/>
		<result property="memComtel" column="MEM_COMTEL"/>
		<result property="memHp" column="MEM_HP"/>
		<result property="memMail" column="MEM_MAIL"/>
		<result property="memJob" column="MEM_JOB"/>
		<result property="memLike" column="MEM_LIKE"/>
		<result property="memMemorial" column="MEM_MEMORIAL"/>
		<result property="memMemorialday" column="MEM_MEMORIALDAY"/>
		<result property="memMileage" column="MEM_MILEAGE"/>
		<result property="memDelete" column="MEM_DELETE"/>
		<result property="fileGroupNo" column="FILE_GROUP_NO" />
		<result property="enabled" column="ENABLED"/>
		<association property="fgvo" resultMap="fileGroupMap"></association> <!-- 1:1일때 association -->
		<collection property="memberAuthVoList" resultMap="memAuthMap"></collection>
	</resultMap>
	
	<resultMap type="memAuthVo" id="memAuthMap">
		<result property="memId" column="MEM_ID"/>
		<result property="auth" column="AUTH"/>
	</resultMap>
	
	<resultMap type="fileGroupVo" id="fileGroupMap">
		<result property="fileGroupNo" column="FILE_GROUP_NO"/>
		<result property="fileRegdate" column="FILE_REGDATE"/>
		<collection property="fileDetailVoList" resultMap="fileDetailMap"></collection>
	</resultMap>
	
	<resultMap type="fileDetailVo" id="fileDetailMap">
		<result property="fileSn" column="FILE_SN"/>
		<result property="fileGroupNo" column="FILE_GROUP_NO"/>
		<result property="fileOriginalName" column="FILE_ORIGINAL_NAME"/>
		<result property="fileSaveName" column="FILE_SAVE_NAME"/>
		<result property="fileSaveLocate" column="FILE_SAVE_LOCATE"/>
		<result property="fileSize" column="FILE_SIZE"/>
		<result property="fileExt" column="FILE_EXT"/>
		<result property="fileMime" column="FILE_MIME"/>
		<result property="fileFancysize" column="FILE_FANCYSIZE"/>
		<result property="fileSaveDate" column="FILE_SAVE_DATE"/>
		<result property="fileDowncount" column="FILE_DOWNCOUNT"/>
	</resultMap>
</mapper>

type alias 추가

더보기
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
   <!-- 
   [마이바티스] 스프링에서 "_"를 사용한 컬럼명을 사용 시(BOOK 테이블의 BOOK_ID)
   카멜케이스로 읽어줌(bookId)
   ex) 테이블 컬러명이 member_id인 경우 jsp화면단에서 이 값을 사용 시 memberId로 사용
   -->
   <settings>
      <setting name="mapUnderscoreToCamelCase" value="true"/>
   </settings>
   <!-- 자주 사용하는 타입의 별칭을 세팅 -->
   <typeAliases>
       <typeAlias type="kr.or.ddit.vo.BookVO" alias="bookVo" /> 
       <typeAlias type="kr.or.ddit.vo.LprodVO" alias="LprodVo" /> 
       <typeAlias type="kr.or.ddit.vo.JdbcBoardVO" alias="boardVo" /> 
       <typeAlias type="kr.or.ddit.vo.BuyprodVO" alias="buyprodVo" /> 
       <typeAlias type="kr.or.ddit.vo.ProdVO" alias="prodVo" /> 
       <typeAlias type="kr.or.ddit.vo.BuyerVO" alias="buyerVo" /> 
       <typeAlias type="kr.or.ddit.vo.CartVO" alias="cartVo" /> 
       <typeAlias type="kr.or.ddit.vo.MemberVO" alias="memVo" /> 
       <typeAlias type="kr.or.ddit.vo.TblUserVO" alias="tblUserVo" /> 
       <typeAlias type="kr.or.ddit.vo.CardVO" alias="cardVo" /> 
       <typeAlias type="kr.or.ddit.vo.TblHobbyVO" alias="tblHobbyVo" /> 
       <typeAlias type="kr.or.ddit.vo.CarsVO" alias="carsVo" /> 
       <typeAlias type="kr.or.ddit.vo.FileDetailVO" alias="fileDetailVo" /> 
       <typeAlias type="kr.or.ddit.vo.FileGroupVO" alias="fileGroupVo" /> 
       <typeAlias type="kr.or.ddit.vo.MemberAuthVO" alias="memAuthVo" /> 
   </typeAliases>
</configuration>

MemberMapper.java

package kr.or.ddit.mapper;

import org.apache.ibatis.annotations.Mapper;

import kr.or.ddit.vo.MemberVO;

@Mapper
public interface MemberMapper {
	public int updateMember(MemberVO memVo);
	
	public MemberVO getMemberFile(MemberVO memVo);
	
	public MemberVO detail(String memId);
}

로그인 처리

CustomUserDetailsService.java

package kr.or.ddit.security;

import org.springframework.beans.factory.annotation.Autowired;
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 kr.or.ddit.mapper.MemberMapper;
import kr.or.ddit.vo.MemberVO;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {
	@Autowired
	MemberMapper memMapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		MemberVO memberVO = this.memMapper.detail(username);
		log.info("memberVO : "+memberVO);
		//MVC에서는 Controller로 리턴하지 않고, CustomUser로 리턴함
	      //CustomUser : 사용자 정의 유저 정보. extends User를 상속받고 있음
	      //2) 스프링 시큐리티의 User 객체의 정보로 넣어줌 => 프링이가 이제부터 해당 유저를 관리
	      //User : 스프링 시큐리에서 제공해주는 사용자 정보 클래스
	      /*
	       memberVO(우리) -> user(시큐리티)
	       -----------------
	       userId        -> username
	       userPw        -> password
	       enabled       -> enabled
	       memberAuthVoList               -> authorities
	       */
		return memberVO==null?null:new CustomUser(memberVO);
	}

}

CustomUser.java

memberVO형태로 로그인을 해주는 객체

또한 값을 memberVO값을 저장해 꺼낼 수 있음

package kr.or.ddit.security;

import java.util.Collection;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import kr.or.ddit.vo.MemberVO;

//사용자가 유저를 정의함
//memberVO(select결과)정보를 User(스프링 시큐리티에서 정의된 유저) 객체 정보에 연계하여 넣어줌
//CustomUser의 객체 = principal
public class CustomUser extends User{
	
	private MemberVO memberVO;

	// User의 생성자를 처리해주는 생성자
	public CustomUser(String username, String password, Collection<? extends GrantedAuthority> auth) {
		super(username, password, auth);
	}
	/*
    memberVO(우리) -> user(시큐리티)
    -----------------
    userId        -> username
    userPw        -> password
    enabled       -> enabled
    memberAuthVoList               -> authorities
    */
    // 위에 항목을 memberVO를 통해 바꿔주는 생성자
	public CustomUser(MemberVO memVo) {
		super(memVo.getMemId(), memVo.getMemPass(), 
				memVo.getMemberAuthVoList().stream()	//Stream<MemberAuthVO>
				.map(auth->new SimpleGrantedAuthority(auth.getAuth())) // Stream<String> auth만 뽑아 저장
				.collect(Collectors.toList())); // List<String>
		this.memberVO = memVo;
	}
	
	public MemberVO getMemberVO() {
		return memberVO;
	}
	public void setMemberVO(MemberVO memberVO) {
		this.memberVO = memberVO;
	}
	
}

aside.jsp

memberVO값을 CustomUser를 통해 받아와 이름을 가져올 수 있음

      <!-- 로그인 후 -->
      <sec:authorize access="isAuthenticated()">
        <div class="image">
          <img src="/resources/images/NpcNmlDuk11.png" class="img-circle elevation-2" alt="User Image">
        </div>
        <div class="info">
          <sec:authentication property="principal.memberVO" var="user"/>
          <a href="#" class="d-block">${user.memName}</a>
        </div>
      </sec:authorize>
      <!-- 로그인 후 -->
      </div>

로그아웃

aside.jsp

로그아웃 추가

form action = "/logout"과 method="post"로 설정

csrfInput 설정할 것

      <!-- 로그인 후 -->
      <sec:authorize access="isAuthenticated()">
        <div class="image">
          <img src="/resources/images/NpcNmlDuk11.png" class="img-circle elevation-2" alt="User Image">
        </div>
        <div class="info" style="display:flex;">
          <sec:authentication property="principal.memberVO" var="user"/>
          <a href="#" class="d-block">${user.memName} (${user.memId})</a>
          <form action="/logout" method="post" style="margin-left: 5px;">
	          <button type="submit" class="btn btn-block btn-outline-primary btn-xs">로그아웃</button>
	          <sec:csrfInput/>
          </form>
        </div>
      </sec:authorize>
      <!-- 로그인 후 -->

WEB_INF/spring/security-context.xml

	<security:http>
		
		<!-- 로그아웃 처리를 위한 URI지정, 로그아웃한 후 세션을 무효화 -->
		<!-- 로그아웃을 하면 자동 로그인에 사용된 쿠키도 함께 삭제해 줌  invalidate-session="true"-->
		<security:logout logout-url="/logout" invalidate-session="true"/>
	</security:http>

자동 로그인

특정 시간 동안 다시 로그인 할 필요가 없는 기능

스프링 시큐리티은 메모리나 데이터베이스를 사용하여 처리한다

여기서는 데이터베이스를 이용하므로 SQL로 테이블 하나를 추가시킨다

/*
USERNAME : 누구
SERIES : 기본키(중복방지코드)
TOKEN : 위조방지키
LAST_USED : 마지막 사용한 날짜
*/
CREATE TABLE PERSISTENT_LOGINS(
    USERNAME VARCHAR2(200),
    SERIES VARCHAR2(200),
    TOKEN VARCHAR2(200),
    LAST_USED DATE,
    CONSTRAINT PK_PER_LIN PRIMARY KEY(SERIES)
);

WEB_INF/spring/security-context.xml

	<security:http>

		<!-- dataSource를 통해 지정한 Database의 약속된 테이블(PERSISTENT_LOGINS)을
      		이용하여 기존 로그인 정보를 기록함 -->
      <!-- token-validity-seconds : 쿠키의 유효시간(초) 604800초는 7일 -->
		<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
		
		<!-- 로그아웃 처리를 위한 URI지정, 로그아웃한 후 세션을 무효화 -->
		<!-- 로그아웃을 하면 자동 로그인에 사용된 쿠키도 함께 삭제해 줌  invalidate-session="true"-->
		<security:logout logout-url="/logout" invalidate-session="true" delete-cookies="remember-me,JSESSION_ID"/>
	</security:http>

loginForm.jsp

<div class="col-8">
	<div class="icheck-primary">
	<!-- name값은 remember-me -->
		<input type="checkbox" id="remember-me" name="remember-me"> 
		<label for="remember-me"> 자동 로그인 </label>
	</div>
</div>