commit 0111ded181bdfa24256727810a43d9fe012ead7b Author: kimscott Date: Mon Sep 2 11:19:16 2019 +0900 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b93c1ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +/target/ +/bin/ +/.settings/ +*# +*.iml +*.ipr +*.iws +*.jar +*.sw? +*~ +.#* +.*.md.html +.DS_Store +.classpath +.factorypath +.gradle +.idea +.metadata +.project +.recommenders +.settings +.springBeans +/build +/code +MANIFEST.MF +_site/ +activemq-data +bin +build +build.log +dependency-reduced-pom.xml +dump.rdb +interpolated*.xml +lib/ +manifest.yml +overridedb.* +settings.xml +target +transaction-logs +.flattened-pom.xml +secrets.yml +.gradletasknamecache +.sts4-cache +node_modules +.dist/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..26ec245 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:8u212-jdk-alpine +COPY target/*SNAPSHOT.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-Xmx400M","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar","--spring.profiles.active=docker"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a627a7 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +## 인증토큰을 얻는 방법 + +## 1. postman 에서 확인 +-- url 주소 POST +http://localhost:8090/oauth/token + +Type : Basic Auth +Username : uengine-client +Password : uengine-secret + +-- body 부분 +grant_type : password +username : 4@4.com +password : password + + +## httpie 로 테스트 +http --form POST localhost:8090/oauth/token \ +"Authorization: Basic dWVuZ2luZS1jbGllbnQ6dWVuZ2luZS1zZWNyZXQ=" \ +grant_type=password \ +username=4@4.com \ +password=password + +## jks 파일 생성 방법 +https://www.lesstif.com/pages/viewpage.action?pageId=20775436 + diff --git a/boot-camp-oauth.iml b/boot-camp-oauth.iml new file mode 100644 index 0000000..aa900bd --- /dev/null +++ b/boot-camp-oauth.iml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..1eafe33 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,77 @@ +steps: + ### Build + - id: 'build' + name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: + - '-c' + - | + echo '$COMMIT_SHA =' $COMMIT_SHA + docker build -t gcr.io/$PROJECT_ID/$_PROJECT_NAME:$COMMIT_SHA . + ### Test + ### Publish + - id: 'publish' + name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: + - '-c' + - | + docker push gcr.io/$PROJECT_ID/$_PROJECT_NAME:$COMMIT_SHA + ### deploy + - id: 'deploy' + name: 'gcr.io/cloud-builders/gcloud' + entrypoint: 'bash' + args: + - '-c' + - | + PROJECT=$$(gcloud config get-value core/project) + gcloud container clusters get-credentials "$${CLOUDSDK_CONTAINER_CLUSTER}" \ + --project "$${PROJECT}" \ + --zone "$${CLOUDSDK_COMPUTE_ZONE}" + + cat < + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.4.RELEASE + + + com.example + boot-camp-oauth + 0.0.1-SNAPSHOT + boot-camp-oauth + Demo project for Spring Boot + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + 2.1.4.RELEASE + + + org.springframework.boot + spring-boot-starter-jdbc + + + mysql + mysql-connector-java + runtime + + + org.apache.commons + commons-lang3 + + + org.projectlombok + lombok + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/example/template/AuthorizationServerApplication.java b/src/main/java/com/example/template/AuthorizationServerApplication.java new file mode 100755 index 0000000..a1eafd0 --- /dev/null +++ b/src/main/java/com/example/template/AuthorizationServerApplication.java @@ -0,0 +1,46 @@ +package com.example.template; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import com.example.template.entity.User; +import com.example.template.repository.UserRepository; + + +@SpringBootApplication +public class AuthorizationServerApplication implements CommandLineRunner { + + public static void main( String[] args ) + { + SpringApplication.run(AuthorizationServerApplication.class, args); + } + + // TODO 삭제 - 테스트로 유저를 넣어서 확인하는 코드입니다. +// @Autowired +// private UserRepository repository; +// @Autowired private PasswordEncoder passwordEncoder; + + @Override + public void run(String... args) throws Exception { + +// User user = new User(); +// user.setId(3l); +// user.setUsername("4@4.com"); +// user.setPassword(passwordEncoder.encode("password")); +// user.setRole("USER_ADMIN"); +// +// repository.save(user); + + } + + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/com/example/template/config/OAuth2AuthorizationServerConfig.java b/src/main/java/com/example/template/config/OAuth2AuthorizationServerConfig.java new file mode 100755 index 0000000..bcb3ec1 --- /dev/null +++ b/src/main/java/com/example/template/config/OAuth2AuthorizationServerConfig.java @@ -0,0 +1,188 @@ +package com.example.template.config; + +import com.example.template.entity.User; +import com.example.template.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.token.TokenEnhancer; +import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; + +import java.io.PrintWriter; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + + +@Configuration +@EnableAuthorizationServer +public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + + public static final String CLIENT_ID = "uengine-client"; + public static final String CLIENT_SECRET = "uengine-secret"; + static final String GRANT_TYPE_PASSWORD = "password"; + static final String AUTHORIZATION_CODE = "authorization_code"; + static final String CLIENT_CREDENTIALS = "client_credentials"; + static final String REFRESH_TOKEN = "refresh_token"; + static final String IMPLICIT = "implicit"; + static final String SCOPE_READ = "read"; + static final String SCOPE_WRITE = "write"; + static final String TRUST = "trust"; + static final int ACCESS_TOKEN_VALIDITY_SECONDS = 24*60*60; // 24시간 + static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60; + + @Autowired + @Qualifier("authenticationManagerBean") + private AuthenticationManager authenticationManager; + + @Autowired + PasswordEncoder passwordEncoder; + @Autowired + UserRepository userRepository; + + @Autowired + private Environment env; + + /** + * DB 설정을 별도로 하게 되면 에러가 발생한다. + * @throws Exception + */ +// @Bean +// public DataSource oauthDataSource() { +// +// DriverManagerDataSource dataSource = new DriverManagerDataSource(); +// dataSource.setDriverClassName(env.getProperty("spring.datasource.driverClassName")); +// dataSource.setUrl(env.getProperty("spring.datasource.url")); +// dataSource.setUsername(env.getProperty("spring.datasource.username")); +// dataSource.setPassword(env.getProperty("spring.datasource.password")); +// return dataSource; +// } + + @Override + public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + security.accessDeniedHandler((request, response, exception)->{ + response.setContentType("application/json;charset=UTF-8"); + response.setHeader("Cache-Control", "no-cache"); + PrintWriter writer = response.getWriter(); + writer.println(new AccessDeniedException("access denied !")); + }) + .authenticationEntryPoint((request, response, exception)->{ + response.setContentType("application/json;charset=UTF-8"); + response.setHeader("Cache-Control", "no-cache"); + PrintWriter writer = response.getWriter(); + writer.println(new AccessDeniedException("access denied !")); + }) + ; + } + + /** + * 클라이언트 인증을 인메모리로 하였지만 클라이언트를 등록 및 별도로 관리하려면 + * clientDetailsService 를 만든 후에 + * clients.withClientDetails(clientDetailsService); + * 을 사용하면 된다. + * @param clients + * @throws Exception + */ + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { +// clients.withClientDetails(clientDetailsService); + + clients + .inMemory() + .withClient(CLIENT_ID) + .secret(passwordEncoder.encode(CLIENT_SECRET)) + .authorizedGrantTypes(GRANT_TYPE_PASSWORD, CLIENT_CREDENTIALS, REFRESH_TOKEN, IMPLICIT ) + .scopes(SCOPE_READ, SCOPE_WRITE, TRUST) + .authorities("ROLE_CLIENT","ROLE_TRUSTED_CLIENT") + .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS). + refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS); + } + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + + TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); + tokenEnhancerChain.setTokenEnhancers( + Arrays.asList(tokenEnhancer(), accessTokenConverter())); + + endpoints + .authenticationManager(authenticationManager) + .tokenStore(tokenStore()) //토큰과 관련된 인증 데이터를 저장, 검색, 제거, 읽기를 정의 + .tokenEnhancer(tokenEnhancerChain) + ; + } + + @Bean + public JwtTokenStore tokenStore() { + return new JwtTokenStore(accessTokenConverter()); + } + + @Bean + public KeyPair makeKeyPair(){ + KeyPair keyPair = new KeyStoreKeyFactory( + new ClassPathResource("server.jks"), "qweqwe".toCharArray()) + .getKeyPair("uengine", "qweqwe".toCharArray()); + return keyPair; + } + + /** + * JWT 토큰을 sign 하는 방법중 간단하게 쓰려면 Key 값을 쓰면되고, + * jks(java key store) 파일로 쓰려면 keypair 방식을 사용하면 됨 + */ + @Bean + public JwtAccessTokenConverter accessTokenConverter() { + JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); + converter.setKeyPair(this.makeKeyPair()); // use Spring cloud gateway +// converter.setSigningKey("non-prod-signature");// use zuul 1 + return converter; + } + + + /** + * JWT 토큰을 생성할때 추가적인 정보를 넣어주는 부분이다. + * 유저의 상세 정보를 추가하고 싶을때는 userService or userRepository 를 Autowired 하여 조회하여 데이터를 추가할수있다. + * @return + */ + @Bean + public TokenEnhancer tokenEnhancer(){ + return (accessToken, authentication) -> { + + if(authentication.isAuthenticated()) { + Map additionalInfo = new HashMap<>(); + additionalInfo.put("company", "CLT"); +// String clientId = authentication.getOAuth2Request().getClientId(); +// logger.debug("client ID : " + clientId); + +// ClientDetails client = clientService.loadClientByClientId(clientId); +// Map addInfo = client.getAdditionalInformation(); +//// logger.debug("client : " + client.toString()); +// +// if(addInfo!=null){ +// for(String key:addInfo.keySet()){ +// additionalInfo.put(key, addInfo.get(key)); +// } +// } + + ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); + } + return accessToken; + }; + + } +} diff --git a/src/main/java/com/example/template/config/WebSecurityConfig.java b/src/main/java/com/example/template/config/WebSecurityConfig.java new file mode 100755 index 0000000..e007fbb --- /dev/null +++ b/src/main/java/com/example/template/config/WebSecurityConfig.java @@ -0,0 +1,121 @@ +package com.example.template.config; + +import com.example.template.service.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import javax.annotation.PostConstruct; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ + + @Autowired + private WebApplicationContext applicationContext; + private UserDetailsServiceImpl userDetailsService; + + @Autowired + PasswordEncoder passwordEncoder; + + private final AuthenticationManager authenticationManager; + + @PostConstruct + public void completeSetup() { + userDetailsService = applicationContext.getBean(UserDetailsServiceImpl.class); + } + + /** + * 생성자에 @Lazy 를 쓰는 이유는 Autowired 시 bean 이 등록된 후에 실행을 하기 위해서 사용되어진다. + * @param authenticationManager + */ + public WebSecurityConfig( @Lazy AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void configure(WebSecurity web) throws Exception { + + web.ignoring() + .antMatchers("/css/**") + .antMatchers("/vendor/**") + .antMatchers("/js/**") + .antMatchers("/favicon*/**") + .antMatchers("/img/**") + ; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService) + .passwordEncoder(passwordEncoder); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /** + * websecurity 부분에 핵심적인 endpoint 를 설정해 주는 부분이다. + * 특정 uri 를 허락하고싶다면 + * .antMatchers("/login").permitAll() 등으로 사용이 가능하다. + * 현재는 cors 의 preflight 부분만 허용하고 있다. + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + + http + .cors() + .and() + .authorizeRequests() + .antMatchers("/login").permitAll() + .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() + .anyRequest().authenticated() + .and() + .csrf() + .disable() + ; + } + + /** + * CORS 셋팅 + * 만약 gateway 를 통해서 oauth 서버를 접근한다면 삭제해 줘야함 + * @return + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +} diff --git a/src/main/java/com/example/template/entity/User.java b/src/main/java/com/example/template/entity/User.java new file mode 100755 index 0000000..b63a780 --- /dev/null +++ b/src/main/java/com/example/template/entity/User.java @@ -0,0 +1,57 @@ +package com.example.template.entity; + +import java.util.Collection; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ToString +@Entity +@Table(name = "user") +public class User implements UserDetails{ + + @Id + @Getter@Setter + private Long id; + + @Getter@Setter + private String username; + + @Column(length=400) + @Getter@Setter + private String password; + + @Column + @Getter@Setter + private String role; + + @Transient + @Getter@Setter + private Collection authorities; + + @Getter@Setter + private boolean accountNonExpired = true; + + @Getter@Setter + private boolean accountNonLocked = true; + + @Getter@Setter + private boolean credentialsNonExpired = true; + + @Getter@Setter + private boolean enabled = true; + + +} diff --git a/src/main/java/com/example/template/repository/UserRepository.java b/src/main/java/com/example/template/repository/UserRepository.java new file mode 100755 index 0000000..418b97a --- /dev/null +++ b/src/main/java/com/example/template/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.example.template.repository; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; +import com.example.template.entity.User; + +@Repository +public interface UserRepository extends CrudRepository { + User findByUsername(String username); +} diff --git a/src/main/java/com/example/template/service/UserDetailsServiceImpl.java b/src/main/java/com/example/template/service/UserDetailsServiceImpl.java new file mode 100755 index 0000000..5af0626 --- /dev/null +++ b/src/main/java/com/example/template/service/UserDetailsServiceImpl.java @@ -0,0 +1,47 @@ +package com.example.template.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +import lombok.extern.slf4j.Slf4j; +import com.example.template.entity.User; +import com.example.template.repository.UserRepository; +import org.springframework.web.context.WebApplicationContext; + +import javax.annotation.PostConstruct; + + +@Slf4j +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private WebApplicationContext applicationContext; + private UserRepository repository; + + @PostConstruct + public void completeSetup() { + repository = applicationContext.getBean(UserRepository.class); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + log.info("UserDetailsServiceImpl.loadUserByUsername :::: {}",username); + + User user = repository.findByUsername(username); + + if(ObjectUtils.isEmpty(user)) { + throw new UsernameNotFoundException("Invalid resource owner, please check resource owner info !"); + } + + user.setAuthorities(AuthorityUtils.createAuthorityList(String.valueOf(user.getRole()))); + + return user; + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100755 index 0000000..f2a89b2 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,23 @@ +spring: + jpa: + hibernate: + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL57Dialect + show_sql: true + datasource: + url: jdbc:mysql://104.198.86.212:3306/uengine + username: root + password: test1234!@ + # mybatis 설정시에는 driverClassName 을 넣어주어야함 + driverClassName: com.mysql.cj.jdbc.Driver + +# --- server +server: + port: 8090 + + + diff --git a/src/main/resources/server.jks b/src/main/resources/server.jks new file mode 100644 index 0000000..ad255a9 Binary files /dev/null and b/src/main/resources/server.jks differ