스프링 시큐리티는 인증 (Authentication) ,권한(Authorize) 부여 및 보호 기능을 제공하는 프레임워크다.
출처
```
1. Http Request 수신
-> 사용자가 로그인 정보와 함께 인증 요청을 한다.
-> AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성한다.
-> AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.
-> AutenticationManger는 등록된 AuthenticationProvider들을 조회하며 인증을 요구한다.
-> 실제 데이터베이스에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.
-> 넘겨받은 사용자 정보를 통해 데이터베이스에서 찾아낸 사용자 정보인 UserDetails 객체를 만든다.
-> AuthenticaitonProvider들은 UserDetails를 넘겨받고 사용자 정보를 비교한다.
-> 인증이 완료가되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.
-> 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.
-> Authentication 객체를 Security Context에 저장한다.
최종적으로는 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다. 사용자 정보를 저장한다는 것은 스프링 시큐리티가 전통적인 세선-쿠키 기반의 인증 방식을 사용한다는 것을 의미한다.
1. login 요청이 발생하면 Spring Security Filter에서 로그인을 진행
2. 로그인이 완료 되면 Security Session을 생성 (Security Context Holder)
3. Security Context Holder 에 Authentication 객체를 가지고 있고
4. Authentication 에 UserDetails 객체가 있어야 함
회원분류, 회원상태, 이메일 컬럼이 추가되었다.
Spring Security 라이브러리 적용
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserLogoutSucessHandler userLogoutSucessHandler;
@Autowired
private MemberSocialService memberSocialService;
@Bean
//public 을 선언하면 default로 바꾸라는 메세지 출력
WebSecurityCustomizer webSecurityConfig() {
//Security에서 무시해야하는 URL 패턴 등록
return web -> web
.ignoring()
.antMatchers("/images/**")
.antMatchers("/js/**")
.antMatchers("/css/**")
.antMatchers("/favicon/**")
.antMatchers("/templates/**")
;
}
@Bean
SecurityFilterChain fiterChain(HttpSecurity httpSecurity)throws Exception{
httpSecurity
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
//URL과 권한 매칭
// .permitAll()
.antMatchers("/**").permitAll()
// .antMatchers("/member/join").permitAll()
.and()
.formLogin()//로그인 폼 방식 인증
.loginPage("/member/login")//내장된 로그인 폼을 안쓰고 개발자가 만든 폼을 요청할 URL (Controller 작성)
.usernameParameter("accountId")//내장된 로그인 폼을 안쓰고 개발자가 만든 폼을 요청할 URL (Controller 작성)
.successHandler(new UserSuccessHandler())//인증이 성공한 후 실행 하는 객체(개발자가 생성)
.failureHandler(new UserLoginFailHandler())//인증에 실패한 후 실행 하는 객체(개발자가 생성)
.permitAll()
.and()
.logout()
.logoutUrl("/member/logout")//로그아웃 URL 주소 변경 가능 (Controller 처리 X), defaultURL-/logout
// .addLogoutHandler(userLogoutHandler)// 로그아웃 성공 후 이동할 URL 설정(단순 로그아웃)-> 아래 로그아웃success핸들러로 로그아웃 구현
.logoutSuccessHandler(userLogoutSucessHandler) // 로그아웃 성공 후 이동할 URL 설정(로그아웃 성공시)
.invalidateHttpSession(true)// 로그아웃 후 세션 초기화 설정
.deleteCookies("JSESSIONID")// 로그아웃 후 쿠기 삭제 설정
.permitAll()
.and()
//API 로그인
.oauth2Login() //Social Login 설정
.userInfoEndpoint()
.userService(memberSocialService)
;
return httpSecurity.build();
}
//password를 암호화 해주는 Bean
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
public class UserSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String remember = request.getParameter("remember");
MemberVO memberVO= (MemberVO)authentication.getPrincipal();
if(remember != null && remember.equals("remember")) {
memberVO= (MemberVO)authentication.getPrincipal();
Cookie cookie = new Cookie("remember", memberVO.getAccountId());
//Cookie cookie = new Cookie("remember", authentication.getName());
cookie.setMaxAge(60*60*24);
response.addCookie(cookie);
}else {
Cookie [] cookies = request.getCookies();
for(Cookie cookie:cookies) {
if(cookie.getName().equals("remember")) {
cookie.setMaxAge(0);
response.addCookie(cookie);
break;
}
}
}
response.sendRedirect("/");
}
}
@Component
public class UserLogoutSucessHandler implements LogoutSuccessHandler{
//개발자가 SecurityConfig 파일에서 new UserLogoutSucessHandler()로 직접 객체 생성히면 autowired는 먹지 X
//스프링이 대신 해주는 기능이기 때문
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String restKey;
// 로그아웃 시 kakao의 정보도 로그아웃.
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String redirectUrl="/";
MemberVO memberVO= (MemberVO)authentication.getPrincipal();
if(memberVO.getJoinType().equals("Kakao")) {
redirectUrl="https://kauth.kakao.com/oauth/logout?client_id="+restKey+"&logout_redirect_uri=http://localhost/";
}
response.sendRedirect(redirectUrl);
}
}
public class UserLoginFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String errorMessage="";
if(exception instanceof BadCredentialsException) {
errorMessage="비번 틀림";
}else if(exception instanceof InternalAuthenticationServiceException) {
errorMessage="ID 확인";
}else if(exception instanceof DisabledException) {
errorMessage="유효하지 않은 사용자입니다";
}else {
errorMessage="로그인 실패";
}
errorMessage=URLEncoder.encode(errorMessage, "UTF-8");
response.sendRedirect("/member/login?errorMessage="+errorMessage);
}
}
@Data
public class MemberVO implements UserDetails,OAuth2User{
private Long id;
@NotBlank
private String accountId;
@NotBlank
private String password;
@NotBlank
private String passwordCheck;
private String name;
private String phone;
private String eMail;
private Boolean marketing;
private Boolean status;
private String joinType;
private Timestamp regDate;
private Timestamp updateDate;
private Timestamp loginDate;
/* DB에서 조회시 사용자 권한을 담을 List */
private List<RoleVO> roleVOs;
//OAuth2User, token 정보 저장
private Map<String, Object> attributes;
/* 사용자 권한을 Security에서 사용 할 수 있도록 변환 */
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
List<GrantedAuthority> authorities = new ArrayList<>();
for(RoleVO roleVO:roleVOs) {
authorities.add(new SimpleGrantedAuthority(roleVO.getName()));
}
return authorities;
}
@Override
public String getPassword() {
// password 반환
return password;
}
@Override
public String getUsername() {
// accountId 반환(config에서 userName-> accountId로 설정함)
return accountId;
}
@Override
public boolean isAccountNonExpired() {
// 계정의 만료 여부
// true 만료 안됨,
// false 만료됨, 로그인 안됨
return true;
}
@Override
public boolean isAccountNonLocked() {
// 계정 잠김 여부
// true : 계정 잠기지 않음
// false : 계정 잠김, 로그인 안됨
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// 비밀번호 만료 여부
// true : 만료 안됨
// false : 만료 됨, 로그인 안됨
return true;
}
@Override
public boolean isEnabled() {
// 계정 사용 여부
// true : 계정 활성화(계정 사용 가능)
// false : 계정 비활성화 (계정 사용 불가, 로그인 안됨)
return enabled;
}
}
@Setter
@Getter
public class RoleVO {
private Long id;
private String name;//roleName
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.man.ameise.dao.MemberDAO">
<select id="getMemberList" resultType="MemberVO">
select * from MEMBER
</select>
<!-- id 중복검사 -->
<select id="idDuplicateCheck" resultType="MemberVO" parameterType="MemberVO">
SELECT account_id FROM MEMBER WHERE account_id=#{accountId}
</select>
<!-- 로그인 -->
<select id="getMemberLogin" parameterType="MemberVO"
resultMap="getLoginResult">
select m.id as m_id,m.account_id
,m.password ,m.status,m.join_type,r.id
as r_id, r.name as r_name from `MEMBER` m
inner join
MEMBERROLE mr
on m.id =mr.member_id
INNER JOIN
`ROLE` r
on
mr.role_id =r.id
where account_id =#{accountId}
</select>
<!-- 로그인 resultMap -->
<resultMap type="MemberVO" id="getLoginResult">
<id property="id" column="m_id" />
<result property="accountId" column="account_id" />
<result property="password" column="password" />
<result property="status" column="status" />
<result property="joinType" column="join_type" />
<collection property="roleVOs" javaType="List"
ofType="RoleVO">
<id property="id" column="r_id" />
<result property="name" column="r_name" />
</collection>
</resultMap>
<!-- 일반 회원가입 -->
<insert id="setMemberJoin" parameterType="MemberVO">
<selectKey keyProperty="id" resultType="Long" order="AFTER">
select
id from `MEMBER` where id=(SELECT MAX(id) from `MEMBER`)
</selectKey>
insert into `MEMBER`(account_id,password,name,phone,marketing,join_type,reg_date,update_date,login_date,status,e_mail)
VALUES(#{accountId},#{password},#{name},#{phone},#{marketing},#{joinType},now(),now(),now(),1,#{eMail})
</insert>
<!-- member별 ROLE추가 -->
<insert id="setMemberRole" parameterType="Map">
INSERT INTO MEMBERROLE
(role_id, member_id)
VALUES(#{roleId}, #{memberId})
</insert>
</mapper>
@Service
public class MemberService implements UserDetailsService {
@Autowired
private MemberDAO memberDAO;
@Autowired
private PasswordEncoder passwordEncoder;
/* ****************************** 로그인 ****************************** */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemberVO memberVO = new MemberVO();
memberVO.setAccountId(username);
try {
memberVO = memberDAO.getMemberLogin(memberVO);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return memberVO;
}
/* UserDetails를 리턴하면 Spring security에서 memberVO의 password를 꺼내 비교하고 최종 결정 한다 */
public MemberVO getMemberList()throws Exception {
return memberDAO.getMemberList();
}
public int setMemberJoin(MemberVO memberVO)throws Exception{
memberVO.setPassword(passwordEncoder.encode(memberVO.getPassword()));
memberVO.setJoinType("Nomal");
if(memberVO.getMarketing()==null) {
memberVO.setMarketing(false);
}
int result =memberDAO.setMemberJoin(memberVO);
Map<String, Object> map = new HashMap<>();
map.put("roleId", 3);
map.put("memberId", memberVO.getId());
result = memberDAO.setMemberRole(map);
return result;
}
//패스워드가 일치하는지 검즈하는 메서드
public boolean memberCheck(MemberVO memberVO, BindingResult bindingResult)throws Exception{
boolean result=false;
//false : error가 없음 , 검증성공
//true : error가 실패 , 검증 실패
//1. annotation 검증 결과
result= bindingResult.hasErrors();
//2. password 일치 검증
if(!memberVO.getPassword().equals(memberVO.getPasswordCheck())) {
result=true;
bindingResult.rejectValue("passwordCheck", "member.password.notEqual");
}
//3. ID중복 검사
MemberVO checkMember = memberDAO.idDuplicateCheck(memberVO);
if(checkMember != null) {
result=true;
bindingResult.rejectValue("accountId", "member.id.duplicate");
}
return result;
}
public MemberVO idDuplicateCheck(MemberVO memberVO)throws Exception{
return memberDAO.idDuplicateCheck(memberVO);
}
}
@Controller
@RequestMapping("/member/*")
public class MemberController {
@Autowired
private MemberService memberService;
@Autowired
private MailManager mailManager;
@GetMapping("join")
public ModelAndView setMemberJoin(@ModelAttribute MemberVO memberVO) {
ModelAndView mv = new ModelAndView();
mv.setViewName("member/join");
return mv;
}
//회원가입
@PostMapping("join")
public ModelAndView setMemberJoin(ModelAndView mv, @Valid MemberVO memberVO, BindingResult bindingResult)throws Exception{
boolean check = memberService.memberCheck(memberVO, bindingResult);
if(check) {
mv.setViewName("member/join");
return mv;
}
int result = memberService.setMemberJoin(memberVO);
mv.setViewName("redirect:/");
return mv;
}
//로그인
@GetMapping("login")
public ModelAndView getLogin(HttpSession session)throws Exception{
ModelAndView mv = new ModelAndView();
Object obj =session.getAttribute("SPRING_SECURITY_CONTEXT");
if(obj==null) {
mv.setViewName("member/login");
}else {
mv.setViewName("redirect:/");
}
return mv;
}
//아이디 중복검사
@GetMapping("idDuplicateCheck")
@ResponseBody
public boolean idDuplicateCheck(MemberVO memberVO)throws Exception{
boolean check=false;
memberVO = memberService.idDuplicateCheck(memberVO);
if(memberVO == null) {
check=true;
}
return check;
}
}