본문 바로가기

JAVA/- Spring

[Spring security] 이미 만들어 놓은 로그인 시스템에 시큐리티 적용하기

기존에 세션을 가지고 로그인을 구분했습니다. 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를 이용해서 로그인 컨트롤러를 지나기 전에 인증 허가를 받을 수 있게 됩니다.

 

 

728x90