스프링 시큐리티
스프링에서 제공하는 프레임 워크중 하나로써 스프링 기반 어플리케이션의 보안(인증, 인가 , 권한)에 관한 처리들을 담당한다. 스프링 시큐리티를 사용하지 않으면 세션체크등 관련 내용들을 직접 구현해야한다.
자주사용되는 용어
Principal(접근주체) : 보호된 대상에 접근하는 유저
Authentication(인증) : 어떤 유저가 애플리케이션의 작업을 수행 할 수 있는 주체인지 증명하는 과정?
Authorization(인가) : 권한부여/허가와 동일한 의미, 현재 유저가 애플리케이션에 접근 할 수 있는 권한이 있는지를 검사
권한 : 인증된 주체가 애플리케이션의 동작을 수행할 수 있도록 허락 되어있는지를 결정
권한부여의 두가지 영역 -> 웹 요청 권한 , 메소드 호출 및 도메인 인스턴스에 대한 접근 권한 부여
webapp\WEB_INF\spring 폴더내에 security-context.xml 파일 생성
<security:authentication-manager>
<security:authentication-provider>
<security:jdbc-user-service data-source-ref=""/>
<!-- <security:user-service> -->
<!-- <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:authentication-provider>
</security:authentication-manager>
</beans>
AuthenticationManager - 인증 처리하는 filter로부터 첫번째로 지시받는 클래스
<bean id="dataSource"
class="org.apache.commons.dbcp2.BasicDataSource"
destroy-method="close">
<property name="driverClassName"
value="oracle.jdbc.driver.OracleDriver" />
<property name="url"
value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="jspexam" />
<property name="password" value="java" />
</bean>
data-source-ref="dataSource" 작성시 root-context의 dataSource에 해당하는 객체정보를 모두 끌고온다
<security:authentication-manager>
<security:authentication-provider>
<security:jdbc-user-service data-source-ref="dataSource"/>
<!-- <security:user-service> -->
<!-- <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:password-encoder ref="customPasswordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
사용자 정의 패스워드 인코더 -> 패스워드 인증 직접 구현
dataSource-> 사용자 정보
customPasswordEncoder-> 패스워드암호
빈 객체 추가/구현 CustomNoOpPasswordEncoder -> no option
<bean id="customPasswordEncoder" class="kr.or.ddit.security.CustomNoOpPasswordEncoder"></bean>
CustomNoOpPasswordEncoder 클래스 생성
package kr.or.ddit.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomNoOpPasswordEncoder implements PasswordEncoder{
@Override
public String encode(CharSequence rawPassword) {
// 비밀번호를 받아서 인코딩 해주는 메소드 , but 아무것도 하지 않는다
log.info("before encode : " + rawPassword);
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// 비밀번호를 받은 것과 DB의 비밀번호랑 비교
log.warn("matches : " + rawPassword + " : " + encodedPassword);
// 일치하면 true를 리턴, 불일치하면 false를 리턴
return rawPassword.toString().equals(encodedPassword);
}
}
부모테이블의 기본키가 자식테이블의 외래키가 되고,
자식테이블의 외래키가 부모테이블의 기본키를 참조
참조무결성에 위배된다 -> 4의 부모키 존재하지않는다
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<!-- 사용자의 디테일 서비스를 직접 구현 -->
<!-- <security:jdbc-user-service data-source-ref="dataSource"/> -->
<!-- <security:user-service> -->
<!-- <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> -->
<!-- 1. 비밀번호 암호화 사용하지 않는다 -->
<!-- <security:password-encoder ref="customPasswordEncoder" /> -->
<!-- 2. 비밀번호 암호화 사용(직접구현x) -->
<security:password-encoder ref="passwordEncoder" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
security user <--> member 와의 매핑작업
customUserDetailsService에서
1.select
2.super(부모)
<bean id="customUserDetailsService" class="kr.or.ddit.security.CustomUserDetailsService"></bean>
빈객체 추가
UserDetailsService -> 사용자 상세정보를 가지고 있는 인터페이스(스프링 시큐리티에서 제공)
package kr.or.ddit.controller;
import java.sql.Date;
import java.util.List;
import lombok.Data;
@Data
public class MemberVO {
private int userNo;
private String userId;
private String userPw;
private String userName;
private int coin;
private Date regDate;
private Date updDate;
private String enabled;
// 회원 : 권한 = 1 : N
// 중첩된 자바빈
private List<MemberAuthVO> memberAuthVOList;
}
package kr.or.ddit.controller;
import lombok.Data;
@Data
public class MemberAuthVO {
private int userNo;
private String auth;
}
MemberVO , MemberAuthVO 생성
package kr.or.ddit.controller;
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.dao.MemberDao;
import lombok.extern.slf4j.Slf4j;
/*
UserDetailsService : 스프링 시큐리티에서 제공해주는 사용자 상세 정보를 가지고 있는 인터페이스
*/
@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {
//DI Dependency Injection : 의존성 주입
//IoC Inversion of Control : 제어역전
@Autowired
private MemberDao memberDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1) 사용자 정보를 검색
// String username : 로그인 시 입력받은 회원의 아이디 ↓↓↓
// <input type="text" name="username"> 에서 username에 해당하는 부분
MemberVO memberVO = this.memberDao.detail(username);
log.info("memberVO : " + memberVO);
// MVC에서는 Controller로 리턴하지 않고, CustomUser로 리턴함
// CustomUser : 사용자 정의 유저 정보. extends User를 상속받고 있음
// 2) 스프링 시큐리티의 User 객체의 정보로 넣어줌 => 스프링에서 해당 유저를 관리
// User : 스프링 시큐리티에서 제공해주는 사용자 정보 클래스(최상위클래스)
/*
memberVO(우리) -> user(시큐리티)
-----------------
userId -> username
userPw -> password
enabled -> enabled
auth들 -> authorities
*/
return memberVO == null ? null : CustomUser(memberVO);
// CustomUser 클래스는 상속받아서 직접 선언/
}
}
package kr.or.ddit.dao;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import kr.or.ddit.controller.MemberVO;
//자바빈 선언
@Repository
public class MemberDao {
// 쿼리 실행 객체 선언, DI
@Autowired
SqlSessionTemplate sqlSessionTemplate;
// 로그인 회원 검색
public MemberVO detail(String username) {
// selectOne("namespace.id" , 파라미터)
return this.sqlSessionTemplate.selectOne("member.detail", username);
}
}
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">
<!-- namespace : xml파일은 여러개일 수 있음. 이를 구별하기 위한 식별 용도로 사용 -->
<mapper namespace="member">
<!-- member 테이블과 memberAuth 테이블이 조인되어야 하기때문에 resultType이 아니라 resultMap -->
<resultMap type="memberVO" id="memberMap">
<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="memberAuthVOList" resultMap="memberAuthMap"></collection>
</resultMap>
<resultMap type="MemberAuthVO" id="memberAuthMap">
<result property="userNo" column="USER_NO"/>
<result property="auth" column="AUTH"/>
</resultMap>
<!-- 로그인 처리를 위해 회원 검색 -->
<select id="detail" parameterType="String" resultMap="memberMap">
SELECT
A.USER_NO, A.USER_ID, A.USER_PW, A.USER_NAME, A.COIN , A.REG_DATE,
A.UPD_DATE, A.ENABLED , B.AUTH
FROM MEMBER A, MEMBER_AUTH B
WHERE
A.USER_NO = B.USER_NO
AND A.ENABLED = '1'
AND A.USER_ID = 'admin';
</select>
</mapper>
equl join = 동등조인 = 내부조인 = inner join
조인테이블
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 MEMBER A, MEMBER_AUTH B
WHERE A.USER_NO = B.USER_NO
AND A.ENABLED = '1'
AND A.USER_ID = 'admin';
쿼리문에서 중복되는 부분은 제거 해준다
<?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.StudVO" alias="studVO" />
<typeAlias type="kr.or.ddit.vo.LprodVO" alias="lprodVO" />
<typeAlias type="kr.or.ddit.vo.HobbyVO" alias="hobbyVO" />
<typeAlias type="kr.or.ddit.vo.MemberVO" alias="memberVO" />
<typeAlias type="kr.or.ddit.vo.MemberAuthVO" alias="MemberAuthVO" />
</typeAliases>
</configuration>
mybatisAlias에서 VO alias 추가
일대다의 관계이기때문에 resultMap사용
memberVO에는 list의 형태로 권한이 존재(memberVO의 memberAuthVOList) ↓ ↓ ↓
스프링에서 제공하는 superclass 의 user를 상속받아서 customuser 클래스 생성
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;
//사용자가 유저를 정의함
//memVO(사용자 정의한 유저)정보를 User(스프링 시큐리티에서 정의된 유저) 객체 정보에 연계하여 넣어줌
//CustomUser의 객체 = principal
public class CustomUser extends User {
/*
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
*/
private MemberVO memberVO;
// USER의 생성자 오버로딩
public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUser(MemberVO memberVO) {
// 사용자가 정의한 (select한) MemVO 타입의 객체 memVO를
// 스프링 시큐리티에서 제공해주고 있는 UsersDetails 타입으로 변환
// 회원정보를 보내주면 스프링에서 직접 관리
super(memberVO.getUserNo()+ "" , memberVO.getUserPw(), memberVO.getMemberAuthVOList().stream().
map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList())
);
// 리스트를 스트림으로 받는다 -> auth 객체에 넣어준다
this.memberVO = memberVO;
}
public MemberVO getMemberVO() {
return memberVO;
}
public void setMemberVO(MemberVO memberVO) {
this.memberVO = memberVO;
}
}
스프링 시큐리티를 사용하여 별도의 인증/권한을 체크하는 이유?