이번 연재에서는 이전에 만들었던 소스에 아래와 같은 기능을 추가할것이다.

HTTPS  기능 

https 는 http 에 SSL 기능을 추가한것인데 ,  HTTP 는 문자를 가지고 누가 엿보기가 쉽다. 따라서 

통신하는데 해당 문자를 암호화해주며, 암호화 하기위한 키에 대해 안전성을 보장해주는 기술이 들어가있다. 

SSL : https://wiki.kldp.org/HOWTO/html/SSL-Certificates-HOWTO/x70.html

1. security-context.xml 파일에 설정 추가 


<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"

  xmlns:beans="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://www.springframework.org/schema/beans

           http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

           http://www.springframework.org/schema/security

           http://www.springframework.org/schema/security/spring-security-3.2.xsd">

            

<http auto-config='true'  use-expressions="true">      

<intercept-url pattern="/login" access="permitAll" />   

<intercept-url pattern="/resources/**" access="permitAll" />  

<intercept-url pattern="/**" access="hasRole('ROLE_USER')"  requires-channel="https" />  

<form-login login-page="/login"                    

default-target-url="/monitering"          

username-parameter="username"        

password-parameter="password"          

authentication-failure-url="/login?error" 

always-use-default-target='true'           

/> 

<logout invalidate-session="true"   

     delete-cookies="JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE"  

logout-success-url="/login?logout" /> 

<remember-me key="wmoskey" token-validity-seconds="2419200"/> <!-- 4 weeks -->

<!-- enable csrf protection -->

<csrf/> 

</http> 


        <authentication-manager>

    <authentication-provider user-service-ref="memberService"/> 

</authentication-manager>

<beans:bean id="memberService" class="com.company.wmos.auth.MemberService"> 

    </beans:bean>

   

</beans:beans>

 

설명)

requires-channel 를 추가하였다. (한줄이면됨)

설정은 위에 한줄이면 되며 , 톰캣에 HTTPS 설정을 하면되는데 아래 싸이트를 참고하자

http://visu4l.tistory.com/419


싸이트 내용중에 참고로 국가코드는 KR 을 넣어주면 되며, 
아래에서 명령에서 trustcscerts  들은 모두 오타이다. trustcacerts 로 바꿔주자.


keytool -import -alias Root -trustcscerts -file TrialRoot.pem -keystore testserver


keytool -import -alias Intermediate -trustcscerts -file TrialIntermediate.pem -keystore testserver


keytool -import -alias testserver -trustcscerts -file cert.pem -keystore testserver





HTTPS 설정을 위한 다른 방법  ( 번역글 ) 

http://www.javacodegeeks.com/2012/12/securing-your-tomcat-app-with-ssl-and-spring-security.html\

SSL 과 Spring Security 를 가지고 톰캣 웹어플리케이션을 보호해보자.

만약 나의 마지막 블로그를 읽었다면 ten things that you can do with Spring Security.  를 알게되었을것이다.
그러나 스프링 시큐리티를 시작하기 전에 해야할것은 웹어플리케이션이 HTTPS 를 지원해야한다는것인데, 그것은
비밀번호같은것을 평문으로 인터넷상에 돌아다니는것을 막아줄것이다.  
자 HTTPS 설정을 시작해볼까? 

1.Key Store 만들기

처음으로 할것은 인증서를 포함한 사설 키스토어 를 만들것것이다.  그것을 생성할 가장 간단한 방법은 

자바 keytool 유틸리티를 사용하는것이다. 자바SDK 설치했으면 /bin 디렉토리안에 있을것이다.

keytool -genkey -alias MyKeyAlias -keyalg RSA -keystore /Users/Roger/tmp/roger.keystore


위의 명령어에서 

  • -alias 키에 대한 유니크한 ID
  • -keyalg   'RSA',  'DSA' , 'DES' 같은 키를 만들기위한 알고리즘 . 
  • -keystore  키스토어가 저장될 위치 

실행하면 다음과 같은것들을 물을것이다. 적절히 써넣자. (대한민국 국가코드는 KR) 


Roger$ keytool -genkey -alias MyKeyAlias -keyalg RSA -keystore /Users/Roger/tmp/roger.keystore
Enter keystore password: 
Re-enter new password:
What is your first and last name?
  [Unknown]:  localhost
What is the name of your organizational unit?
  [Unknown]:  MyDepartmentName
What is the name of your organization?
  [Unknown]:  MyCompanyName
What is the name of your City or Locality?
  [Unknown]:  Stafford
What is the name of your State or Province?
  [Unknown]:  NA
What is the two-letter country code for this unit?
  [Unknown]:  UK
Is CN=localhost, OU=MyDepartmentName, O=MyCompanyName, L=Stafford, ST=UK, C=UK correct?
  [no]:  Y

Enter key password for 
     (RETURN if same as keystore password): 

2. Tomcat 설정 변경 

톰캣이 SSL connector 를 가져야하는데 /conf 디렉토리에 보통 위치한 server.xml 파일에 아래를 추가한다.

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" />

…and making it look something like this:

<Connector SSLEnabled="true" keystoreFile="/Users/Roger/tmp/roger.keystore" keystorePass="password" port="8443" scheme="https" secure="true" sslProtocol="TLS"/> 

위에서 password에 평문을 넣는것은 보안에 좋지 않다. 여러 방법이 있는데 이 포스트를 넘어서는것이다.

3. security-context.xml  설정 

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
  xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/security
           http://www.springframework.org/schema/security/spring-security-3.1.xsd">
   
       <http auto-config='true' >
          <intercept-url pattern="/**" requires-channel="https" />    
       </http>
 
       <authentication-manager>
       </authentication-manager>

</beans:beans>

requires-channel="https" 를 추가하고 톰캣을 이용해서 시작하면  HTTPS 을 사용해서 접근할수있다. 

https://localhost:8443/my-app 요렇게 입력하면 작동할것이다. 
http://localhost:8080/my-app  요렇게 입력하면 자동으로 https 로 바뀔것이다.


이번 연재에서는 이전에 만들었던 소스에 아래와 같은 기능을 추가할것이다.


Remember Me 기능 

 Remember Me 기능이란 사용자 세션이 종료(디폴트 30분?) 된 후에도 자동으로 로그인 할수있는 기능이다.  
 이것은
추가적인  쿠키를 저장  하는데 , 그 기간(디폴트 2주) 을 정하면 2주동안은 로그인 하지 않고도 인증할수있게된다.  쿠키는 username / expirationTime / password  / key 와  이것들을  MD5 hash 로 인코딩한 정보를  포함한다.Remember Me 는 2가지 방법으로 구현가능한데 , 이 게시물에서는 심플 해쉬 기반 쿠키로 만들었다. 

1. security-context.xml 파일에  리멤버 미  설정 추가 


<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"

  xmlns:beans="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://www.springframework.org/schema/beans

           http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

           http://www.springframework.org/schema/security

           http://www.springframework.org/schema/security/spring-security-3.2.xsd">

            

<http auto-config='true'  use-expressions="true">      

<intercept-url pattern="/login" access="permitAll" />   

<intercept-url pattern="/resources/**" access="permitAll" />  

<intercept-url pattern="/**" access="hasRole('ROLE_USER')" />  

<form-login login-page="/login"                    

default-target-url="/monitering"          

username-parameter="username"        

password-parameter="password"          

authentication-failure-url="/login?error" 

always-use-default-target='true'           

/> 

<logout invalidate-session="true"   

 delete-cookies="JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE"  

logout-success-url="/login?logout" /> 


<remember-me key="wmoskey" token-validity-seconds="2419200"/> <!-- 4 주 -->

<!-- enable csrf protection -->

<csrf/> 

</http> 


        <authentication-manager>

    <authentication-provider user-service-ref="memberService"/> 

</authentication-manager>

<beans:bean id="memberService" class="com.company.wmos.auth.MemberService"> 

    </beans:bean>

   

</beans:beans>

설명)

<remember-me key="wmoskey" token-validity-seconds="2419200"/> 를 추가하였다. (한줄이면됨)

key 와 유효기간 4주를 설정하였다.

 delete-cookies="JSESSIONID,SPRING_SECURITY_REMEMBER_ME_COOKIE"  명시적으로 로그아웃

할때는 관련정보를 삭제하게했다.


2. 로그인 페이지에 Remember_me 기능 추가 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<title>마리나 로그인 페이지</title>
<link rel="stylesheet" href="../..//resources/css/login.css">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<script type="text/javascript"> 
function doLogin() {
if(frm.j_username.value == "") {
alert("아이디를 입력해주세요.");
return;
}
if(frm.j_password.value == "") {
alert("패스워드를 입력해주세요.");
return;
}
frm.submit();
}
</script>
</head>
<body>
<!--------------------------------- 탑 메뉴  ------------------------------------>
<br>
<br>
<div class="top_menu">
<div class="top_banner_left">&nbsp;&nbsp;<Font style="font-size:30px;">MARINA  <B>전기서비스 현황</B></Font>
&nbsp;&nbsp;&nbsp;&nbsp;
</div>

<!--------------------------------- 로그인 ------------------------------------>

<br>
<br>
<br>
<br>
<br>

<section class="loginform cf">
<form name="frm" action="j_spring_security_check" method="post">
<table>
<tr>
<td>
<ul>
<li>
<label for="userID">ID</label>
<input id = "userID" type="userID" name="username" placeholder="ID" required>
</li>
<li>
<label for="password">Password</label>
<input id = "password" type="password" name="password" placeholder="PASSWORD" required></li>
<li>
<input type="submit" value="로그인" onclick="doLogin()"/>
</li>
</ul>
</td>
</tr>
<tr>
<td>
<input id = "remember_me" name ="_spring_security_remember_me" type = "checkbox"/>Remember me
</td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>

</section>

<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
<c:if test="${not empty msg}">
<div class="msg">${msg}</div>
</c:if>

</body>
</html>

설명) 
<input id = "remember_me" name ="_spring_security_remember_me" type = "checkbox"/>Remember me
달랑 한줄이면된다.


레퍼런스)




만들던 웹어플리케이션에 인증(로그인)기능을 추가하게되었다.  추가했던 과정을 그대로 보여주는 게시글이다.
15년전에 홈페이지 (디아블로2 펜페이지)  를 처음 개발 했었을때 로그인방식은 ID/PWD 를 FORM 기반으로 
서버로 보내면 , 서버는 ID/PWD 를 확인하여  세션에 등록한후에, 요청이 오면 세션을 확인하여 존재하면 보여주고 , 
없으면 로그인창으로 리다이렉트 시켰던것으로 기억한다. 대규모 웹SI 개발을 해보지 않아서 요즘 트랜드가 어떤지 잘모르지만 스프링이 대세니깐 스프링 보안으로 해본다. :-) 프레임워크를 사용함으로써 고난이도의 구현 과 사상을 쉽게 가져다 쓸수있게 되었다. 

이번 연재에서는 아래와 같은 기능을 추가할것이다.

1. 기본 환경설정  (POM.XML /  WEB.XML /  Security-context.XML )

2. 로그인 인증 시스템  (로그인페이지등 제작) 

3. 인터셉터를 통한 권한 설정  (ROLE_USER)

3. DB 를 통한 사용자 정보 획득 기술  

4. CSRF 보안

1. POM.XML 파일에 라이브러리를 추가.

 <!-- Spring Security -->

        <dependency>

            <groupId>org.springframework.security</groupId>

            <artifactId>spring-security-web</artifactId>

            <version>3.2.3.RELEASE</version>

        </dependency>

        <dependency>

            <groupId>org.springframework.security</groupId>

            <artifactId>spring-security-config</artifactId>

            <version>3.2.3.RELEASE</version>

        </dependency>   

설명)

스프링 시큐리티 3.2.3 을 추가하였다.  
참고로  스프링이 3.1 버전일때 문제가 생겼다.  3.2 이상으로 올려야한다. 이런 의존성 문제 때문에 한참을 고생했다.

2. WEB.XML 수정 

<context-param>

<param-name>contextConfigLocation</param-name>

<param-value>

/WEB-INF/spring/root-context.xml

/WEB-INF/spring/security-context.xml       <--- 추가 

</param-value>

</context-param>

<!-- Security filter 추가 -->

    <filter>

        <filter-name>springSecurityFilterChain</filter-name>

        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

    </filter>   

    <filter-mapping>

        <filter-name>springSecurityFilterChain</filter-name>

        <url-pattern>/*</url-pattern>

    </filter-mapping>

설명) 

스프링이 시큐리티 관련 설정을 읽어가도록 security-context.xxml 을 추가 해주며
필터도 추가해주자.  유의사항은 필터 이름은 springSecurityFilterChain 이어야만 한다.

3. security-context.xml 파일 추가 

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"

  xmlns:beans="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://www.springframework.org/schema/beans

           http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

           http://www.springframework.org/schema/security

           http://www.springframework.org/schema/security/spring-security-3.2.xsd">

            

<http auto-config='true'  use-expressions="true">      ---------- (1)

<intercept-url pattern="/login" access="permitAll" />   ---------- (2)

<intercept-url pattern="/resources/**" access="permitAll" />  ---------- (3)

<intercept-url pattern="/**" access="hasRole('ROLE_USER')" />  ---------- (4)

<form-login login-page="/login"                     ---------- (5)

default-target-url="/monitering"            ---------- (6)

username-parameter="username"         ---------- (7)

password-parameter="password"          ---------- (8)

authentication-failure-url="/login?error"  --------- (9)

always-use-default-target='true'             --------- (10)

/> 

 <logout invalidate-session="true" logout-url="/logout"   logout-success-url="/login?logout" /> - (11)

<!-- enable csrf protection -->

<csrf/>   ---------- (12)

</http> 


        <authentication-manager> ---------- (13)

   <authentication-provider user-service-ref="memberService"/> ---------- (14)

</authentication-manager>

<beans:bean id="memberService" class="com.company.wmos.auth.MemberService"> ---------- (15)

    </beans:bean>

   

</beans:beans>=

설명)

(1) auto-config='true' 를 설정한것만으로 기본 로그인페이지/ HTTP 기본인증로그아웃기능등을 제공한다. 

      use-expressions="true" 는 SpEL 을 사용한다는 의미이다.  

- use-expressions="true"를 설정하지 않으면 default 가 false 이다. 이럴때는 SpEL을 사용하지 않는다.

<http auto-config="true">

        <intercept-url pattern="..." access="ROLE_ANONYMOUS" />

  <intercept-url pattern="..." access="IS_AUTHENTICATED_ANONYMOUSLY" />

        <intercept-url pattern="..." access="ROLE_USER" />

        <intercept-url pattern="..." access="ROLE_ADMIN" />

 </http>


- use-expressions="true" 로 설정하면 SpEL을 사용해서 작성을 해야한다. 그렇지 않으면 에러가 발생한다.

<http auto-config="true" use-expressions="true">

<intercept-url pattern="..." access="permitAll" />

    <intercept-url pattern="..." access="hasRole('ROLE_ANONYMOUS')" />

        <intercept-url pattern="..." access="hasRole('ROLE_USER')" />

        <intercept-url pattern="..." access="hasRole('ROLE_ADMIN')" />

</http>


(2)~(4)해당 URL 에 접근하기위한 권한을 설정하여준다. ( 접근가능한 IP 등을 설정할수도있다) . 그리고 권한은 위쪽이 우선시된다.
(2)  /login 으로는 모두 허용해준다./login 을 막아놓으면 안되니깐~   
(3) 리소스도 허용해주고
(4) 나머지는 모두 ROLE_USER 권한을 가진사람만 허용해준다. 
(5) <form-login  사용자이름과 비밀번호를 가지고있는 폼기반 인증방법을 사용한다. 

     login-page="/login"  를 설정하여 사용자가 만든 로그인페이지를 스프링에게 알려준다. 

     이거 설정안하면 스프링이 만들어준것을 사용. 

(6) default-target-url="/monitering" 로그인성공하면 이동할 페이지설정
(7) (8)  이거 설정안해도 된다. 나중에 long.jsp 페이지에서 FORM 안에 들어갈 name 을 여기서 지정한것으로 바꾸어준다. 
(9) authentication-failure-url="/login?error" 실패시 호출해줄 URL (login 페이지에 error 파라미터를 보내준다)  
(10) always-use-default-target='true' 이거 안하면 로그인성공해도 /monitering 로 제대로 안가더라
(11) 로그아웃되면 세션을 초기화한다. 

         logout-success-url="/login?logout" <-- 로그아웃되면 이동할 페이지  
         
 logout-url="/logout"<--로그아웃을 위한 URL설정. 이거 안해주면 디폴트로 j_spring_security_logout 해주면됨.
(12) 간단한 설정으로 csrf 를 통한 해킹을 막을수있다.  
       ( CSRF 설명: http://tm-csc.tistory.com/entry/7-WebHacking%EC%9B%B9%ED%95%B4%ED%82%B9-CSRF)

(13) 인증처리를 위한 최상위 태그   
(14) user-service-ref="memberService" 사용자이름/비밀번호를 제공해줄 서비스 등록

      이 게시물에서는 DB 를 이용하여 처리하려고하는데, 인메모리로 바로 적어줄수도있다. 다음처럼  

<authentication-manager> <authentication-provider> <user-service> <user name="admin" password="1234" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager>

(15) MemberService 빈등록 

HTTP 기본인증이란 ? 

위와 같은 폼 기반 인증은 애플리케이션 사용자가 사람일때 이상적이지만 , 사용자가 다른 애플리케이션이라면 폼로그인을 사용하지 못할수도있다.

HTTP 기본인증은 HTTP 요청 시 사용자가 직접 어플리케이션에 대해 인증하는 방법이다. 웹 브라우저에서 HTTP 기본인증을 접하면 단순한 모달 대화상자로 사용자에게 입력을 요청한다.  이것은 실제로는 사용자명과 패스워드가 필요하다는 HTTP 401 응답이다. 이러한 HTTP 기본 인증은 REST 클라이언트가 사용하는 서비스의 인증 수단으로 적합하다. 

따라서 로그인으로  인증에 성공하면 인증정보가 클라이언트에 저장되어서 후속 요청시에 서버로 HTTP 프로토콜에 뭍어서 날라올거라는것을 알수가 있다. 그  사용자 정보(롤포함) +  요청URL 의 권한정보를 토대로 접근을 허용할지 안할지 결정할수있을것이라 생각된다.

http://iloveulhj.github.io/posts/http/http-basic-auth.html

4. 컨트롤러 추가

package com.company.wmos.auth;

import java.util.Map;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.servlet.ModelAndView;

@Controller

public class authController {

private static final Logger logger = LoggerFactory.getLogger(authController.class);

@RequestMapping(value = "/login", method = RequestMethod.GET)

public ModelAndView login(

@RequestParam(value = "error", required = false) String error,

@RequestParam(value = "logout", required = false) String logout) {

ModelAndView model = new ModelAndView();

if (error != null) {

model.addObject("error", "Invalid username and password!");

}

if (logout != null) {

model.addObject("msg", "You've been logged out successfully.");

}

model.setViewName("login");

return model;

}

}


설명)

long 페이지에  로그인 실패할경우와 로그아웃할경우에 대한 메세지를 뿌려주기위해서 파라미터를 받는다. required=false 는 파라미터 없어도 된다는 말이고 login.jsp 로 이동한다.

5. MemberService 추가  

package com.company.wmos.auth;

import java.util.ArrayList;

import java.util.Collection;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.company.wmos.model.UserVO;

import com.company.wmos.service.UserService;

public class MemberService implements UserDetailsService{

@Autowired UserService        userService;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

     UserVO userVO = userService.getUsersByID(username);

if (userVO == null) {

        throw new UsernameNotFoundException("No user found with username" +                                                                                         userVO.getName());

}  

Collection<SimpleGrantedAuthority> roles = new ArrayList<SimpleGrantedAuthority>();

     roles.add(new SimpleGrantedAuthority("ROLE_USER"));

    UserDetails user = new User(username, userVO.getPassword(), roles);

           return user;

}

}


설명)
사용자가 입력한 ID/PWD 를 검증하기위해 저장소에 그 ID/PWD 가 있는지 확인하는 소스이다.
저장소는 인메모리가 될수도있고, 이것처럼 DB 가 될수도있고 어떠한것(페이스북,네이버등등) 도 될수있다. 
userService는 DB 에 접근하기위해 내가 만든 서비스이다. 그냥 DAO 를 만들어도되고 DB에 접근하는 아무거나  만들면된다.
중요한것은 loadUserByUsername  함수안에서는 
사용자가 입력한 username에 대한 데이터를 못찾으면 UsernameNotFoundException 예외를 날려주고 
찾으면 UserDetails  객체에 값들 (이름,비밀번호, 해당사용자의 롤) 을 넣어서 리턴시켜준다는것이다.
저 위의 소스에서는 누구든지 ROLE_USER 가 되는데, DB에 사용자테이블에 맞춤식으로 넣는게 일반적일것이다.
저렇게 리턴된 객체를 가지고 스프링 내부 에서 인증에 대한 판단을 한다. 


6. 로그인 페이지 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<title>마리나 로그인 페이지</title>
<link rel="stylesheet" href="../..//resources/css/login.css">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<script type="text/javascript"> 
function doLogin() {
if(frm.j_username.value == "") {
alert("아이디를 입력해주세요.");
return;
}
if(frm.j_password.value == "") {
alert("패스워드를 입력해주세요.");
return;
}
frm.submit();
}
</script>
</head>
<body>
<!--------------------------------- 탑 메뉴  ------------------------------------>
<br>
<br>
<div class="top_menu">
<div class="top_banner_left">&nbsp;&nbsp;<Font style="font-size:30px;">MARINA  <B>전기서비스 현황</B></Font>
&nbsp;&nbsp;&nbsp;&nbsp;
</div>

<!--------------------------------- 로그인 ------------------------------------>

<br>
<br>
<br>
<br>
<br>

<section class="loginform cf">
<form name="frm" action="j_spring_security_check" method="post">
<ul>
<li>
<label for="userID">ID</label>
<input id = "userID" type="userID" name="username" placeholder="ID" required>
</li>
<li>
<label for="password">Password</label>
<input id = "password" type="password" name="password" placeholder="PASSWORD" required></li>
<li>
<input type="submit" value="로그인" onclick="doLogin()"/>
</li>
</ul>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
 
</form>
</section>
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
<c:if test="${not empty msg}">
<div class="msg">${msg}</div>
</c:if>

</body>
</html>


설명) 
중요한것은 username 과 password  를 post 로  j_spring_security_check  으로 보내준다는것!! 끝이다.
나머지는 화면 꾸미기이다.  (csrf 보안을 위한 코드도 포함됨)

7. 로그아웃 

<body ng-controller="moniteringCtrl"  class="pad">


<c:url value="/logout" var="logoutUrl" />

 

<!-- csrt for log out-->

<form action="${logoutUrl}" method="post" id="logoutForm">

  <input type="hidden" 

name="${_csrf.parameterName}"

value="${_csrf.token}" />

</form>

 

  <script>

function formSubmit() {

document.getElementById("logoutForm").submit();

}

</script>

 

  <!------------------------     navigation   ------------------------>

 ....

         <form class="navbar-form navbar-right" >

             <span style="color: gray;" ><h5> ${username} 님 반갑습니다. 

                                <a href = "javascript:formSubmit()"> 로그아웃 </a> </h5></span>

      </form>

   

     ....


설명) 
로그아웃시 중요한것은 post 로  /logout으로 보내준다는것!! 끝이다. (csrf 보안을 위한 코드도 포함됨)
 security-context.xml 에서 logout-url="/logout" <--로그아웃을 위한 URL설정했던 기억이 날것이다.
이거 안해주면 디폴트로 
j_spring_security_logout 해주면됨.

8. 실제 화면 모습


-  로그인 실패시  - 



                                         -  Monitering  페이지 - 

   -  로그아웃 화면

지금까지 간단히 스프링보안으로 로그인기능을 추가하는것을 살펴보았다.
스프링 보안은 이거 말고도 엄청나게 많은 기능을 갖고있기때문에 일단 한번 간단하게 공부/구현을 한 후에는 
적극적으로 파보아야할것이다.



얼마전에 프로젝트B에 MyBatis-Spring 을 추가해서 개발하는데 A 프로젝트에서는 잘 되던게, 잘안되서 한참 해매다보니..A프로젝트는 mybatis-3.0.6  이었는데  B 프로젝트는 mybatis-3.1 이었다. 버전문제인지는 생각치도 않고  각종 설정등을 바꾸어가고 삽질좀하다가 찾아내었다.   스프링과 아이들 너므너므해~






요구사항 


 mybatis-spring

  mybatis 

 spring 

 

 1.0.0 and 1.0.1

 3.0.1 to 3.0.5

 3.0.0 or higher

 

 1.0.2  

 3.0.6

 3.0.0 or higher 

 

 1.1.0 or higher

 3.1.0 or higher

 3.0.0 or higher

 


 이런식의 실수는 도처에 도사리고있다!!!


 스프링 시큐리티도 3.2.3을 가져다 쓰다가 안되었는데 


 스프링 버전이 3.1 이라서 ~ ㅎㅎ


component-scan /  annotation-config /  annotation-driven  차이점

최근에 스프링(3.2)으로 개발하게 되면서 가장 어려웠던것은, 스프링 설정인데 버전이 올라가면서 
바뀐분/기존부분등이 서로 어울어져있어서 (특히 검색해서 코드를 짜다보면) 어떤것이 맞는건지, 
어떤것을 해야하는지 선택의 과정에서 힘들게했다.  스타트업회사의 특성상 여러가지일들을 하다보니  
스프링만 주구장창 만지고있을수는 없으니 ,  시간이 지나서 다시보면 또다시 헥깔리고..ㅎㅎ
오늘은  그동안 무턱대고 써온 component-scan / annotation-config / annotation-driven  이것들에 
대해서 알아본다.



많은 스프링개발자들이  스프링 IOC 컨테이너 안에서 어노테이션이 어떻게 작동하는지에 대해 혼동을 하고

있는데 스프링 MVC 프레임워크는 효율적으로 빈들을 관리하고 빈을 요구되어지는곳에 주입시키기위해  스프링 컨테이터를 도와주거나  지침을 줄수있는 여러  설정방법들을 제공한다. 

스프링 설정파일에서 보여지는 XML 설정은 다음과 같은것들이 있는데  

  • context:component-scan

  • mvc:annotation-config

  • context:annotation-driven

위의 어노테이션의 기능은 유사하며 , 특정 시나리오 상에서 어떻게 반응하는지에 대한 작은 차이점이 있다. 
이 튜토리얼은 언제 그것이 요구되어지는지 각각 요소들에 대해 중요한 포인트를 짚어본다.


1. <context:component-scan/>


이 요소는 스프링 2.5 버전에 소개되어졌으며 , 만약 그 이전 버전의 스프링으로 작업하려면 , 모든 빈들은 수동적으로 XML 파일에 설정되어야 했었다.  자바빈에 대해 어노테이션 지원은 없었다.  이것은 많은 XML 코딩을 초래했으며 ,유지보수에서도 심각한 문제가 있었다. context:component-scan 요소는 XML 파일안의 모든 빈들의 선언을 제거했다. 

아래는 스프링 설정파일에서 선언된 모습이다.


1<context:component-scan base-package="org.controller"/>


위의 선언은 특정 패키지(위에서는 org.controller) 안의 클래스들을 스캔하고 , 빈 인스턴스를 생성한다. 

아래와 같은 정확한 어노테이션이 존재해야지 빈을 생성할수있다. 


  • @Component
  • @Repository
  • @Service
  • @Controller



이것의  장점중 하나는   @Autowired and @Qualifier 어노테이션을 이해한다는것인데 

만약 당신이 <context:component-scan> 를 선언했다면 <context:annotation-config>
선언할필요가 없다는것이다.


DefaultAnnotationHandlerMapping는 type레벨에서 @RequestMapping을 처리하고  AnnotationMethodHandlerAdapter는 메서드레벨에서 처리하는데 사용합니다. 이 2가지는 DispatcherServlet을 사용할 경우 디폴트로 사용되지만 다른 핸들러맵핑을 합께 사용하려고 하면 지정하여야 합니다. 


2. <mvc:annotation-driven/>

mvc:annotation-driven스프링 MVC 컴포넌트들을 그것의 디폴트 설정을 가지고  활성화 하기위해 
사용된다. 만약  context:component-scanXML 파일에서 빈을 생성하기위해 사용하면서 mvc:annotation-driven 를  포함시키지 않아도 MVC 어플리케이션은 작동할것이다. 그러나  mvc:annotation-driven 은 특별한
일들을 하는데 이 태그는 당신의 @Controllers 에게 요청을 전파하기위해  요구되는  HandlerMapping 와  HandlerAdapter 를 등록한다.  게다가 , 클래스패스상에 존재하는것에 기반한 아래와 같은 어떤 
디폴트 작업을 적용한다.  
  • Using the Spring 3 Type ConversionService as a simpler and more robust alternative to JavaBeans PropertyEditors
  • Support for formatting Number fields with @NumberFormat
  • Support for formatting Date, Calendar, and Joda Time fields with @DateTimeFormat, if Joda Time is on the classpath
  • Support for validating @Controller inputs with @Valid, if a JSR-303 Provider is on the classpath
  • Support for reading and writing XML, if JAXB is on the classpath
  • Support for reading and writing JSON, if Jackson is on the classpath
질문: 이거  xml 안에 선언안해주면  @Controller 와  @RequestMapping 은 잘 동작하나?  


답변 : 


대중들의 믿음과 대조적으로 이 두개 (component-scan 과 annotation-driven) 의 선언에 는 어떤 

의존성이 없다. context:component-scan 이 포함해주면  mvc:annotation-driven  선언안해

준 @Controller / @RequestMapping 도  요청을 핸들링하는것이나 이슈 없이 잘 동작한다. 



3. <context:annotation-config/>


context:annotation-config 는 어플리케이션 컨텍스트안에 이미 등록된 빈들의 어노테이션을 활성화를 위해 사용된다. (그것들이 XML 으로 설정됬는지 혹은 패키지스캐닝을 통한건지는 중요하지 않다) 

그 의미는 이미 스프링 컨텍스트에 의해 생성되어 저장된  빈들에 대해서  @Autowired and @Qualifier 

어노테이션을 해석할거란 얘기다.

context:component-scan   또한 같은일을 할수있는데, 추가적으로 어플리케이션 컨텍스트에 빈을 등록하기위한 패키지들을 스캔한다context:annotation-config 은 빈을 등록하기위해 검색할수없다. 

<context:annotation-config /> 태그를 설정하면 아래의 기능을 각각 설정하는 수고를 덜게 해준다. 

- RequiredAnnotationBeanPostProcessor : @Required 어노테이션 처리

- AutowiredAnnotationBeanPostProcessor : @Autowired 어노테이션 처리

   <bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>

- CommonAnnotationBeanPostProcessor : @Resource, @PostConstruct, @PreDestroy 어노테이션 처리

- ConfigurationClassPostProcessor : @Configuration 어노테이션 처리



component-scan / annotation-config 의 차이

http://stackoverflow.com/questions/7414794/difference-between-contextannotation-config-vs-contextcomponent-scan



component-scan /  annotation-driven 의 차이


참고

http://www.javabeat.net/spring-mvc-component-scan-annotations/



서버분석 


아래과 같은 라이브러리가 필요합니다.

dependencies {

compile("org.springframework.boot:spring-boot-starter-web")

compile("org.springframework.boot:spring-boot-starter-thymeleaf")

compile("org.springframework.boot:spring-boot-starter-security")

testCompile("org.springframework.boot:spring-boot-starter-test")

}

특이한건 스프링 boot 를 사용했다는건데, 이번 예제를 통해서 저도 처음 boot 를 접해보았는데 설정부분 및 REST 서비스를  굉장히 단순화 시켜 놓았더군요.

 XML  설정파일은 필요없고, 어노테이션을 통해서 설정을 해줍니다. 

(자바 코드를 통한 설정은 스프링 3.1에 거의 완성되었습니다.) 


중요한 2개의 자바 파일을 분석해 보겠습니다.(Application.java 에서 스프링을 실행시켜줍니다.)




1WebSecurityConfiguration 


첫번째 소스를 분석하기전에 기본 지식을 쌓도록 합시다. 

(http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ ) 

WebSecurityConfigurerAdapter 

@EnableWebSecurity 어노테이션과  WebSecurityConfigurerAdapter 은 웹기반 보안을 위해 함께 작동 하며  WebSecurityConfigurerAdapter 을 상속받아서 몇줄만 코딩하면 다음과 같은것을 할수있게된다.

  • 우리의 어플리케이션에 어떤 URL 로 들어오던간에 사용자에게 인증을 요구할수있음.
  • 사용자이름과 비밀번호 및 롤기반으로 사용자를 생성할수있다.
  • HTTP Basic 과 폼기반 인증을 가능하게함.
  • 스프링 시큐리티는 자동적으로 로그인페이지,인증실패 url 등을 제공한다.

@EnableWebSecurity @Configuration public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) { auth .inMemoryAuthentication() .withUser("user") // #1 .password("password") .roles("USER") .and() .withUser("admin") // #2 .password("password") .roles("ADMIN","USER"); } @Override public void configure(WebSecurity web) throws Exception { web .ignoring() .antMatchers("/resources/**"); // #3 } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeUrls() .antMatchers("/signup","/about").permitAll() // #4 .antMatchers("/admin/**").hasRole("ADMIN") // #6 .anyRequest().authenticated() // 7 .and() .formLogin() // #8 .loginUrl("/login") // #9 .permitAll(); // #5 } }
  • #1  사용자 이름 “user” 에 대해서 유저ROLE로 인메모리 인증을 허락한다.
  • #2  사용자 이름  “admin” 에 대해 관리자ROLE로  인메모리 인증을 부여한다.
  • #3 “/resources/”  경로 호출은 인증 무시한다. XML 설정에서는 다음과 같다. http@security=none  
  • #4 인증없는 사용자를 포함한 누구한테나  “/signup” and “/about”  경로는  허용한다.
  • #5 “/login” and “/login?error” 경로는 누구한테나 허용
  • #6 /admin/ 로 시작되는 경로는 반드시 관리자 ROLE 에게만 허용된다.
  • #7 모든 남은 URLs 는 인증이 성공해야지만 접근가능하다.
  • #8 자바 세팅을 통해 폼기반인증셋업. 인증은  /login 으로 username/password 가 POST 로 날라올때
        수행된다. 
  • #9  명시적으로 GET /login 이 요청되었을때 로그인페이지를 보여지는게 요구되어진다. 

위의 것은 아래와 같은  기존의  XML based configuration 과 비슷하다.


<http security="none" pattern="/resources/**"/> <http use-expressions="true"> <intercept-url pattern="/logout" access="permitAll"/> <intercept-url pattern="/login" access="permitAll"/> <intercept-url pattern="/signup" access="permitAll"/> <intercept-url pattern="/about" access="permitAll"/> <intercept-url pattern="/**" access="hasRole('ROLE_USER')"/> #7 <logout logout-success-url="/login?logout" logout-url="/logout" /> <form-login authentication-failure-url="/login?error" login-page="/login" login-processing-url="/login" password-parameter="password" username-parameter="username" /> </http> <authentication-manager> <authentication-provider> <user-service> <user name="user" #1 password="password" authorities="ROLE_USER"/> <user name="admin" #2 password="password" authorities="ROLE_USER,ROLE_ADMIN"/> </user-service> </authentication-provider> </authentication-manager>

XML 설정과 몇가지 차이점 

  • #1 , #2 에서 보면 자바설정에서는 ROLE_ 이 없다. 자동적으로 추가해주기때문이다.
  • Java configuration has different defaults URLs and parameters. Keep this in mind when creating custom login pages. The result is that our URLs are more RESTful. Additionally, it is not quite so obvious we are using Spring Security which helps to prevent information leaks. For example:

  • /spring_security_login 대신해서 GET /login 호출하면 로그인 페이지를 연다.
  • /j_spring_security_check 대신해서 POST /login 를 호출하면 인증체크를 한다. 
  • 사용자이름 기본 설정은  j_username 대신해서 username  이다.
  • 패스워드 기본 설정은   j_password 대신해서  password 이다.
  • 자바 설정은  여러가지 요청 URL 에 대하여 동일한 롤에 의해 쉽게  매핑할수있게 해준다. #4 참고 
  • 자바설정은 여분의 쓸모없는 코드들을 제거한다 . intercept-url 같은 경우 중복이 심하다.
  • When mapping HTTP requests using the hasRole method as we did in #6, we do not need to specify “ROLE_” as we would in XML. Again, this is so common of a convention that the hasRole method automatically adds “ROLE_” for you. If you did not want to automatically prefix with “ROLE_”, you could use the “access” method.

실제 코드 

package showcase;

// org.springframework.security 를 사용합니다.

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;


// 스프링 시큐리티 Java Config 설정 (security-context.xml 로 설정할수도있다.)

// configuration 은 springSecurityFilterChain 로 알려진 서블릿 필터를 생성한다. 

// configuration 은 URLs 보호, ID/PWD 검증 같은 모든 보안에 대해서 책임진다.

@Configuration

@EnableWebSecurity


public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {


@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()  // roy , spring 이면 인증 성공, USER 롤 받음

.withUser("roy") 

.password("spring")

.roles("USER");

}


@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().antMatchers("/**")  // 모든 요청에 대해서 USER 롤을 가지고있는지 확인

.hasRole("USER").and().httpBasic();

}

}





둘째. HomeController 

package showcase;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseBody;


@Controller

@RequestMapping("/*")

public class HomeController {


private static final Logger logger = LoggerFactory.getLogger(HomeController.class);


@RequestMapping(value = "/", method = RequestMethod.GET)   

public String home() {

logger.info("Spring Android Basic Auth");

return "home";    // home.html 호출 해줌

}


@RequestMapping(value = "/getmessage", method = RequestMethod.GET, produces = "application/json")

public @ResponseBody Message getMessage() {

logger.info("Accessing protected resource");  // USER 롤을 가지고있으면 여기에 들어옴

  // response body 로 Message 객체를 JSON 형식으로 보내줌. 

return new Message(100, "Congratulations!", "You have accessed a Basic Auth protected resource.");

}


}







클라이언트 분석 


아래과 같은 라이브러리가 필요합니다.

dependencies {
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.11'
}


안드로이드 코드를 살펴보기전에 간련 기술을 알아봅시다. (아래 링크 참고)


HTTP basic Authentication

HTTP 기본 인증에서는 서버로 아래와 같이 Request Header 를 보낸다.

1
2
3
4
5
6
7
8
9
10
GET /spring-security-http-basic-authentication/secured/mypage HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:17.0) Gecko/20100101 Firefox/17.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Cookie: JSESSIONID=896331E26095C95449516FCBF2E0E93C; __atuvc=28%7C31%2C0%7C32%2C0%7C33%2C215%7C34%2C59%7C35
Authorization: Basic c3JjY29kZXM6cGFzc3dvcmQ=
Note : 'c3JjY29kZXM6cGFzc3dvcmQ=' is Base64 encoded version of 'username:password' i.e. 'srccodes:password'.


실제 코드 

package org.springframework.android.basicauth;

import java.util.Collections;

import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

/**
* @author Roy Clarkson
*/
public class MainActivity extends AbstractAsyncActivity {

protected static final String TAG = MainActivity.class.getSimpleName();


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity_layout);

// Initiate the request to the protected service
final Button submitButton = (Button) findViewById(R.id.submit);
submitButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
new FetchSecuredResourceTask().execute();
}
});
}

private void displayResponse(Message response) {
Toast.makeText(this, response.getText(), Toast.LENGTH_LONG).show();
}


private class FetchSecuredResourceTask extends AsyncTask<Void, Void, Message> {

private String username;

private String password;

@Override
protected void onPreExecute() {
showLoadingProgressDialog();

// build the message object
EditText editText = (EditText) findViewById(R.id.username);
this.username = editText.getText().toString();

editText = (EditText) findViewById(R.id.password);
this.password = editText.getText().toString();
}

@Override
protected Message doInBackground(Void... params) {
final String url = getString(R.string.base_uri) + "/getmessage";

// HTTP 기본인증헤더를 username , password를 이용하여 생성한다.
HttpAuthentication authHeader = new HttpBasicAuthentication(username, password);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAuthorization(authHeader);
requestHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

// Create a new RestTemplate instance
RestTemplate restTemplate = new RestTemplate();

// JSON 형식 컨버터 설정
restTemplate.getMessageConverters().add(new MappingJacksonHttpMessageConverter());

try {
// 서버의 URL에 데이터를 요청한다.받은데이터는 Message 객체로 변환
Log.d(TAG, url);
ResponseEntity<Message> response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<Object>(requestHeaders), Message.class);
return response.getBody();
} catch (HttpClientErrorException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
return new Message(0, e.getStatusText(), e.getLocalizedMessage());
} catch (ResourceAccessException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
return new Message(0, e.getClass().getSimpleName(), e.getLocalizedMessage());
}
}

@Override
protected void onPostExecute(Message result) {
dismissProgressDialog();
displayResponse(result);
}

}

}





이번 연재에서는 안드로이드를 위한 스프링을 사용하는 법을 알아보도록 하자. 

이 연재는 "인증" 이 첫번째 목적이고 , "REST 통신" 이 두번째 목적이다.


일단 관련 프로그램 설치



다음 예제 소스를 분석해 보도록 할 것이다. 


spring for android 홈페이지 : http://projects.spring.io/spring-android/

spring for android 샘플소스 : https://github.com/spring-projects/spring-android-samples


소스를 다운받아서 압축을 해제하면 나오는 프로젝트중에 spring-android-basic-auth 를 볼것이다.

spring-android-basic-auth 안에는 Server / Client 프로젝트가 각각 있는데 


Client 프로젝트는 안드로이드 스튜디오를 통해서 import 한다. (Import project (Eclipse ADT, Gradle, etc,) 



Server 프로젝트는 STS 를 통해서 Import 한다. (Existing Maven Projects) 



Server 실행 

스프링 서버를 import 한후에  프로젝트 오른쪽 클릭 -  Run as - Srping Boot app  클릭하여 서버를 실행시켜준다. 




실행 브라우저를 통한 서버 테스트

안드로이드 클라이언트를 실행하기 전에  브라우저를 통해서 테스트 해본다. 브라우저 띄우고

 localhost:8080/ 입력하면 



아래와 같이 이름/비밀번호 입력창이 뜬다. roy/spring 입력해서 인증한후에 

브라우저에  localhost:8080/getmessage 입력하면 데이터를 그냥 받을수있다. 




Client 실행

이제 안드로이드 스튜디오로 가보자. 먼저 res/values/urls.xml 에서  http://192.168.1.191:8080/   이것처럼  url 을 

자신의 내부ip 로 바꾸어주고 아까 임포트한 프로젝트에서 실행을 해보자. 



기기를 통해서 보면 다음과 같이 나온다.



이름 / 비밀번호를 넣고 Submit 를 누르면 , 토스트를 통해서 서버로 부터 받아온 메세지를 확인할수있다.

다음 연재에는 소스를 분석해 보도록하자. 

+ Recent posts