티스토리 뷰
보통 로그인부를 직접 만들어 써도 되지만
Spring 에는 security 라는 아주 좋은 인증 기능이 있기 때문에 보통 이것을 사용해왔다.
최근 security 쪽 코드를 개선하다가 사용법을 남겨보려고 한다.
Springboot gradle 에 관련 라이브러리를 작성합니다.
설정에 필요한 라이브러리만 일단 작성해줍시다.
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
이제 securityConfig 관련 클래스를 작성해야하는데
기존에는 WebSecurityConfigurerAdapter 를 상속받아 사용했었는데...
해당 클래스가 deprecated 되었고,
spring-security 5.x 버전인가 부터 SecurityFilterChain 을 Bean으로 등록해서 사용하라고 변경되었습니다.
오히려 간편해진 것 같습니다. (제 체감상...)
공식문서 참조
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/", "/profile").permitAll()
.antMatchers("/images/**", "/js/**", "/css/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/login-Proc")
.usernameParameter("id")
.defaultSuccessUrl("/home")
.permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
return http.build();
}
}
라인들을 간략히 설명해보자면,
@Configuration //-> 해당 클래스를 빈으로 수동 등록
@EnableWebSecurity //-> security로 사용
// 본인 환경에 맞춰 작성하시면 됩니다.
.csrf().disable() //-> csrf 사용안함 (지금 단계에서 불필요)
.headers().frameOptions().sameOrigin() //-> 동일 도메인 iframe 접근 허용
.antMatchers("/", "/profile").permitAll() //-> 해당 주소 접근 모두 허용
.anyRequest().authenticated() //-> 그 외 요청 인증필요
.formLogin() //-> 폼 로그인 사용
.loginPage("/") //-> 커스텀 로그인 페이지 (이거 없으면 security default 폼 나옴)
.loginProcessingUrl("/login-Proc") //-> 로그인 프로세스 url (form action 값 지정)
.usernameParameter("id") //-> form의 name 값 지정 (default가 username임)
.defaultSuccessUrl("/home") //-> 성공시 이동할 url (이외에 추가 처리사항이 있으면 핸들러로 보내도 됨)
위와 같이 설정한대로 html 에서 form 하나를 작성 합니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<head>
<title>LOGIN</title>
<link rel="stylesheet" href="/css/login.css" />
</head>
<body>
<div layout:fragment="content">
<div id="wrap" class="back04">
<div class="login_box">
<div class="login_group">
<form method="post" action="/login-Proc">
<ul class="login_form">
<li class="form_box">
<h2 class="id">아이디</h2>
<input type="text" name="id" placeholder="ID를 입력하세요." />
</li>
<li class="form_box">
<h2 class="pw">패스워드</h2>
<input type="text" name="password" placeholder="비밀번호를 입력하세요."/>
</li>
<li>
<button type="submit" class="btn_login">로그인</button>
</li>
</ul>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
security config 클래스 설정에 따라 action값과 name 값을 맞춰주면 됩니다.
디폴트 설정을 이용해도 상관없습니다.
<form method="post" action="/login-Proc">
<input type="text" name="id" placeholder="사번을 입력하세요." />
이제 사용자 화면에서 값을 받아 왔으니 인증처리 부를 구현해줘야합니다.
UserDetailsService 인터페이스를 구현해줘야 합니다.
가입자 데이터를 조회할 엔티티와 DTO 클래스를 만들어 줍시다.
엔티티는 DB 테이블 명세대로 작성했고
@Entity
@Table(name="MEMBER")
@Getter
@NoArgsConstructor
public class MemberEntity {
@Id
@Column(name="SABUN")
private String id;
@Column(name="PASSWORD")
private String password;
@Column(name="MEMBER_NAME")
private String memberName;
@Column(name="EMAIL")
private String email;
@Column(name="REGDATE")
private Date regDate;
@Column(name="STATE")
private String state;
@OneToMany(mappedBy = "member")
private Set<GroupUserEntity> groups = new HashSet<>();
/**
*
* @description 패스워드 저장시 자동 암호화
*/
public MemberEntity encodePassword(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
return this;
}
}
DB데이터를 조회 후 DTO에 담아 인증받기 위해 UserDetails 인터페이스를 구현해줬습니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberDTO implements UserDetails {
private static final long serialVersionUID = -8360090276325048821L;
private String id;
private String password;
private String memberName;
private String email;
private Date regDate;
private String state;
private List<Roles> Roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleKey()))
.collect(Collectors.toCollection(ArrayList::new));
}
@Override
public String getPassword() { return this.password; }
@Override
public String getUsername() { return this.id; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
위에 DTO에서 Roles는 뭔데 저렇게 넣었어? 라고 하실까봐
저는 DB에 권한값이 숫자로 구분되어 있어서 Roles를 enum 클래스로 작성하여 관리하려 합니다.
(이건 안따라하셔도 됨...)
@Getter
@AllArgsConstructor
public enum Roles {
ADMIN("ROLE_ADMIN", "관리자", "10"),
NOMAL("ROLE_NOMAL", "일반", "20"),
GUEST("ROLE_GUEST", "게스트", "30"),
;
private String roleKey;
private String roleName;
private String roleValue;
public static Roles findByRoleValue(String value) {
return Arrays.stream(Roles.values())
.filter(role -> role.getRoleValue().equals(value))
.findFirst()
.orElse(GUEST);
}
}
이제 JPA를 통해 사용자 정보를 조회하는 repository를 작성하고
public interface LoginRepository extends JpaRepository<MemberEntity, String> {
@Query("select m from MemberEntity m where m.id = :sabun")
Optional<MemberEntity> findUsers(@Param("sabun") String sabun);
}
UserDetailsService 인터페이스를 구현한 Service를 작성합니다.
조회된 값을 DTO에 Mapstruct mapper로 변환해서 넣어주고
권한값을 enum 클래스의 메소드를 통해 담아주도록 하였습니다.
@Service
public class LoginService implements UserDetailsService {
@Autowired private PasswordEncoder passwordEncoder;
@Autowired private LoginToMapper loginToMapper;
@Autowired private LoginRepository loginRepository;
/**
* @description spring-security user check
*/
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
Optional<MemberEntity> memberData = loginRepository.findUsers(id);
if(memberData.isPresent()) {
MemberDTO member = loginToMapper.toDTO(memberData.get());
List<Roles> roles = new ArrayList<>();
// 여기서 각자에 맞게 권한리스트를 set 해주면 됩니다.
memberData.get().getGroups().forEach(list -> {
roles.add(Roles.findByRoleValue(list.getGroupUserId().getGrpCd()));
member.setRoles(roles);
});
return member;
} else {
// member state false 처리
throw new UsernameNotFoundException("User is not Auth");
}
}
}
여기까지 진행해서 로그인을 해보면
MemberDTO 클래스에 로그인 유저정보가 담겨있는 디버깅해보면 알 수 있고
해당 값을 사용해야 할때 @AuthenticationPrincipal 어노테이션으로 가져와서 사용하면 됩니다.
@GetMapping("/home")
public String home(Model model, @AuthenticationPrincipal MemberDTO member) {
model.addAttribute("member", member);
return "main/main";
}
이렇게만 설정하면 서버 세션방식으로 인증하는 상태이고
securityConfig 에 jwt 를 붙여서 토큰으로 인증받게끔 커스텀해도 되고
redis 연결해서 세션을 redis 에 담게 처리해도 됩니다.
사용자 인증을 어떻게 할 것이냐에 따라서 다양한 방법들이 있기 때문에...
일단 여기까지 제가 개발중인 프로젝트 기준으로 작성해서 아마 기본적인 것보다 쫌 더 뭔가 들이 더 들어가 있는데
빼고 하셔도 무방합니다.
'Java > SpringBoot&Spring' 카테고리의 다른 글
[Spring] ObjectMapper, java8 LocalDateTime 유형 직렬화/역직렬화 오류 (feat.Jackson) (0) | 2023.06.08 |
---|---|
[Spring] IBsheet8 데이터처리 공통 제네릭 메소드 작성. (0) | 2023.06.07 |
[Spring] 세션(Session) Redis에 저장하기 (feat.docker Redis) (0) | 2023.04.03 |
[SpringBoot] P6Spy query logging 사용하기 (0) | 2023.02.14 |
[SpringBoot] Security 적용시 swagger 샘플페이지로 나오는 현상 (petstore.swagger.io/v2/swagger.json) (0) | 2023.01.18 |