이제 로그인 절차를 위해 스프링 시큐리티를 사용해본다.
스프링 시큐리티외에 그냥 해도 되긴 하지만 (예전에 포폴은 그냥만든듯) 스프링 시큐리티를 사용해서 로그인 구현을 해보자.
User 관리도 로그인/로그아웃, 회원가입, 권한관리, 회원설정 변경등 다양한 기능이 있지만..
우선 로그인부터하고, 회원설정은 나중에 봐서 추가.
일단 스프링 시큐리티는 Spring Boot 들어오기전 앞단 Filter 에서 처리해준다고 한다.
이런 인증절차를 가지고 있고, 이 인증절차는
Spring MVC 앞에서 수행된다고 한다.
Spring Security 로그인 절차 등록
1. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
gradle 에 dependency 를 추가한다.
2. SecurityConfig.java 생성
SpringBoot 버전에 따라 SpringSecurity Config 설정 방법이 다르니 반드시 버전을 확인해야한다.
(나는 3.3.1 임)
com/example/post 아래 config 패키지(폴더)를 생성한다.
그 아래 SecurityConfig.java 파일 생성
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
UserDetailService userDetailService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login").permitAll()
// .requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/WEB-INF/jsp/login.jsp").permitAll()
.anyRequest().authenticated()
);
http
.formLogin((auth) -> auth
.loginPage("/login")
.loginProcessingUrl("/loginProc")
.defaultSuccessUrl("/", true)
.permitAll()
);
http
.csrf((auth) -> auth.disable());
return http.build();
}
1) 인증관련 서비스를 처리할 UserDetailService 를 Autowired 시켜준다.
2) filterChain 클래스를 만들어 Bean 으로 등록한다.
3) http 으로 시작해서 . 으로 각종 옵션을 등록하는데,
requestMatchers 를 통해 각종 pattern (URL) 을 등록할 수 있다. 또, /WEB-INF/jsp 와 같이 직접적인 웹페이지 경로를 등록할 수 도 있다.
permitAll() 은 권한이 없어도 모두 허용하겠다는 뜻이고, 주석처리된 hasRole의 경우 값으로 받는 권한(admin) 이 있는경우에만 허용한다는 뜻이다.
그 아래 formLogin 은 로그인에 관련된 설정인데, 로그인이 안된경우는 loginPage 로 이동시켜서 로그인을 수행시킨다.
csrf 는 웹 보안 취약점관련 옵션인데, 활성화하려면 추가설정이 필요해서 우선 disable
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
그 아래 passwordEncoder 를 등록하여 이후 DB와 연동 시 암호화 모듈을 세팅해준다.
3. User 관련 VO 객체들을 만들어준다.
User 객체
import lombok.Data;
@Data
public class User {
private String username;
private String password;
}
권한 Role 객체
@Data
public class Role {
private String roleId;
private String roleName;
}
4. 연동한 DB에도 해당 객체 테이블을 만들어준다.
CREATE TABLE user1 (
userId INT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL
);
CREATE TABLE role1 (
roleId INT PRIMARY KEY,
roleName VARCHAR(50) NOT NULL
);
CREATE TABLE role_user1 (
userId INT,
roleId INT,
PRIMARY KEY (userId, roleId),
FOREIGN KEY (userId) REFERENCES user1(userId),
FOREIGN KEY (roleId) REFERENCES role1(roleId)
);
role, user 테이블과 role과 user를 매핑해주는 role_user 테이블을 만들었다.
컬럼은 이후 추가될수도 있음..
처음엔 user 테이블에 role 을쓰면되지않나 싶어서 찾아보니 (user 컬럼으로 username, password, roleId 를 갖도록)
이렇게 테이블을 하나 더 만들어서 사용하는게 더 좋은구성이라고 한다.
5. DB 연동하기
우선 서비스는 UserDetailService 로 만들것이니. Dao 부터 작성해준다.
@Repository
public class UserDao {
@Autowired
SqlSessionTemplate sqlSession;
public User selectByUsername(String username) {
return sqlSession.selectOne("UserMapper.selectByUsername", username);
}
public List<Role> selectRoleByUsername(String username) {
return sqlSession.selectList("UserMapper.selectRoleByUsername", username);
}
}
우선 username 으로 User를 얻는 쿼리, username 으로 해당 유저가 가진 권한을 가진 쿼리 두개 만들어준다.
User Mapper 를 만들것이기에 앞에 UserMapper. 를 붙여준다.
resources/mapper 에 mapper-user.xml 파일을 작성한다.
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="UserMapper">
<select id="selectByUsername" parameterType="String" resultType="com.example.post.model.User">
SELECT * FROM USER1 WHERE username = #{username}
</select>
<select id="selectRoleByUsername" parameterType="String" resultType="com.example.post.model.Role">
SELECT r.*
FROM role1 r
JOIN role_user1 ru ON r.roleId = ru.roleId
WHERE ru.username = #{username}
</select>
</mapper>
selectRoleByUsername 에서 role 과 role_user1 테이블을 조인시켜 username 이 가지고있는 Role name 을 가져올 수 있도록 바꿔준다.
6. 인증 서비스 작성
com.example.post 아래 auth 라는 패키지(폴더)를 하나 생성한다.
그 아래 UserDetailService.java 라는 파일을 하나 만든다.
@Slf4j
@Service
public class UserDetailService implements UserDetailsService{
@Autowired
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.selectByUsername(username);
if(user == null){
log.debug("no Username " + username);
throw new UsernameNotFoundException(username);
}
List<Role> roleList = userDao.selectRoleByUsername(username);
//roleList -> Stream 으로 변환하여 각 요소에 대해 getRoleID을 호출하여 roleId 얻어냄
//그 결과를 toArray 해서 roles 에 넣음. String[]::new 는 Stream의 요소개수만큼 크기를 가진 String 배열을 생성하는 람다 표현식
//메서드 참조를 이용하여 간단하게 표현
String[] roles = roleList.stream().map(m -> m.getRoleName()).toArray(String[]::new);
CustomUserDetails customUserDetails = new CustomUserDetails(user, roles);
return customUserDetails;
}
}
아까 2번에서 Autowired 시킨 UserDetailService 이다.
implements 시키면 틀이 자동으로 짜짐.
우선 username 을 받아 방금 작성한 selectByUsername 을 통해 User 를 가져와 해당 User 가 존재하는지 확인한다.
없으면 Exception 으로 반환시킴.
그리고 Role 도 가져와서 String[] roles 에 권한들을 다 집어 넣어준다.
이후 User 와 String[] roles 를 이용해 CustomUserDetails (객체) 를 작성하여 반환시킨다.
7. CustomUserDetails.java
같은 Auth 에 CustomuserDetails.java 개체 생성
public class CustomUserDetails implements UserDetails {
private final String username;
private final String password;
private final String[] roles;
private final User user;
public CustomUserDetails(User user, String[] roles){
this.username = user.getUsername();
this.password = user.getPassword();
this.roles = roles;
this.user = user;
}
//사용자 권한 Return
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String role : roles){
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public User getUser(){
return this.user;
}
}
생성자를 통해 변수값을 초기화시켜줄 수 있는 객체 만들기
사용자 권한 Return은 저렇게해야한다고 함. Spring Security 방식이니 그냥 넘어가자.. 할게많아서 다 분석못함 ㅠ
8. User 관련 작업할 컨트롤러 생성
controller 패키지에 UserController.java 를 작성해준다.
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage() {
return "login";
}
}
내용은 크게 없고 일단 login page 로 이동시키는것만 만들어둠.
9. Login Page 작성
/WEB-INF/jsp 아래 login.jsp 파일 생성
><html>
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
login page
<hr>
<form action="/loginProc" method="post" name="loginForm">
<input id="username" type="text" name="username" placeholder="id"/>
<input id="password" type="password" name="password" placeholder="password"/>
<input type="submit" value="login"/>
</form>
</body>
</html>
절차를 아까 securiyConfig 에서 작성한 loginProc 으로 맞춰준다.
기동하여 로그인 확인
계정생성은 미리 DB에서 진행하였고 (admin/admin) 로그인 하여 /loginProc 로 전달된 모습을 확인할 수 있다.
여기서 주요사항이 있는데,
만약 나처럼 DB에서 아이디 패스워드를 직접 insert 로 집어넣은 경우 로그인이 되지않을 수 있다.
그 이유는 SecurityConfig 에 등록한
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
때문인데, 이 Bean 이 password 를 암호화하여 비교하기때문이다.
지금 내 코드에는 회원가입절차가 없어서 DB에 직접 넣었기에 DB에 직접넣을때도 암호화 절차를 거쳐서 진행해야 한다.
@Autowired
private PasswordEncoder passwordEncoder;
...
String encodedPassword = passwordEncoder.encode("admin");
System.out.println("암호화된 비밀번호: " + encodedPassword);
로그인 절차시 실행되는 곳에 (나는 UserController 의 /login 에 넣음) 해당 코드를 넣어 암호화된 admin 값을 DB에 새로 update 하니 잘됨.
내가 로그인했는지 확인하는 방법으로
System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());
해당코드로 현재 세션의 사용자 아이디를 가져올수있다. 나는 이걸로 확인했음.