기존에 세션을 가지고 로그인을 구분했습니다. OAuth2를 적용하고 나니 시큐리티 영향인지 기존에 로그인이 안되었습니다.
그래서 내가 만든 컨트롤러에 인증 허가만 내어주면 되는거 아닌가? 라는 생각의 시작이였습니다.
하지만 생각보다 시큐리티는 복잡했습니다.
//인증 매니저 Bean으로 생성
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//커스텀한 인증 방식 Override
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**","/fonts/**","/img/**","/vendor/**", "/js/**","/product/images/**").permitAll() // 허용 파일 범위
.antMatchers("/shop/**").permitAll()
.antMatchers("/members/add").permitAll()
.antMatchers("/loginMember").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/product/**").hasRole(Role.GUEST.name()) // guest 사용자만 들어갈 수 있따. 들어온 String 뒤에 자동으로 _ROLE 붙여준다.
.antMatchers("/members/**").hasRole(Role.GUEST.name()) // username이 있는 사용자만 들어갈 수 있따.
.antMatchers("/chat/**").hasRole(Role.GUEST.name()) // username이 있는 사용자만 들어갈 수 있따.
.anyRequest()
.authenticated()
.and()
.formLogin()//form으로 로그인 하려면 필수
.loginPage("/loginMember") // 로그인 하려는 페이지
.loginProcessingUrl("/login")//스프링 시큐리티가 해당 주소로 요청오는 로그인정보를 가로채서 loadUserByName으로 던짐
.loginProcessingUrl("/") //정상일때
.usernameParameter("loginId") //스프링 시큐리티 username을 loginId 로 받게끔
.and()
.logout()
.logoutSuccessUrl("/")//로그아웃이 성공하면 가는 곳
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
다른 분들이 만든 config에 주의할점이 있습니다.
'나는 기존에 form으로 로그인처리를 했는데 그냥 인증만 해주면 안되나?'라고 생각했습니다.
그걸 구현하는 방법은 formLogin()을 사용해서 '나는 form으로 로그인 처리를 할 꺼야' 라고 시큐리티에게 알려줍니다.
loginPage()는 내가 만든 컨트롤러에 연결이 됩니다.
커스텀한 UserDetailService도 구현해야 합니다.
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return
Optional.ofNullable(memberRepository.findByLoginId(username))
.filter(m -> m!= null)
.map(m -> new SecurityMember(m.get())).get();
}
}
멤버repo에서 id가 있다면 member객체를 반환하고 아니면 null을 반환합니다.
@GetMapping("/loginMember")
public String loginForm(@ModelAttribute("loginFormDto") LoginFormDto loginFormDto) {
return "/login";
}
저는 GET방식으로 loginMember가 요청되면 login.html 페이지로 가게끔 했습니다.
loginProcessingUrl은 시큐리티가 로그인 정보를 가로채서 loadUserByName이란 곳으로 가는 곳입니다.
즉 내가 기존에 있는 POST요청을 써놓으면 됩니다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginFormDto") LoginFormDto loginFormDto,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request,
Model model) {
if (bindingResult.hasErrors()) {
return "login";
}
Members loginMember = loginService.login(loginFormDto.getLoginId(), loginFormDto.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login";
}
HttpSession session = request.getSession();
//쿠키 이름: jsessionid, 값: uuid, uuid를 통해 session 속성에 접근
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
//권한 부여: memberDTO의 getAuthorities() 메소드에서 담당.
List<GrantedAuthority> roles = (List<GrantedAuthority>) loginMember.getAuthorities();
// 스프링 시큐리티 내부 클래스로 인증 토큰 생성
UsernamePasswordAuthenticationToken LoginSuccessToken = new UsernamePasswordAuthenticationToken(loginMember.getLoginId(),
loginMember.getPassword(), roles);
// 시큐리티 컨텍스트 객체를 얻습니다.
SecurityContext context = SecurityContextHolder.getContext();
// 강제로 인증객체에 ROLE=ROLE_GUEST 주입
context.setAuthentication(LoginSuccessToken);
return "redirect:" + redirectURL;
}
여기 SecurityContext라는 객체의 Authentication만 내가 원하는 ROLE로 바꿔두면 되었습니다.
usernameParameter()는 시큐리티는 기본적으로 form 안에 name이 username인 것을 id로 인식합니다.
이걸 내가만든 id 태그로 커스텀하기 위해 적어줘야 합니다.
저는 loginId라는 파라미터로 form 데이터를 받으니 '시큐리티 너도 loginId를 id로 인식해라, username 말고' 라고 말합니다.
시큐리티가 인증된 데이터를 주는 Member 객체입니다.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@BatchSize(size = 100)
@Slf4j
public class Members implements UserDetails{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(nullable = false, unique = true)
private String loginId;
@Column
private String password;
@Column(nullable = false)
private String name;
@Column(nullable = false) // OAuth2로 들어오는 정보
private String email;
@Column
private String picture; // // OAuth2로 들어오는 정보
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role; // // OAuth2로 들어오는 정보
private Integer ranks;
private Integer numberPurchase;
private LocalDate createDate = LocalDate.now();
@OneToMany(mappedBy = "members", fetch = FetchType.EAGER)
private List<Product> productList = new ArrayList<Product>();
@Builder
public Members(String name,
String password,
String email,
String picture,
Role role,
Integer ranks,
Integer numberPurchase) {
this.loginId = email;
this.password = password;
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
this.ranks = ranks;
this.numberPurchase = numberPurchase;
this.createDate = LocalDate.now();
this.productList = new ArrayList<Product>();
}
public Members update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
//로그인 할 때 권한 부여("ROLE_GUEST")로
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
auth.add(new SimpleGrantedAuthority("ROLE_GUEST"));
return auth;
}
@Override
public String getUsername() {
return this.loginId;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails를 상속받아서 Override해주는 메서드들이 많습니다.
UserDetails와 UserDetailService를 이용해서 로그인 컨트롤러를 지나기 전에 인증 허가를 받을 수 있게 됩니다.
'JAVA > - Spring' 카테고리의 다른 글
[Spring Security] OAuth2, AWS ec2 연동 (0) | 2022.09.12 |
---|---|
[Spring security] 권한 설정 해보기, 권한 표현식 알아보기 (0) | 2022.09.11 |
[Spring boot] Spring boot 에서 외부 Api(네이버 쇼핑Api) 사용해보기 (0) | 2022.07.24 |
@EntityGraph에 대해 알아보자 (0) | 2022.06.28 |
[spring boot] @ManyToOne에서 데이터 선택적으로 가져오기 (0) | 2022.06.27 |