빠르게 스프링 시큐리티를 적용시킬 때는 UserDetails와 UserDetailsService를 사용합니다.
왜냐하면 이 두개의 인터페이스만 구현하면 스프링 시큐리티가 나머지는 쉽게 사용할 수 있도록 도움을 주기 때문입니다.
그 이후 설정은 차차 적용시키면서 구현하는 편입니다. 다음은 UserDetails Interface입니다.
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
/**
* Provides core user information.
*
* <p>
* Implementations are not used directly by Spring Security for security purposes. They
* simply store user information which is later encapsulated into {@link Authentication}
* objects. This allows non-security related user information (such as email addresses,
* telephone numbers etc) to be stored in a convenient location.
* <p>
* Concrete implementations must take particular care to ensure the non-null contract
* detailed for each method is enforced. See
* {@link org.springframework.security.core.userdetails.User} for a reference
* implementation (which you might like to extend or use in your code).
*
* @author Ben Alex
* @see UserDetailsService
* @see UserCache
*/
public interface UserDetails extends Serializable {
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* Returns the password used to authenticate the user.
* @return the password
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return
* <code>null</code>.
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
boolean isAccountNonLocked();
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
boolean isEnabled();
}
먼저 그럼 User라는 Entity를 만들고 Userdetails를 구현한 구현체를 먼저 만들어 봅니다.
우선 GrantedAuthority상속받은 구현체가 컬렉션형태로 존재해야 하는데, 이것은 따로 권한 테이블로 빼겠습니다.
그리고 boolean return타입 메소드는 멤버 변수 enabled를 반환하는 것으로 구현합니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "user_id"))
private Set<UserGrantedAuthority> 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;
}
}
이제 GrantedAuthority를 구현한 구현체를 만듭니다.
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "user_authority")
@IdClass(UserGrantedAuthority.class)
public class UserGrantedAuthority implements GrantedAuthority {
@Id
@Column(name = "user_id")
private Long userId;
@Id
private String authority;
}
복합 키를 사용하기 위해 IdClass로 선언하겠습니다.
위에서 설정한 것처럼 외래 키는 userId이고 키 이름 또한 user_id이다
이제 data-jpa를 사용하기 위한 repository를 생성합니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findUserByEmail(String email);
}
이제 UserDetailsService를 구현한 구현체를 생성합니다.
@Service
@Transactional
public class UserService implements UserDetailsService {
@Autowired
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) {
return userRepository.findUserByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException(email));
}
}
빈을 DI받기 위해서 service도 빈으로 등록하며 DI 될 대상인 Repository도 빈으로 선언하였습니다.
application.yml 파일도 입맛대로 작성해서 시작했지만, 모듈을 재활용하는 식이라서 context의 빈을 클라이언트에서 못
찾는 에러가 발생했다. 그래서 cilent에서 User, UserService, UserReposiroty전부 스캔하도록 추가했다.
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private final UserService userService;
public SecurityConfig(UserService userService) {
this.userService = userService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService); //여기서 데이터베이스를 활용한 auth
}
@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(),
PathRequest.toH2Console() //h2console은 무시
)
;
}
}
'Spring|Spring-boot > Spring Security' 카테고리의 다른 글
SecurityContextPersistenceFilter (0) | 2022.10.30 |
---|---|
Spring Security Basic (0) | 2021.03.28 |
Spring Security Form Login (0) | 2021.03.28 |
Spring Security(2) (0) | 2021.03.21 |
Spring Security(1) (0) | 2021.03.21 |
댓글