취미로 음악을 하는 개발자

[Spring Boot] Security Form, Status Check 본문

공대인/Spring[Boot]

[Spring Boot] Security Form, Status Check

영월특별시 2019. 8. 24. 01:27
728x90

프로젝트 생성




코드 구현


* 이전 프로젝트의 파일을 그대로 복사하되 아래 있는 파일들이 추가 및 수정되었다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.study.springboot.auth;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/css/**""/js/**""/img/**").permitAll()
                .antMatchers("/guest/**").permitAll()
                .antMatchers("/member/**").hasAnyRole("USER""ADMIN")
                .anyRequest().authenticated();
        
        http.formLogin()
                .loginPage("/loginForm")        // default : /login
                .loginProcessingUrl("/j_spring_security_check")
                .failureUrl("/loginError")        // default : /login?error
                //.defaultSuccessUrl("/")
                .usernameParameter("j_username")// default : j_username
                .passwordParameter("j_password")// default : j_password
                .permitAll();
        
        http.logout()
                .logoutUrl("/logout"// default
                .logoutSuccessUrl("/")
                .permitAll();
        
        // ssl을 사용하지 않으면 true로 사용
        http.csrf().disable();
    }
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password(passwordEncoder().encode("1234")).roles("USER")
            .and()
            .withUser("admin").password(passwordEncoder().encode("1234")).roles("ADMIN");
            // ROLE_ADMIN 에서 ROLE_는 자동으로 붙음
    }
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
 
cs


[25~32번 줄]

원래 loginPage의 디폴트는 "/login"인데 여기서는 "/loginForm"으로 바꿈

그리고 Security 프레임워크가 처리하는 url 그대로인 "/j_spring_security_check"로 하고

로그인 실패했을 때는 "/loginError"를 호출

jsp의 form에서 "j_username"과 "j_password"의 값을 ID와 패스워드로 지정

로그인은 누구나 할 수 있으므로 permitAll()


[34~37번 줄]

로그아웃 요청은 디폴트 값인 "/logout"으로, 작업이 성공적으로 끝나면 루트 페이지로 감



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.study.springboot;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
 
@Controller
public class MyController {
 
    @RequestMapping("/")
    public @ResponseBody String root() throws Exception {
        return "Security";
    }
    
    @RequestMapping("/guest/welcome")
    public String welcome1() {
        return "guest/welcome1";
    }
    
    @RequestMapping("/member/welcome")
    public String welcome2() {
        return "member/welcome2";
    }
    
    @RequestMapping("/admin/welcome")
    public String welcome3() {
        return "admin/welcome3";
    }
    
    @RequestMapping("/loginForm")
    public String loginForm() {
        return "security/loginForm";
    }
    
    @RequestMapping("/loginError")
    public String loginError() {
        return "security/loginError";
    }
}
cs


이전 프로젝트와 달리 "/loginForm", "loginError"가 추가되었다.


// loginForm.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>LoginForm</title>
</head>
<body>
<h1>loginForm.jsp</h1>
 
<form action="<c:url value="j_spring_security_check" /> " method="post">
        ID : <input type="text" name="j_username"> <br />
        PW : <input type="text" name="j_password"> <br />
        <input type="submit" value="LOGIN"> <br />
</form>
</body>
</html>
cs



// loginError.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>LoginForm</title>
</head>
<body>
<h1>loginError.jsp</h1>
 
로그인 실패 <br><p>
 
<a href=loginForm>로그인 페이지로 가기</a>
</body>
</html>
cs



결과 화면



guest는 누구나 접근 가능하기 때문에 로그인 없이 들어갈 수 있다.



member부터는 로그인이 필요한데 먼저 "USER"의 권한을 가진 user로 들어간다.




member에는 접근이 가능하지만 admin으로 들어갈 때는 "ADMIN" 권한을 가진 유저만 접근이 되므로

403 Forbidden 에러가 뜬다.




/logout을 하면 로그아웃이 되고 다시 admin 계정으로 로그인한다.



그러면 Admin에도 접근이 가능하게 된다.




Status Check



프로젝트 생성




코드 구현


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.study.springboot.auth;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Autowired
    public AuthenticationFailureHandler authenticationFailureHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/css/**""/js/**""/img/**").permitAll()
                .antMatchers("/guest/**").permitAll()
                .antMatchers("/member/**").hasAnyRole("USER""ADMIN")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated();
        
        http.formLogin()
                .loginPage("/loginForm")        // default : /login
                .loginProcessingUrl("/j_spring_security_check")
                //.failureUrl("/loginForm?error")        // default : /login?error
                //.defaultSuccessUrl("/")
                .failureHandler(authenticationFailureHandler)
                .usernameParameter("j_username")// default : j_username
                .passwordParameter("j_password")// default : j_password
                .permitAll();
        
        http.logout()
                .logoutUrl("/logout"// default
                .logoutSuccessUrl("/")
                .permitAll();
        
        // ssl을 사용하지 않으면 true로 사용
        http.csrf().disable();
    }
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password(passwordEncoder().encode("1234")).roles("USER")
            .and()
            .withUser("admin").password(passwordEncoder().encode("1234")).roles("ADMIN");
            // ROLE_ADMIN 에서 ROLE_는 자동으로 붙음
    }
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
 
cs


[34번 줄]

원래는 32번 줄에서 failure를 지정했지만 이제는 AuthenticationFailureHandler를 선언하고

의존성을 자동으로 주입받고 failureHandler로 지정해준다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.study.springboot;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
 
@Controller
public class MyController {
 
    @RequestMapping("/")
    public @ResponseBody String root() throws Exception {
        return "Security";
    }
    
    @RequestMapping("/guest/welcome")
    public String welcome1() {
        return "guest/welcome1";
    }
    
    @RequestMapping("/member/welcome")
    public String welcome2() {
        return "member/welcome2";
    }
    
    @RequestMapping("/admin/welcome")
    public String welcome3() {
        return "admin/welcome3";
    }
    
    @RequestMapping("/loginForm")
    public String loginForm() {
        return "security/loginForm";
    }
}
cs


로그인 에러는 따로 설정해주기 때문에 해당 코드는 없앴다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.study.springboot.auth;
 
import java.io.IOException;
 
import javax.security.auth.login.CredentialExpiredException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 
@Configuration
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception)
    throws IOException, ServletException {
        String loginid = request.getParameter("j_username");
        String errormsg = "";
        
        if (exception instanceof BadCredentialsException) {
            loginFailureCount(loginid);
            errormsg = "아이디나 비밀번호가 맞지 않습니다. 다시 확인해주세요.";
        } else if (exception instanceof InternalAuthenticationServiceException) {
            loginFailureCount(loginid);
            errormsg = "아이디나 비밀번호가 맞지 않습니다. 다시 확인해주세요.";
        } else if (exception instanceof DisabledException) {
            errormsg = "계정이 비활성화되었습니다. 관리자에게 문의하세요.";
        } else if (exception instanceof CredentialsExpiredException) {
            errormsg = "비밀번호 유효기간이 만료되었습니다. 관리자에게 문의하세요.";
        }
        
        request.setAttribute("username", loginid);
        request.setAttribute("error_message", errormsg);
        
        request.getRequestDispatcher("/loginForm?error=true").forward(request, response);
    }
    
    // 비밀번호 3번 이상 틀릴 시 계정 잠금 처리
    protected void loginFailureCount (String username) {
//        // 틀린 횟수 업데이트
//        userDao.countFailure(username);
//        // 틀린 횟수 조회
//        int cnt = userDao.checkFailureCount(username);
//        if(cnt == 3) {
//            // 계정 잠금 처리
//            userDao.disabledUsername(username);
//        }
    }
}
cs


[29~38번 줄]

에러 메시지를 직접 만드는데 특히, 29번과 23번 줄의 에러는 서로 다른 에러지만

너무 구체적인 에러를 알려주면 보안 위협이 있으므로 대략적인 에러 내용만 알려준다.


[41~44번 줄]

request의 속성 "username"loginid 값을, "error_message"errormsg 값을 넣어준다.


* loginFailureCount 메소드는 "이런 식으로 처리되는구나~" 정도로만 보고 직접 구현은 하지 않습니다.



// loginForm.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>LoginForm</title>
</head>
<body>
<h1>loginForm.jsp</h1>
 
<c:url value="j_spring_security_check" var="loginUrl" />
<form action="${loginUrl }" method="post">
        <c:if test="${param.error != null }">
        <p>
            Login Error! <br />
            ${error_message }
        </p>
        </c:if>
        ID : <input type="text" name="j_username" value="${username }"> <br />
        PW : <input type="text" name="j_password"> <br />
        <input type="submit" value="LOGIN"> <br />
</form>
</body>
</html>
cs


[15~18번 줄]

파라미터의 에러가 null이 아니면 에러 메시지 출력

error_message는 에러 핸들러에서 정해준 조건에 따른 에러 메시지가 출력될 것이다.


// welcome3.jsp, welcome2.jsp도 같은 코드를 넣어주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-Type" content="text/html; charset=UTF-8">
<title>Welcome</title>
</head>
<body>
welcome : Admin
 
<hr>
 
<c:if test="${not empty pageContext.request.userPrincipal }">
<p> is Log-In </p>
</c:if>
 
<c:if test="${empty pageContext.request.userPrincipal }">
<p> is Log-Out </p>
</c:if>
 
USER ID : ${pageContext.request.userPrincipal.name } <br />
<a href="/logout">Log Out</a> <br />
</body>
</html>
cs


15번 줄로그인 정보가 있을 때, 19번 줄없을 때의 코드로 보면 된다.



결과 화면



원래 ID/PW는 user/1234지만 일부러 로그인 실패를 불러본다.



그러면 관련 에러 메시지가 뜨면서 다시 로그인하라고 나온다.



다시 user/1234로 로그인하고 성공하면 유저의 ID가 출력되고 로그아웃 버튼이 활성화되어 있다. 



로그아웃하면 다시 홈 화면으로 돌아가고 admin도 마찬가지로 로그인 성공하면 위와 같이 뜨고 

실패하면 에러 메시지를 출력한다.



'공대인 > Spring[Boot]' 카테고리의 다른 글

[Spring Boot] Webjars  (0) 2019.08.29
[Spring Boot] Security Taglibs, Database  (2) 2019.08.26
[Spring Boot] Security  (0) 2019.08.22
[Spring Boot] Transaction (Manager, Template, Propagation)  (0) 2019.08.22
[Spring Boot] logback  (0) 2019.08.16
Comments