0. 테이블 생성하기
CREATE TABLE USERS(
USERNAME VARCHAR2(50),
PASSWORD VARCHAR2(300),
ENABLED VARCHAR2(1),
CONSTRAINT PK_USERS PRIMARY KEY (USERNAME)
);
CREATE TABLE AUTHORITIES (
USERNAME VARCHAR2(50),
AUTHORITY VARCHAR2(300),
CONSTRAINT PK_AUTHORITIES PRIMARY KEY (USERNAME, AUTHORITY),
CONSTRAINT FK_AUTHORITIES FOREIGN KEY (USERNAME) REFERENCES USERS(USERNAME)
);
1. security-context.xml 설정
<bean id="customLoginSuccess"
class="kr.or.ddit.security.CustomLoginSuccessHandler" />
<bean id="customAccessDenied"
class="kr.or.ddit.security.CustomAccessDeniedHandler" />
<bean id="customLogoutSuccessHandler"
class="kr.or.ddit.security.CustomLogoutSuccessHandler" />
<security:http>
<!-- URI 패턴으로 접근 제한을 설정 -->
<!-- 권한이 없을 경우 로그인 페이지로 이동한다. -->
<security:intercept-url pattern="/board/list" access="permitAll" />
<security:intercept-url pattern="/board/register" access="hasRole('ROLE_MEMBER')" />
<security:intercept-url pattern="/notice/list" access="permitAll" />
<security:intercept-url pattern="/notice/register" access="hasRole('ROLE_ADMIN')" />
<!-- form 기반 인증 기능 사용 -->
<security:form-login login-page="/login"
authentication-success-handler-ref="customLoginSuccess" />
<!-- 등록한 CustomAccessDeniedHandler를 접근 거부 처리자로 지정함 -->
<security:access-denied-handler ref="customAccessDenied" />
<!-- 로그아웃 처리를 위한 URI를 지정하고, 로그아웃한 후에 세션을 무효화 -->
<security:logout logout-url="/logout" invalidate-session="true"
uccess-handler-ref="customLogoutSuccessHandler" />
</security:http>
<bean id="customUserDetailsService"
class="kr.or.ddit.security.CustomUserDetailsService" />
<bean id="customPasswordEncoder"
class="com.java.web.security.CustomNoOpPasswordEncoder" />
<!-- <bean id="bCryptPasswordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" /> -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<!-- dataSource는 root-context.xml 에서 생성한 DB연동 정보 빈이다. -->
<!-- <security:jdbc-user-service data-source-ref="dataSource"
users-by-username-query="SELECT MEM_ID username,
MEM_PASS password,
MEM_STATUS enabled
FROM TESTMEMBER
WHERE MEM_ID = ?"
authorities-by-username-query="SELECT MEM_ID username,
AUTHORITY
FROM TESTAUTH
WHERE MEM_ID = ?" />
-->
<!-- 사용자 정의 암호화 처리기 설정 -->
<security:password-encoder ref="customPasswordEncoder" />
<!-- 스프링 시큐리티에서 제공하는 암호화 처리기 사용 시 -->
<!-- <security:password-encoder ref="bCryptPasswordEncoder" /> -->
</security:authentication-provider>
</security:authentication-manager>
| <security:http> | 접근을 제한할 URL과 인증 처리의 시작과 마무리를 담당 |
| <security:intercept-url> | 접근을 제한할 URL을 지정 |
| pattern | 접근을 제한할 URL 경로 |
| access | 접근 제한 유형 permitAll : 모든 사용자가 접근 가능 hasRole('권한명') : 해당 권한명을 가진 사용자만 접근 가능 |
| <security:form-login> | form 기반의 사용자 인증을 사용 |
| login-page | 인증이 필요할 때 사용할 로그인 페이지 경로 |
| authentication-success-handler-ref | 인증에 성공했을 때 작업을 수행할 클래스 |
| <security:access-denied-handler> | 인증에는 성공했으나 권한이 없을 때 (403) |
| ref | 접근 거부(403) 시 작업을 수행할 클래스 |
| <security:logout> | 로그아웃과 관련된 설정 |
| logout-url | 로그아웃을 시도하고자 할 때 사용해야하는 경로명 |
| invalidate-session="true: | true 일 경우 자동으로 세션이 초기화 됨 |
| success-handler-ref | 로그아웃 성공 시 작업을 수행할 클래스 |
* 로그아웃 시 주의할 점
로그아웃 요청 경로 ("/logout")는 반드시 POST 방식으로 요청을 보내야한다.
GET 방식으로 요청을 보내면 요청을 인식하지 못 해 404 오류가 발생한다.
| <security:authentication-manager> | 인증 과정에 관한 처리를 담당 |
| <security:authentication-provider> | |
| user-service-ref | 사용자 정의 인증 서비스 사용자 정보를 DB에서 꺼내오고 확인하는 과정을 개발자 임의로 작성할 때 사용한다. |
| <security:jdbc-user-service> | 사용자 인증을 DB와 연동하여 수행하기 위한 태그 |
| data-source-ref | 연동하는 DB 정보가 담긴 DataSource 객체. root-context.xml에서 MyBatis에 사용하기 위해 생성했던 BasicDataSource 빈을 사용하면 된다. |
| users-by-username-query | 인증을 위한 쿼리를 직접 작성. 사용자의 아이디, 비밀번호를 확인하고 로그인을 허가하기 위한 쿼리. |
| authorities-by-username-query | 권한 확인을 위한 쿼리를 직접 작성. 인증된 사용자의 권한을 확인하고 접근을 허가하기 위한 쿼리. |
| <security:password-encoder> | 사용자 정의 암호화 처리기를 사용하기 위한 태그 |
| ref | 사용자 정의 암호화 처리기 빈의 id |
* 사용자 정의 쿼리를 사용할 경우
사용자 정의 쿼리를 통해 인증 정보와 사용자 권한을 조회할 수 있다. 주의해야하는 점은 스프링 시큐리티에서 사용하는 기본 컬럼명은 username, password, enabled, authority 이다. 만약 컬럼명이 다르다면 별칭으로 이 이름들을 지정해주어야한다.
* BCryptPasswordEncoder
스프링 시큐리티에서 제공하는 암호화 처리 클래스
2. 사용자 정의 암호화 처리기 생성
원래라면 사용자의 비밀번호를 DB에 암호화하여 저장해야하고 로그인 시 들어온 비밀번호 값을 암호화된 값과 비교하는 과정이 필요하다. 하지만 현재는 테스트 단계이므로 암호화 과정이 생략된 암호화 처리 클래스를 사용한다.
package kr.or.ddit.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.extern.slf4j.Slf4j;
public class CustomNoOpPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
}
스프링에서 제공하는 기본 암호화 클래스를 사용하는 것도 가능하다.
<security:password-encoder ref="bCryptPasswordEncoder" />
3. 사용자 정의 인증 서비스 생성
스프링 시큐리티에서 기본으로 제공하는 쿼리가 아닌 개발자가 직접 처리하고자 할 경우 security:authentication-provider> 에 user-service-ref 속성을 사용해 직접 서비스를 생성할 수 있다.
이 서비스는 UserDetailsService 인터페이스를 구현해야한다.
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private MemMapper memMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemVO memVO = new MemVO();
memVO.setUserId(username);
memVO = memMapper.memLogin(memVO);
return memVO == null ? null : new CustomUser(memVO);
}
}
1. SQL 작성
사용자와 인증의 관계는 1 대 N 이다. 때문에 1 대 N을 처리하기 위한 쿼리를 작성해야한다.
<resultMap id="memMap" type="memVO">
<result property="userNo" column="USER_NO" />
<result property="userId" column="USER_ID" />
<result property="userPw" column="USER_PW" />
<result property="userName" column="USER_NAME" />
<result property="coin" column="COIN" />
<result property="regDate" column="REG_DATE" />
<result property="updDate" column="UPD_DATE" />
<result property="enabled" column="ENABLED" />
<collection property="list" resultMap="memAuthMap" />
</resultMap>
<resultMap id="memAuthMap" type="memAuthVO">
<result property="userNo" column="USER_NO" />
<result property="auth" column="AUTH" />
</resultMap>
<select id="memLogin" parameterType="memVO" resultMap="memMap">
SELECT A.USER_NO, A.USER_ID, A.USER_PW, A.USER_NAME, A.COIN, A.REG_DATE, A.UPD_DATE, A.ENABLED,
B.USER_NO, B.AUTH
FROM MEM A LEFT OUTER JOIN MEM_AUTH B ON(A.USER_NO = B.USER_NO)
WHERE USER_ID = #{userId}
</select>
2. VO 클래스 생성
VO 클래스를 작성한다.
@Data
public class MemVO {
private int userNo;
private String userId;
private String userPw;
private String userName;
private int coin;
private Date regDate;
private Date updDate;
private String enabled;
private List<MemAuthVO> list;
}
@Data
public class MemAuthVO {
private int userNo;
private String auth;
}
3. Mapper, Service 작성
기존 MyBatis의 DB 연동과 같이 Mapper와 Service를 작성한다. 작성이 끝난 후 위에서 생성했던 CustomUserDetailsService에 연결하면 된다.
public interface MemMapper {
public MemVO memLogin(MemVO memVO);
}
package kr.or.ddit.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import kr.or.ddit.mapper.MemMapper;
import kr.or.ddit.vo.MemVO;
@Service("empService")
public class EmpServiceImpl implements EmpService {
@Autowired
private MemMapper memMapper;
@Override
public MemVO memLogin(MemVO memVO) {
return memMapper.memLogin(memVO);
}
}
4. User 클래스의 상속 클래스 작성
User 클래스를 상속하여 사용자의 권한을 스프링에서 관리할 수 있도록 준비한다.
UserDetailsService 인터페이스의 loadUserByUsername() 메서드의 반환 타입은 UserDetails이다. UserDetails 의 구현 클래스가 User 클래스이므로 이 User 클래스를 사용해 새로운 클래스를 만든다.
UserDetails 인터페이스는 사용자 아이디, 비밀번호, 권한 목록을 파라미터로 하는 생성자의 구현을 요구한다. 아래의 CustomUser 클래스는 DB 처리로 받아온 VO 객체를 사용해 이 데이터를 부모 클래스(super) 생성자로 보낸다.
권한 목록의 경우 GrantedAuthority 객체가 담긴 List 타입으로 제공해야한다. 우리가 방금 만든 VO 클래스에는 회원정보가 담긴 list 멤버변수가 존재한다. 이 멤버변수에서 권한명을 하나씩 꺼내어 SimpleGrantedAuthority 객체를 만든 후 리스트에 담아 제공하면 된다. 보기 좋게 스트림을 사용해도 좋고 이를 처리해주는 static 메서드를 생성해도 된다.
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.MemVO;
// User : 스프링 시큐리티가 제공하고 있는 사용자 정보 클래스
public class CustomUser extends User {
// 이 객체는 JSP에서 사용할 수 있게 된다.
private MemVO memVO;
public CustomUser(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
// 스프링에서 이 회원정보를 관리한다.
public CustomUser(MemVO memVO) {
// 1) username 2) password 3) authorities
super(memVO.getUserId(), memVO.getUserPw(),
memVO.getList().stream()
.map(auth -> new SimpleGrantedAuthority(auth.getAuth()))
.collect(Collectors.toList())
);
// 다음과 같은 방법을 사용할 수도 있다.
// super(memVO.getUserId(), memVO.getUserPw(), getCollect(memVO));
this.memVO = memVO;
}
// 다음과 같은 방법을 사용할 수도 있다.
/*
public static List<SimpleGrantedAuthority> getCollect(MemVO memVO){
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<MemAuthVO> memAuthList = memVO.getList();
for(MemAuthVO memAuth : memAuthList) {
authorities.add(new SimpleGrantedAuthority(memAuth.getAuth()));
}
return authorities;
}
*/
public MemVO getMemVO() {
return memVO;
}
public void setMemVO(MemVO memVO) {
this.memVO = memVO;
}
}
'Java > Spring Framework' 카테고리의 다른 글
| [Spring Framework] 스프링 시큐리티 자동 로그인 (0) | 2023.02.14 |
|---|---|
| [Spring Framework] 스프링 시큐리티 태그 라이브러리 (0) | 2023.02.14 |
| @PathVariable (0) | 2023.02.08 |
| [Spring Framework] Date 타입 파라미터 (0) | 2023.02.06 |
| [Spring Framework] 클라이언트 파일 다운로드 구현 (0) | 2023.02.02 |