스프링 시큐리티는 DelegatingFilterProxy를 사용해 추상화된 필터를 사용합니다.
그중 Form으로 Login 하는 경우를 살펴봅니다.
Form으로 login을 하는경우에는 UsernamePasswordAuthenticationFilter를 사용합니다.
이때 Token을 발급해주며 해당 토큰으로 원하는 페이지에 접근할 수 있습니다.
보통 새로운 SecurityConfig클래스에 WebSecurityConfigurerAdapter를 상속받아서 security의 설정 파일을 만들어 줍니다.
dependencies {
compile 'org.springframework.security:spring-security-web:4.2.2.RELEASE'
compile 'org.springframework.security:spring-security-config:4.2.2.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.1.8.RELEASE'
}
스프링 혹은 스프링 부트에 맞는 자신에게 적합한 라이브러리를 추가해주어야합니다.
스프링 시큐리티를 적용했다면 자신이 만들지 않은 login창이 나타나는데 이것은 DefaultLoginPageGenerationFilter에 의해 자동적인 로그인 폼페이지로 리다이렉트 되기 때문입니다.
이 필터는 GET/login을 처리하며 별도의 로그인 페이지를 설정하지 않은 경우 제공되는 필터입니다.
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
String errorMsg = "Invalid credentials";
if (loginError) {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials";
}
}
String contextPath = request.getContextPath();
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n");
sb.append("<html lang=\"en\">\n");
sb.append(" <head>\n");
sb.append(" <meta charset=\"utf-8\">\n");
sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
sb.append(" <meta name=\"description\" content=\"\">\n");
sb.append(" <meta name=\"author\" content=\"\">\n");
sb.append(" <title>Please sign in</title>\n");
sb.append(" <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" "
+ "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
sb.append(" <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" "
+ "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
sb.append(" </head>\n");
sb.append(" <body>\n");
sb.append(" <div class=\"container\">\n");
if (this.formLoginEnabled) {
sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath
+ this.authenticationUrl + "\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n");
sb.append(" <label for=\"username\" class=\"sr-only\">Username</label>\n");
sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter
+ "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
sb.append(" </p>\n");
sb.append(" <p>\n");
sb.append(" <label for=\"password\" class=\"sr-only\">Password</label>\n");
sb.append(" <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter
+ "\" class=\"form-control\" placeholder=\"Password\" required>\n");
sb.append(" </p>\n");
sb.append(createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request));
sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
sb.append(" </form>\n");
}
if (this.openIdEnabled) {
sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath
+ this.openIDauthenticationUrl + "\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n");
sb.append(" <label for=\"username\" class=\"sr-only\">Identity</label>\n");
sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter
+ "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
sb.append(" </p>\n");
sb.append(createRememberMe(this.openIDrememberMeParameter) + renderHiddenInputs(request));
sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
sb.append(" </form>\n");
}
if (this.oauth2LoginEnabled) {
sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
sb.append(createError(loginError, errorMsg));
sb.append(createLogoutSuccess(logoutSuccess));
sb.append("<table class=\"table table-striped\">\n");
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : this.oauth2AuthenticationUrlToClientName
.entrySet()) {
sb.append(" <tr><td>");
String url = clientAuthenticationUrlToClientName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());
sb.append(clientName);
sb.append("</a>");
sb.append("</td></tr>\n");
}
sb.append("</table>\n");
}
if (this.saml2LoginEnabled) {
sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
sb.append(createError(loginError, errorMsg));
sb.append(createLogoutSuccess(logoutSuccess));
sb.append("<table class=\"table table-striped\">\n");
for (Map.Entry<String, String> relyingPartyUrlToName : this.saml2AuthenticationUrlToProviderName
.entrySet()) {
sb.append(" <tr><td>");
String url = relyingPartyUrlToName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue());
sb.append(partyName);
sb.append("</a>");
sb.append("</td></tr>\n");
}
sb.append("</table>\n");
}
sb.append("</div>\n");
sb.append("</body></html>");
return sb.toString();
}
다음과 같이 미리 page가 만들어져 있는 것을 확인할 수 있는데, 잘 보면 if문에 OAuth2 / OpenID /Saml2 로그인과도 사용할 수 있는 것을 확인할 수 있습니다.
LogoutFilter 또한 마찬가지입니다. DefaultLogoutPageGeneratingFilter가 요청을 가로채어 GET /logout을 처리하며 UI 또한 제공해줍니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(request -> {
request
.antMatchers("/").permitAll()
.anyRequest().authenticated();
})
.formLogin(
login -> login.loginPage("/login")
.permitAll()
)
}
configure 메서드의 HttpSecurity를 인자로 받는 메서드는 스프링 시큐리티의 규칙을 정의합니다.
이때는 UsernamePasswordAuthenticationFilter가 사용되는데, POST /login을 처리하며 processingUrl을 변경하면 해당 주소를 변경할 수 있습니다.
UsernamePasswordAuthenticationFilter : username, password를 사용하는 form기반 인증을 처리하는 필터입니다.
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
- AuthenticationManager를 통한 인증 실행
- 성공하면, Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 실패하면 AuthenticationFailureHandler 실행
이 필터는 id와 password를 가지고 Token을 생성하며 해당 토큰으로 ProviderManger에게 인증을 진행하도록 해당 토큰을 넘겨주는 것을 볼 수 있습니다.
- AuthenticationManager (interface)
: Authentication 객체를 받아 인증하고 인증되었다면 인증된 Authentication 객체를 돌려주는 메서드를 구현하도록 하는 인터페이스다.
이 메서드를 통해 인증되면 isAuthenticated(boolean) 값을 TRUE로 바꿔준다.
-ProviderManager (class)
: AuthenticationManager의 구현체로 스프링에서 인증을 담당하는 클래스로 볼 수 있다. 스프링 빈으로 등록이 되어서 제공된다.
-AuthenticationProvider
: 인증과정이 이 메서드를 통해서 진행된다.
위 과정을 글로 풀면 다음과 같습니다. 해당 토큰이 구현체인 ProviderManager에게 전달됩니다. 그러면 이 매니저는 AuthenticationProvider 목록을 여러 개 가지고 있는데 이 토큰을 주면서 true를 리턴해주는 provider의 authenticate() 메서드를 실행하여 인증을 진행합니다.
AuthenticationProvider 은 인터페이스이기 때문에 내가 usernamepasswordToken을 처리할 클래스를 생성해서 처리해 줄 수 도 있다.
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
그러면 반복문을 돌다가 내가 만든 토큰 처리 클래스가 true를 반환하고 만든 토큰을 돌려주게 할 수 있습니다.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userId = authentication.getName();
String userPassword = authentication.getCredentials().toString(); //userPassword
내가 처리하고 토큰을 리턴
return new myToken(userId, userPassword, null, user);
}
또한 이러한 인증 과정 중에 내가 ip 나 브라우저 세션 아이디 등을 부가적으로 얻을 수 있는 방법이 존재하는데
authenticationDetailsSource()를 사용하여 처리할 수 있습니다.
이 메서드는 details를 생성하여 Authentication 객체의 멤버에 넣어 줄 수 있습니다.
@Component
public class CustomAuthDetails implements AuthenticationDetailsSource<HttpServletRequest,RequestInfo> {
//login
@Override
public RequestInfo buildDetails(HttpServletRequest context) {
return RequestInfo.builder()
.remoteIp(context.getRemoteAddr())
.sessionId(context.getSession().getId())
.loginTime(LocalDateTime.now())
.build();
}
}
인터페이스를 impl 하고 첫 번 째는 HttpServletRequest를 2번째는 내가 필요할 정보를 가질 VO를 생성하여 넣어주고
buildDetails를 오버라이드 하여 request에서 정보를 뽑을 수 있습니다.
또한 Resource파일은 이 인증절차를 무시하고 진행해야 하기 때문에 다음과 같이 진행할 수 있습니다.
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.requestMatchers(
PathRequest.toStaticResources().atCommonLocations()
);
}
뭐 실제로 antMatcher를 사용해 특정 주소를 추가할 수 있는데, 이것은 src/main/resource/static내의 자원들에 대해서 ignoring 됩니다.
만약 ROLE에 따른 요청 주소가 존재한다면 Controller에서 다음과 같은 설정을 진행할 수 있습니다.
@PreAuthorize("hasAnyAuthority('ROLE_USER')") 이후 config파일에 다음과 같은 어노테이션을 추가합니다.
@EnableGlobalMethodSecurity(prePostEnabled = true)
이 경우에 불편한 점은 관리자가 유저의 정보에 접근을 못한다는 것인데, 이때는 RoleHierarchy설정을 통한 변경이 가능합니다.
@Bean
public RoleHierarchy roleHierarchy(){
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return roleHierarchy;
}
'Spring|Spring-boot > Spring Security' 카테고리의 다른 글
Spring Security(4) (0) | 2021.04.04 |
---|---|
Spring Security Basic (0) | 2021.03.28 |
Spring Security(2) (0) | 2021.03.21 |
Spring Security(1) (0) | 2021.03.21 |
Spring Security (0) | 2021.03.21 |
댓글