회원 가입하기
회원가입 → DB저장 → 인증
스프링 시큐리티에선
1. 하나의 계정에 복수개의 권한 부여 가능. 즉! 계정:권한 => 1:N 관계
2. DB 저장시 password 는 반드시 ‘암호화’ 해서 저장되어야 한다
a. 암호화는 스프링 시큐리티의 PasswordEncoder 사용
3. 인증을 위해 DB 조회시 필요한 ‘동작’들이 제공되어야 한다.
* DB 테이블 작성
[회원계정] 1:n [권한]
ERD 폴더와 sql 생성
* UserDTO 생성
* DummyData 만들기
* 로그인 폼에 회원가입 폼 링크추가
loginForm.jsp
* /join 핸들러 추가
IndexController.java
@GetMapping("/join")
public String join() {
return "joinForm";
}
회원가입 폼 작성
{view_root}/joinForm.jsp 생성
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr>
<form action="/joinOk" method="POST">
<input type="text" name="id" placeholder="아이디 입력"/><br>
<input type="password" name="pw" placeholder="패스워드 입력"/><br>
<input type="email" name="email" placeholder="이메일 입력"/><br>
<input type="submit" value="회원가입">
</form>
</body>
</html>
* /joinOk 핸들러 추가
IndexController.java
@PostMapping("/joinOk")
public String joinOk(UserDTO user) {
System.out.println("/joinOk: " + user);
// password 는 암호화 하여 저장
String rawPassword = user.getPw();
String encPassword = passwordEncoder.encode(rawPassword);
user.setPw(encPassword);
userService.addMember(user);
return "redirect:/login";
}
* UserDAO.java 작성
{base-pacakge}/domain
인터페이스 작성
package com.lec.spring.domain;
import java.util.List;
public interface UserDAO {
// 사용자(user) 추가
int addUser(UserDTO user);
// 특정 id(username)의 사용자 에 권한(auth) 추가
int addAuth(String id, String auth);
// 사용자(user) 삭제
int deleteUser(UserDTO user);
// 특정 id(username)의 사용자의 특정 권한(auth) 삭제
int deleteAuth(String id, String auth);
// 특정 id(username)의 사용자의 권한(들) 전부 삭제
int deleteAuths(String id);
// 특정 id(username)의 사용자 조회
UserDTO findById(String id);
// 특정 id(username)의 사용자의 권한(들) 뽑기
List<String> selectAuthoritiesById(String id);
}
* UserDAO.xml
MyBatis 매퍼 파일
{base-pacakge}/domain
<?xml version="1.0" encoding="UTF-8"?>
http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lec.spring.domain.UserDAO">
<insert id="addUser" parameterType="com.lec.spring.domain.UserDTO"
flushCache="true">
INSERT INTO test_member2(mb_id, mb_pw, mb_email)
VALUES(#{id}, #{pw}, #{email})
</insert>
<insert id="addAuth" flushCache="true">
INSERT INTO test_authority2
VALUES(#{param1}, #{param2})
</insert>
<delete id="deleteUser" parameterType="com.lec.spring.domain.UserDTO"
flushCache="true">
DELETE FROM test_member2
WHERE mb_id = #{id}
</delete>
<delete id="deleteAuth" flushCache="true">
DELETE FROM test_authority2
WHERE mb_id = #{param1} AND mb_auth = #{param2}
</delete>
<delete id="deleteAuths" flushCache="true">
DELETE FROM test_authority2
WHERE md_id = #{param1}
</delete>
<select id="findById" resultType="com.lec.spring.domain.UserDTO">
SELECT
mb_uid uid,
mb_id id,
mb_pw pw,
mb_email email,
mb_enabled enabled,
mb_regdate regdate
FROM test_member2
WHERE mb_id = #{id}
</select>
<select id="selectAuthoritiesById" resultType="String">
SELECT mb_auth
FROM test_authority2
WHERE mb_id = #{id}
</select>
</mapper>
* UserDAOImpl.java 작성
{base-pacakge}/domain
package com.lec.spring.domain;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserDAOImpl implements UserDAO {
private UserDAO mapper;
@Autowired
public UserDAOImpl(SqlSession sqlSession) {
mapper = sqlSession.getMapper(UserDAO.class);
}
@Override
public int addUser(UserDTO user) {
return mapper.addUser(user);
}
@Override
public int addAuth(String id, String auth) {
return mapper.addAuth(id, auth);
}
@Override
public int deleteUser(UserDTO user) {
return mapper.deleteUser(user);
}
@Override
public int deleteAuth(String id, String auth) {
return mapper.deleteAuth(id, auth);
}
@Override
public int deleteAuths(String id) {
return mapper.deleteAuths(id);
}
@Override
public UserDTO findById(String id) {
return mapper.findById(id);
}
@Override
public List<String> selectAuthoritiesById(String id) {
return mapper.selectAuthoritiesById(id);
}
}
* UserService.java 작성
{base-pacakge}/service
package com.lec.spring.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.lec.spring.domain.UserDAO;
import com.lec.spring.domain.UserDTO;
@Service
public class UserService {
@Autowired
UserDAO dao;
// 회원가입
// ROLE_MEMBER 권한 부여
@Transactional
public int addMember(UserDTO user) {
int cnt = dao.addUser(user);
dao.addAuth(user.getId(), "ROLE_MEMBER");
return cnt;
}
// 회원삭제
@Transactional
public int deleteMember(UserDTO user) {
dao.deleteAuths(user.getId()); // 권한(들) 먼저 삭제 (차라리 DDL 에 ON DELETE CASCADE 하자..)
int cnt = dao.deleteUser(user);
return cnt;
}
// 특정 id(username) 의 정보 가져오기
public UserDTO findById(String id) {
return dao.findById(id);
}
// 특정 id 의 권한(들) 정보 가져오기
public List<String> selectAuthoritiesById(String id){
return dao.selectAuthoritiesById(id);
}
}
* PasswordEncoder
스프링 시큐리티는 기본적으로 PasswordEncoder 객체를 통해 ‘password’ 는 반.드.시 암호화 하여 다루도록 되어 있다.
public class SecurityConfig extends WebSecurityConfigurerAdapter{
// PasswordEncoder 를 bean 으로 IoC 에 등록
// IoC 에 등록된다, IoC 내에선 어디서든 가져다가 사용할수 있다.
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
* IndexController 수정
@Controller
public class IndexController {
@Autowired
UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
* 로그인 진행, DB를 통한 인증
UserDetails 작성
{bast-package}/config/PrincipalDetails.java
package com.lec.spring.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.lec.spring.domain.UserDTO;
import com.lec.spring.service.UserService;
//시큐리티가 /loginOk 주소요청이 오면 낚아채서 로그인을 진행시킨다.
//로그인 진행이 완료되면 '시큐리티 session' 에 넣어주게 된다.
//우리가 익히 알고 있는 같은 session 공간이긴 한데..
//시큐리티가 자신이 사용하기 위한 공간을 가집니다.
//=> Security ContextHolder 라는 키값에다가 session 정보를 저장합니다.
//여기에 들어갈수 있는 객체는 Authentication 객체이어야 한다.
//Authentication 안에 User 정보가 있어야 됨.
//User 정보 객체는 ==> UserDetails 타입 객체이어야 한다.
//따라서 로그인한 User 정보를 꺼내려면
//Security Session 에서
// => Authentication 객체를 꺼내고, 그 안에서
// => UserDetails 정보를 꺼내면 된다.
public class PrincipalDetails implements UserDetails {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
private UserDTO user;
public UserDTO getUser() {
return user;
}
public PrincipalDetails(UserDTO user) {
System.out.println("PrincipalDetails(user) 생성: " + user);
this.user = user;
}
// 해당 User 의 '권한(들)'을 리턴
// 현재 로그인한 사용자의 권한정보가 필요할때마다 호출된다. 혹은 필요할때마다 직접 호출해 사용할수도 있다
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
System.out.println("getAuthorities() 호출");
Collection<GrantedAuthority> collect = new ArrayList<>();
List<String> list = userService.selectAuthoritiesById(user.getId()); // DB 에서 권한들 읽어옴.
for(String auth : list) {
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return auth;
}
@Override
public String toString() {
return auth;
}
});
}
return collect;
}
// 로그인 한 사용자의 password 는?
@Override
public String getPassword() {
return user.getPw();
}
// 로그인 한 사용자의 username 은?
@Override
public String getUsername() {
return user.getId();
}
// 계정이 만료된건 아닌지?
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠긴건 아니지?
@Override
public boolean isAccountNonLocked() {
return true;
}
// 계정 credential 이 만료된건 아니지?
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 활성화 되었니?
@Override
public boolean isEnabled() {
if("1".equals(user.getEnabled())) return true;
return false;
}
}
* UserDetailsService 생성
{base-package}/config/PrincipalDetailsService.java
package com.lec.spring.config;
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 com.lec.spring.domain.UserDTO;
import com.lec.spring.service.UserService;
//UserDetailsService
//컨테이너에 등록한다.
//시큐리티 설정에서 loginProcessingUrl(url) 로 걸어 놓았기 때문에
//로그인시 위 url 로 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어 있는
//loadUserByUsername() 가 실행되고
//인증성공하면 결과를 UserDetails 로 리턴
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
// UserDetails 를 리턴한다, --> 누구한테 리턴하나?
// 시큐리티 session ( <= Authentication( <= 리턴된 UserDetails ) )a
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("loadUserByUsername(" + username + ")");
UserDTO user = userService.findById(username); // DB 조회
// 해당 username 의 user 가 DB 에 있었다면
// UserDetails 을 생성하여 리턴
if(user != null) {
PrincipalDetails details = new PrincipalDetails(user);
details.setUserService(userService);
return details;
}
// 해당 username 의 user 가 없다면!
// UsernameNotFoundException을 throw 해주도록 한다
throw new UsernameNotFoundException(username);
}
}
sesstion attribute 에 SPRING_SECURITY_CONTEXT 라는 이름으로 현재 로그인 정보가 담겨 있다.
그리고 그 정보는 Authentication 객체다.
'Spring' 카테고리의 다른 글
[SpringBoot] Security (0) | 2022.05.16 |
---|---|
[SpringBoot] MyBatis (0) | 2022.05.13 |
[Spring Boot] Validation (0) | 2022.05.12 |
[SpringBoot] Request Parameter (0) | 2022.05.12 |
[SpringBoot] RequestMapping (0) | 2022.05.12 |