๐ฅ #10ในใใชใณใฐๅฎๅ จ็ป้ฒใ่กใ
35360 ใฏใผใ
๐ฅ #็ถใใฆ8่กใใพใ~!
UserApi Controlใใ่ฆใใจใ
ในใใชใณใฐๅฎๅ จๅผใ้ฎๆญใใใใใใงใ.
ใใญใใฏใใใซใฏใSecurityConfigใฎconfigureใกใฝใใใซ
ใใใใฃใฆใ็พๅจใฎSecurityConfigใฏไปฅไธใฎใใใซใชใใพใ.
< SecurityConfig >
UserDetailsใฟใคใใฎUserใชใใธใงใฏใใไฝๆใใๅฟ ่ฆใใใใพใ.
ใญใฐใคใณใๅฎไบใใใจSpring Securityใปใใทใงใณใซใฆใผใถใผๆ ๅ ฑใ็ป้ฒใใๅฟ ่ฆใใใใพใใใใฆใผใถใผใชใใธใงใฏใใ็ป้ฒใใใใจใฏใงใใพใใ.UserDetailsใฟใคใใฎใฟ็ป้ฒใงใใพใ.
< PrincipalDetail >
ใฟใคใใฏUserDetailsใงใชใใใฐใชใใชใใฎใงใใใใๅฎ็พใใพใ.
UserDetailsใๆก็จใใฆใใใในใฆใฎๆฝ่ฑก็ใชๆนๆณใไธๆธใใใใฆใใพใ.
ใณใณใใธใใใณใณใปใใ
ไธๆธใใใใloadUserByUsername้ขๆฐใงใๅใๅใฃใใฆใผใถใผๅๅคๆฐใDBใซใใใใจใ็ขบ่ชใใพใ.
ใญใฐใคใณใ่ฆๆฑใใใจใloadUserByUsername้ขๆฐใ่ชๅ็ใซๅฎ่กใใใใฆใผใถใผๅใซๅฏพๅฟใใใฆใผใถใผใๆค็ดขใใใPrincipalDetailใฟใคใ(ใใชใใกใUserDetailsใฟใคใ)ใ่ฟใใใพใ.
->ใฆใผใถDetailsใฟใคใใงใใใใใใฆใผใถๆ ๅ ฑใฏใปใญใฅใขใปใใทใงใณใซๆ ผ็ดใใใพใ.
ใใฎ้ขๆฐใฏ้ๅฐใซๅฎ็พใใๅฟ ่ฆใใใ.ใใใง็งใใกใฏๆฌๅฝใซ(?)ใฆใผใถใผๆ ๅ ฑใๅซใใฆใชใฟใผใณใงใใพใ.
< UserRepository >
<ใปใญใฅใชใใฃๆงๆ>ๆ็ต็
UserDetailใฟใคใใงPrincipalDetailใไฝๆใใPrincipalDetailใไฝฟ็จใใฆใใใฑใผใธๅใใฆไฟๅญใใพใ.
ใงใฏใspring securityใไฝๆใใใปใใทใงใณๆ ๅ ฑใใณใณใใญใผใฉใงๆค็ดขใใใซใฏใฉใใใใฐใใใฎใงใใใใ.ใณใณใใญใผใฉใใใญใฐใคใณใปใใทใงใณใๆค็ดข:@่ช่จผPrincipal Principal
UserApi Controlใใ่ฆใใจใ
/auth/loginProc
ใฏไฝๆใใใฆใใพใใ.ในใใชใณใฐๅฎๅ จๅผใ้ฎๆญใใใใใใงใ.
ใใญใใฏใใใซใฏใSecurityConfigใฎconfigureใกใฝใใใซ
.loginProcessingUrl("/auth/loginProc")
ใ่ฟฝๅ ใใๅฟ
่ฆใใใใพใ.ใใใใฃใฆใ็พๅจใฎSecurityConfigใฏไปฅไธใฎใใใซใชใใพใ.
< SecurityConfig >
@Configuration // ๋น ๋ฑ๋ก (IoC๊ด๋ฆฌ)
@EnableWebSecurity // Security ํํฐ๊ฐ ๋ฑ๋ก๋จ = ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ์ด๋ฏธ ํ์ฑํ๋ ๋์ด์์ง๋ง, ์ค์ ์ ํด๋น ํ์ผ์์ ํ ๊ฒ์
@EnableGlobalMethodSecurity(prePostEnabled = true) // ํน์ ์ฃผ์๋ก ์ ๊ทผ์ ํ๋ฉด ๊ถํ ๋ฐ ์ธ์ฆ์ ๋ฏธ๋ฆฌ ์ฒดํฌ (์ํํ ํ์ ์ฒดํฌํ๋ ๊ฒ์ด ์๋)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 1. Bean ์ด๋
ธํ
์ด์
์ ๋ฉ์๋์ ๋ถ์ฌ์ ๊ฐ์ฒด ์์ฑ์ ์ฌ์ฉ
@Bean // IoC๊ฐ ๋๋ค.
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder(); // ์ด ๊ฐ์ฒด๋ฅผ ์คํ๋ง์ด ๊ด๋ฆฌํ๊ฒ ๋จ. ํ์ํ ๋๋ง๋ค ๊ฐ์ ธ๊ฐ์ ์ฐ๋ฉด ๋๋ค.
}
// ํํฐ๋ง
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // csrf ํ ํฐ ๋นํ์ฑํ (ํ
์คํธ์ ๊ฑธ์ด๋๋๊ฒ ์ข์)
.authorizeRequests() // request๊ฐ ๋ค์ด์ค๋ฉด
.antMatchers("/", "/auth/**", "/js/**", "/css/**", "/image/**") // ์ฌ๊ธฐ๋ก ๋ค์ด์ค๋ฉด
.permitAll() // ๋ชจ๋ ๊ฐ๋ฅ (๋๊ตฌ๋ ๊ฐ๋ฅ)
.anyRequest() // ๊ทธ๊ฒ ์๋ ๋ค๋ฅธ ๋ชจ๋ ์์ฒญ์
.authenticated() // ์ธ์ฆ์ด ๋์ด์ผ ํ๋ค
.and()
.formLogin()
.loginPage("/auth/loginForm") // ์ธ์ฆ์ด ํ์ํ ์์ฒญ์ ์ด ๋ก๊ทธ์ธ ํผ์ผ๋ก ์จ๋ค
.loginProcessingUrl("/auth/loginProc") // ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ํด๋น ์ฃผ์๋ก ์์ฒญ์ด ์ค๋ ๋ก๊ทธ์ธ์ ๊ฐ๋ก์ฑ์ ๋์ ๋ก๊ทธ์ธ์ ํ๋ค.
.defaultSuccessUrl("/"); // ๋ก๊ทธ์ธ ์ฑ๊ณตํ๋ฉด "/"๋ก ๊ฐ๋ค.
// .failureUrl("/fail"); // ์คํจ์ url
}
// ์ฐธ๊ณ : .headers().frameOptions().disable() // ์์ดํ๋ ์ ์ ๊ทผ ๋ง๊ธฐ
// ์ฐธ๊ณ : .csrf().disable() // csrf ํ ํฐ ๋นํ์ฑํ (ํ
์คํธ์ ๊ฑธ์ด์ฃผ๋ ๊ฒ์ด ์ข์)
}
Spring securityใฏใใฆใผใถใผใ่ฆๆฑใใใฆใผใถใผๅใจใในใฏใผใใใใญใใฏใใฆใญใฐใคใณใใพใ.UserDetailsใฟใคใใฎUserใชใใธใงใฏใใไฝๆใใๅฟ ่ฆใใใใพใ.
ใญใฐใคใณใๅฎไบใใใจSpring Securityใปใใทใงใณใซใฆใผใถใผๆ ๅ ฑใ็ป้ฒใใๅฟ ่ฆใใใใพใใใใฆใผใถใผใชใใธใงใฏใใ็ป้ฒใใใใจใฏใงใใพใใ.UserDetailsใฟใคใใฎใฟ็ป้ฒใงใใพใ.
< PrincipalDetail >
// ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ๋ก๊ทธ์ธ ์์ฒญ์ ๊ฐ๋ก์ฑ์ ๋ก๊ทธ์ธ์ ์งํํ๊ณ ์๋ฃ๊ฐ ๋๋ฉด,
// UserDetails ํ์
์ ์ค๋ธ์ ํธ๋ฅผ ์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ณ ์ ํ ์ธ์
์ ์ฅ์์ ์ ์ฅํ๋ค.
public class PrincipalDetail implements UserDetails {
private User user; // ์ฝคํฌ์ง์
(๊ฐ์ฒด๋ฅผ ํ๊ณ ์๋ ๊ฒ)
public PrincipalDetail(User user) {
this.user = user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// ๊ณ์ ์ด ๋ง๋ฃ๋์ง ์์๋์ง ๋ฆฌํดํ๋ค. (true : ๋ง๋ฃ ์๋จ)
@Override
public boolean isAccountNonExpired() {
return true;
}
// ๊ณ์ ์ด ์ ๊ฒจ์๋์ง ์ ์ ๊ฒจ์๋์ง ๋ฆฌํดํ๋ค. (true : ์ ๊ธฐ์ง ์์)
@Override
public boolean isAccountNonLocked() {
return true;
}
// ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋ฃ๋์ง ์์๋์ง๋ฅผ ๋ฆฌํดํ๋ค. (true : ๋ง๋ฃ ์๋จ)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ๊ณ์ ์ด ํ์ฑํ(์ฌ์ฉ๊ฐ๋ฅ)์ธ์ง ๋ฆฌํดํ๋ค. (true : ํ์ฑํ)
@Override
public boolean isEnabled() {
return true;
}
// ๊ณ์ ์ด ๊ฐ๊ณ ์๋ ๊ถํ ๋ชฉ๋ก์ ๋ฆฌํดํ๋ค. (๊ถํ์ด ์ฌ๋ฌ๊ฐ ์์ ์ ์์ด์ ๋ฃจํ๋ฅผ ๋์์ผ ํ๋๋ฐ ํ์ฌ๋ ํ๋๋ง)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>(); // ArrayList๋ Collection ํ์
์ด๋ค. (์์)
// collectors.add(new GrantedAuthority() {
//
// @Override
// public String getAuthority() {
// return "ROLE_"+user.getRole(); // role ์ ๋ฐ์ ๋ ์์ "ROLE_" ๋ถ์ด๋ ๊ฒ(prefix)์ด ์คํ๋ง์ ๊ท์น, ์ฆ ROLE_USER ๊ฐ์ ์์ผ๋ก ๋ฆฌํด๋จ
// }
// });
collectors.add(()->{return "ROLE_"+ user.getRole();}); // ์ด์ฐจํผ add ์์ ๋ค์ด๊ฐ ํจ์๋ GrantedAuthority() ๋ฟ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค์์ผ๋ก ๊ฐ๋ฅ
return collectors;
}
}
PrincipalDetailใใปใใทใงใณใซไฟๅญใใพใ.ใใฎๆ็นใงใDBใซๆ ผ็ดใใใฆใใUserใชใใธใงใฏใใๅซใใๅฟ
่ฆใใใใใใใณใณใใคใซใ่กใใพใ.ใฟใคใใฏUserDetailsใงใชใใใฐใชใใชใใฎใงใใใใๅฎ็พใใพใ.
UserDetailsใๆก็จใใฆใใใในใฆใฎๆฝ่ฑก็ใชๆนๆณใไธๆธใใใใฆใใพใ.
ใณใณใใธใใใณใณใปใใ
// ์ปดํฌ์ง์
public class PrincipalDetail {
private User user; // ์ฝคํฌ์ง์
(๊ฐ์ฒด๋ฅผ ํ๊ณ ์๋ ๊ฒ) }
// ์์
public class PrincipalDetail extends User { }
- ์์ - 'IS-A' ๊ด๊ณ
- ์ปดํฌ์ง์
- 'HAS-A' ๊ด๊ณ
- DI - ?????
'IS-A', 'HAS-A' ๊ด๊ณ๋ ์์, ์์์ **์ ์ ์ธ ํด๋์ค** ๊ด๊ณ์์ ์ค๋ช
ํ๋ ๋ฐฉ์์ด๋ค.
**DI**๋ 'HAS-A' ๊ด๊ณ์์ **๋์ ์ผ๋ก ๋์ ๊ฐ์ฒด๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ**์ ๋ํ ๊ฒ์ด๋ค.
์ฆ, 'HAS-A' ๊ด๊ณ๋ก ๋ณด๋ฉด ์ธํฐํ์ด์ค(์ถ์ ํด๋์ค)์ ์์กดํ๋๋ก ๊ด๊ณ๋ฅผ ๋งบ๋ ๊ฒ๊น์ง ์ธ๋ฐ, DI๋ ์ฌ๊ธฐ์ ์ถ๊ฐํด์ ๋์ ์ผ๋ก ์ธํฐํ์ด์ค ๊ตฌํ ๊ฐ์ฒด๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ๊น์ง๋ฅผ ์ด์ผ๊ธฐํ๋ค.
< PrincipalDetailService >@Service // Bean ๋ฑ๋ก
public class PrincipalDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
// ์คํ๋ง์ด ๋ก๊ทธ์ธ ์์ฒญ์ ๊ฐ๋ก์ฑ ๋, username๊ณผ password ๋ณ์ 2๊ฐ๋ฅผ ๊ฐ๋ก์ฑ
// password ์ฒ๋ฆฌ๋ ์คํ๋ง์ด ์์์ ํจ
// ๋๋ username์ด ํด๋น DB์ ์๋์ง๋ง ํ์ธํด์ ๋ฆฌํดํด์ฃผ๋ฉด ๋จ
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username) // Optional ํ์
์ด๊ธฐ ๋๋ฌธ์ .orElseThrow
.orElseThrow(()->{
return new UsernameNotFoundException("ํด๋น ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค." + username);
});
return new PrincipalDetail(principal); // ์ด๋ ์ํ๋ฆฌํฐ ์ธ์
์ ์ ์ ์ ๋ณด ์ ์ฅ๋จ
}
}
็ถๆฟใฟใคใใฏUserDetailServiceใงใ.ไธๆธใใใใloadUserByUsername้ขๆฐใงใๅใๅใฃใใฆใผใถใผๅๅคๆฐใDBใซใใใใจใ็ขบ่ชใใพใ.
ใญใฐใคใณใ่ฆๆฑใใใจใloadUserByUsername้ขๆฐใ่ชๅ็ใซๅฎ่กใใใใฆใผใถใผๅใซๅฏพๅฟใใใฆใผใถใผใๆค็ดขใใใPrincipalDetailใฟใคใ(ใใชใใกใUserDetailsใฟใคใ)ใ่ฟใใใพใ.
->ใฆใผใถDetailsใฟใคใใงใใใใใใฆใผใถๆ ๅ ฑใฏใปใญใฅใขใปใใทใงใณใซๆ ผ็ดใใใพใ.
ใใฎ้ขๆฐใฏ้ๅฐใซๅฎ็พใใๅฟ ่ฆใใใ.ใใใง็งใใกใฏๆฌๅฝใซ(?)ใฆใผใถใผๆ ๅ ฑใๅซใใฆใชใฟใผใณใงใใพใ.
< UserRepository >
public interface UserRepository extends JpaRepository<User, Integer> {
// SELECT * FROM user WHERE username = 1?;
Optional<User> findByUsername(String username);
}
findByUsernameใใชใใใใUserRepositoryใซใใฃใฆไฝๆใใใพใ.(ๅฝๅ่ฆๅ) // ์ํ๋ฆฌํฐ๊ฐ ๋์ ๋ก๊ทธ์ธ ํจ -> password ๊ฐ๋ก์ฑ
// ๊ฐ๋ก์ฑ password๊ฐ ํ์๊ฐ์
๋ ๋ ๋ฌด์์ผ๋ก ํด์ฌ๊ฐ ๋์๋์ง ์์์ผํจ -> ๊ทธ๋์ผ ๊ฐ์ ํด์ฌ๋ก ์ํธํ ํ๊ณ DB์ ์๋ ํด์ฌ์ ๋น๊ตํ์ฌ ๋ก๊ทธ์ธ
// ์ฆ, ํจ์ค์๋ ๋น๊ตํ๋ ๋ฉ์๋
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD()); // passwordEncoder ํ๋ ์ ๊ฐ encodePWD ์.
}
ใใใงใฏ,็ป้ฒใใใในใฏใผใใ็ฌฆๅทๅๆฏ่ผใใ.<ใปใญใฅใชใใฃๆงๆ>ๆ็ต็
@Configuration // ๋น ๋ฑ๋ก (IoC๊ด๋ฆฌ)
@EnableWebSecurity // Security ํํฐ๊ฐ ๋ฑ๋ก๋จ = ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ์ด๋ฏธ ํ์ฑํ๋ ๋์ด์์ง๋ง, ์ค์ ์ ํด๋น ํ์ผ์์ ํ ๊ฒ์
@EnableGlobalMethodSecurity(prePostEnabled = true) // ํน์ ์ฃผ์๋ก ์ ๊ทผ์ ํ๋ฉด ๊ถํ ๋ฐ ์ธ์ฆ์ ๋ฏธ๋ฆฌ ์ฒดํฌ (์ํํ ํ์ ์ฒดํฌํ๋ ๊ฒ์ด ์๋)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PrincipalDetailService principalDetailService;
// 1. Bean ์ด๋
ธํ
์ด์
์ ๋ฉ์๋์ ๋ถ์ฌ์ ๊ฐ์ฒด ์์ฑ์ ์ฌ์ฉ
@Bean // IoC๊ฐ ๋๋ค.
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder(); // ์ด ๊ฐ์ฒด๋ฅผ ์คํ๋ง์ด ๊ด๋ฆฌํ๊ฒ ๋จ. ํ์ํ ๋๋ง๋ค ๊ฐ์ ธ๊ฐ์ ์ฐ๋ฉด ๋๋ค.
}
// 2. ์ํ๋ฆฌํฐ๊ฐ ๋ก๊ทธ์ธํ ๋ ์ด๋ค ์ํธํ๋ก ์ธ์ฝ๋ฉํด์ ๋น๋ฒ์ ๋น๊ตํ ์ง ์๋ ค์ค์ผ ํจ.
// ์ํ๋ฆฌํฐ๊ฐ ๋์ ๋ก๊ทธ์ธ ํจ -> password ๊ฐ๋ก์ฑ
// ๊ฐ๋ก์ฑ password๊ฐ ํ์๊ฐ์
๋ ๋ ๋ฌด์์ผ๋ก ํด์ฌ๊ฐ ๋์๋์ง ์์์ผํจ -> ๊ทธ๋์ผ ๊ฐ์ ํด์ฌ๋ก ์ํธํ ํ๊ณ DB์ ์๋ ํด์ฌ์ ๋น๊ตํ์ฌ ๋ก๊ทธ์ธ
// ์ฆ, ํจ์ค์๋ ๋น๊ตํ๋ ๋ฉ์๋
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD()); // passwordEncoder ํ๋ ์ ๊ฐ encodePWD ์.
}
// 3. ํํฐ๋ง
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // csrf ํ ํฐ ๋นํ์ฑํ (ํ
์คํธ์ ๊ฑธ์ด๋๋๊ฒ ์ข์)
.authorizeRequests() // request๊ฐ ๋ค์ด์ค๋ฉด
.antMatchers("/", "/auth/**", "/js/**", "/css/**", "/image/**") // ์ฌ๊ธฐ๋ก ๋ค์ด์ค๋ฉด
.permitAll() // ๋ชจ๋ ๊ฐ๋ฅ (๋๊ตฌ๋ ๊ฐ๋ฅ)
.anyRequest() // ๊ทธ๊ฒ ์๋ ๋ค๋ฅธ ๋ชจ๋ ์์ฒญ์
.authenticated() // ์ธ์ฆ์ด ๋์ด์ผ ํ๋ค
.and()
.formLogin()
.loginPage("/auth/loginForm") // ์ธ์ฆ์ด ํ์ํ ์์ฒญ์ ์ด ๋ก๊ทธ์ธ ํผ์ผ๋ก ์จ๋ค
.loginProcessingUrl("/auth/loginProc") // ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ํด๋น ์ฃผ์๋ก ์์ฒญ์ด ์ค๋ ๋ก๊ทธ์ธ์ ๊ฐ๋ก์ฑ์ ๋์ ๋ก๊ทธ์ธ์ ํ๋ค.
.defaultSuccessUrl("/"); // ๋ก๊ทธ์ธ ์ฑ๊ณตํ๋ฉด "/"๋ก ๊ฐ๋ค.
// .failureUrl("/fail"); // ์คํจ์ url
}
// ์ฐธ๊ณ : .headers().frameOptions().disable() // ์์ดํ๋ ์ ์ ๊ทผ ๋ง๊ธฐ
// ์ฐธ๊ณ : .csrf().disable() // csrf ํ ํฐ ๋นํ์ฑํ (ํ
์คํธ์ ๊ฑธ์ด์ฃผ๋ ๊ฒ์ด ์ข์)
}
ใใใ่กใใจใSpring Securityใปใใทใงใณใซใฆใผใถใผๆ
ๅ ฑใไฟๅญใใใพใ.UserDetailใฟใคใใงPrincipalDetailใไฝๆใใPrincipalDetailใไฝฟ็จใใฆใใใฑใผใธๅใใฆไฟๅญใใพใ.
ใงใฏใspring securityใไฝๆใใใปใใทใงใณๆ ๅ ฑใใณใณใใญใผใฉใงๆค็ดขใใใซใฏใฉใใใใฐใใใฎใงใใใใ.
@GetMapping({"", "/"})
public String index(@AuthenticationPrincipal PrincipalDetail principal) { // ์ปจํธ๋กค๋ฌ์์ ๋ก๊ทธ์ธ ๋ ์ธ์
์ ์ฐพ๋ ๋ฐฉ์ : @AuthenticationPrincipal PrincipalDetail principal
// /WEB-INF/views/index.jsp
System.out.println("๋ก๊ทธ์ธ ์ฌ์ฉ์ ์์ด๋:"+principal.getUsername());
return "index";
}
ใใฉใกใผใฟใจใใฆPrincipalDetail principal
ใๅ
ฅๅใใพใ.@Autowired private PrincipalDetail principal;
ใไพๅญๆณจๅ
ฅๆนๅผใงๆณจๅ
ฅใใใใจใฏไธๅฏ่ฝใงใใ.Reference
ใใฎๅ้กใซใคใใฆ(๐ฅ #10ในใใชใณใฐๅฎๅ จ็ป้ฒใ่กใ), ๆใ ใฏใใใๅคใใฎๆ ๅ ฑใใใใง่ฆใคใใพใใ https://velog.io/@rladuswl/10-์คํ๋ง-์ํ๋ฆฌํฐ-๋ก๊ทธ์ธ-์งํใใญในใใฏ่ช็ฑใซๅ ฑๆใพใใฏใณใใผใงใใพใใใใ ใใใใฎใใญใฅใกใณใใฎURLใฏๅ่URLใจใใฆๆฎใใฆใใใฆใใ ใใใ
Collection and Share based on the CC Protocol