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>
'Spring' 카테고리의 다른 글
[Spring] 오류 페이지 (0) | 2024.08.13 |
---|---|
[Spring] 페이지 별 권한 주기 @PreAuthorize (0) | 2024.08.13 |
[Spring] JDBC 이용한 인증 (0) | 2024.08.12 |
[Spring] 시큐리티 (0) | 2024.08.09 |
[Spring] 입력값 검증 (Validator) (0) | 2024.08.09 |