이제까지 User 정보는 모두 InMemory 상태에서 처리를 하였음
하지만, 실제 개발 상황에서 이렇게 쓰지 않음
-> 대부분 Mysql이나 Oracle 과 같은 RDBMS를 쓰거나, MongoDB 와 같은 기타 데이터베이스를 사용해 사용자를 관리
스프링 시큐리티를 써서 서비스를 만들라고 하면, 대부분의 개발자들은 UserDetails
를 구현한 User 객체
와 UserDetailsService
부터 구현
-> 왜냐하면, UserDetailsService와 UserDetails 구현체만 구현하면 스프링 시큐리티가 나머지는 쉽게 쓸 수 있도록 도움을 준다
User Details Service가 빈으로 등록되어있으면 DaoAuthenticationProvider가 Bean에 UsernamePasswordAuthenticationToken을 넘겨서 User Details라는 Principal 객체로 받게 되어있다
Principal 객체를 가지고 Token에 의해 인증이 된 사용자라면 넣어서 return 하도록 구조화
web
: 웹 컨트롤러나 리소스를 여러 서버에서 공통으로 사용하는 경우 web 컴포넌트로 관리
comp
: 컴포넌트가 되는 모듈을 넣는다(여기에 User와 Authority 정의)
build.gralde에서 jpa 의존성 추가
SpAuthority.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name="sp_user_authority")
@IdClass(SpAuthority.class)
public class SpAuthority implements GrantedAuthority {
@Id
@Column(name = "user_Id")
private Long userId;
@Id
private String authority;
}
SpUser.java(UserDetails 구현)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name="sp_user_table")
public class SpUser implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
// 유저와 라이프 사이클이 같기에 ALL로 지정
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "user_Id", foreignKey = @ForeignKey(name = "user_id"))
private Set<SpAuthority> authorities;
private String email;
private String password;
private boolean enabled;
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return enabled;
}
@Override
public boolean isAccountNonLocked() {
return enabled;
}
@Override
public boolean isCredentialsNonExpired() {
return enabled;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
Repository
public interface SpUserRepository extends JpaRepository<SpUser, Long> {
Optional<SpUser> findSpUserByEmail(String email);
}
UserService
@Service
@Transactional
public class SpUserService implements UserDetailsService {
private final SpUserRepository spUserRepository;
public SpUserService(SpUserRepository spUserRepository) {
this.spUserRepository = spUserRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return spUserRepository.findSpUserByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username));
}
public Optional<SpUser> findUser(String email){
return spUserRepository.findSpUserByEmail(email);
}
public SpUser save(SpUser user){
return spUserRepository.save(user);
}
public void addAuthority(Long userId, String authority){
spUserRepository.findById(userId).ifPresent(user -> {
SpAuthority newRole = new SpAuthority(user.getUserId(), authority);
if(user.getAuthorities() == null){
HashSet<SpAuthority> authorities = new HashSet<>();
authorities.add(newRole);
user.setAuthorities(authorities);
save(user);
} else if(!user.getAuthorities().contains(newRole)){
HashSet<SpAuthority> authorities = new HashSet<>();
authorities.addAll(user.getAuthorities());
authorities.add(newRole);
user.setAuthorities(authorities);
save(user);
}
});
}
public void removeAuthority(Long userId, String authority){
spUserRepository.findById(userId).ifPresent(user -> {
if(user.getAuthorities()==null) return;
SpAuthority targetRole = new SpAuthority(user.getUserId(), authority);
if(user.getAuthorities().contains(targetRole)){
user.setAuthorities(
user.getAuthorities().stream().filter(auth -> !auth.equals(targetRole))
.collect(Collectors.toSet())
);
save(user);
}
});
}
}
서버 실행 시 실행되는 쿼리
Hibernate:
create table sp_user_authority (
user_id bigint not null,
authority varchar(255) not null,
primary key (user_id, authority)
)
Hibernate:
create table sp_user_table (
user_id bigint generated by default as identity,
email varchar(255),
enabled boolean not null,
password varchar(255),
primary key (user_id)
)
Hibernate:
alter table sp_user_authority
add constraint user_id
foreign key (user_id)
references sp_user_table
Security Config
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final SpUserService userService;
public SecurityConfig(SpUserService userService) {
this.userService = userService;
}
// 기존에 User Details Service를 구현해놓은 객체 인자로 사용
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
// DB에서 사용자를 편의상 넣었다 뺐다 하기 위해(테스트 시에만 사용하자)
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(request->
request.antMatchers("/").permitAll()
.anyRequest().authenticated()
)
.formLogin(login->
login.loginPage("/login")
.loginProcessingUrl("/loginprocess")
.permitAll()
.defaultSuccessUrl("/", false)
.failureUrl("/login-error")
)
.logout(logout->
logout.logoutSuccessUrl("/"))
.exceptionHandling(error->
error.accessDeniedPage("/access-denied")
)
;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.requestMatchers(
PathRequest.toStaticResources().atCommonLocations(),
// path: /h2-console 옵션을 사용하기 위해 Path를 열어야함
PathRequest.toH2Console()
)
;
}
}
application.yml
server:
port: 9055
spring:
devtools:
livereload:
enabled: true
restart:
enabled: true
# thymleaf을 모듈에서 발견하기 위해
thymeleaf:
prefix: classpath:/templates/
cache : false
check-template-location: true
h2:
console:
enabled: true
path: /h2-console
datasource:
url: jdbc:h2:mem:userdetails-test;
driverClassName: org.h2.Driver
username: sa
password:
build.gradle
apply from: "/Applications/dev/works/fastcampus/lec9/sp-fastcampus-spring-sec/server/web-common.gradle"
dependencies {
implementation("$boot:spring-boot-starter-data-jpa")
runtime("com.h2database:h2")
compile project(":comp-user-admin")
compile project(":web-user-admin")
}
comp에 구현해 놓은 User 객체를 서버에서 사용할 수 있도록 하였음
UserDetailsTestApplication.java
@SpringBootApplication(scanBasePackages = {
"com.sp.fc.user",
"com.sp.fc.web"
})
@EntityScan(basePackages = {
"com.sp.fc.user.domain"
})
@EnableJpaRepositories(basePackages = {
"com.sp.fc.user.repository"
})
public class UserDetailsTestApplication {
public static void main(String[] args) {
SpringApplication.run(UserDetailsTestApplication.class, args);
}
}
UserService 의존성 주입을 위한 스캔 범위를 Main Application에서 지정
기본적인 컨트롤러 구현
@Controller
public class HomeController {
@GetMapping("/")
public String main(){
return "index";
}
@GetMapping("/login")
public String login(){
return "loginForm";
}
@GetMapping("/login-error")
public String loginError(Model model){
model.addAttribute("loginError", true);
return "loginForm";
}
@GetMapping("/access-denied")
public String accessDenied(){
return "AccessDenied";
}
@PreAuthorize("hasAnyAuthority('ROLE_USER')")
@GetMapping("/user-page")
public String userPage(){
return "UserPage";
}
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
@GetMapping("/admin-page")
public String adminPage(){
return "AdminPage";
}
@ResponseBody
@GetMapping("/auth")
public Authentication auth(){
return SecurityContextHolder.getContext().getAuthentication();
}
}
insert
쿼리문을 통해 사용자 계정을 생성하고, 권한을 지정해주는 것이 가능
위처럼 권한 부여시 관리자페이지, 유저 페이지 둘다 접속 가능