All Articles

Spring Security in MVC

로그인

login → mainController → CurrentUser - Annotation → UserAccount

Authentication Components

SecurityContextHolder

// AccountService
public void login(Account account) {
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
        new UserAccount(account),
        account.getPassword(),
        List.of(new SimpleGrantedAuthority("ROLE_USER"))); // 권한 <- 정석 대로는 authenticationManager를 통해서 함

		SecurityContext context = SecurityContextHolder.getContext();
    context.setAuthentication(token);
}

// UserAccount.java
@Getter
public class UserAccount extends User {

    private Account account;

    public UserAccount(Account account) {
        super(account.getNickname(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER")));
        this.account = account;
    }
}

Authentication

The Authentication serves two main purposes within Spring Security:

  • An input to AuthenticationManager to provide the credentials a user has provided to authenticate. When used in this scenario, isAuthenticated() returns false.
  • Represents the currently authenticated user. The current Authentication can be obtained from the SecurityContext.

The Authentication contains:

  • principal - identifies the user. When authenticating with a username/password this is often an instance of UserDetails.
  • credentials - Often a password. In many cases this will be cleared after the user is authenticated to ensure it is not leaked.
  • authorities - the GrantedAuthority`s are high level permissions the user is granted. A few examples are roles or scopes.

https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-authentication-authentication

Test

  • 인증된 사용자로 기능 테스트하기
  • 테스트할 때 사용할 사용자를 만든다

@WithSecurityContext

  • 유연한 형태
  • SecurityContext 는 JUnit @Before 시점에 설정됨
// StudySettingsControllerTest.java

@Test
@WithAccount("jieun")
@DisplayName("스터디 소개 수정 폼 조회 - 실패 (권한 없는 유저)")
void updateDescriptionForm_fail() throws Exception {
        Account accountUnauthorized = accountFactory.createAccount("accountUnauthorized");
        Study study = studyFactory.createStudy("test-study", accountUnauthorized);
	mockMvc.perform(get("/new-study"))
	  .andExpect(status().isOk())
	  .andExpect(view().name("study/form"))
    .andExpect(model().attributeExists("account"))
    .andExpect(model().attributeExists("studyForm"));
}
// WithAccount.java
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAccountSecurityContextFactory.class)
public @interface WithAccount {
    String value();
}

// WithAccountSecurityContextFactory.java
@RequiredArgsConstructor
public class WithAccountSecurityContextFactory implements WithSecurityContextFactory<WithAccount> {
private final AccountService accountService;

@Override
public SecurityContext createSecurityContext(WithAccount withAccount) {
    String nickname = withAccount.value();

    SignUpForm signUpForm = new SignUpForm();
    signUpForm.setNickname("jieun");
    signUpForm.setEmail("jieun@icloud.com");
    signUpForm.setPassword("12341234");
    accountService.processNewAccount(signUpForm);

    UserDetails principal = accountService.loadUserByUsername(nickname);
    Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication((authentication));
    return context;
	}
}

@BeforeEach

// studyTest.java
@BeforeEach
void beforeEach() {
    study = new Study();
    account = new Account();
    account.setNickname("jieun");
    account.setPassword("12341234");
    userAccount = new UserAccount(account);
}

Persistent Login

refer to https://jieun.dev/posts/security-persistent-login