From b3c82396ac8eb2ea4e81df7f1f58cdd067b94372 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 13 Apr 2022 17:08:34 +0800 Subject: [PATCH] feat: Add security config for halo (#1838) * feat: Add security config for halo * chore: delete file * feat: add security config intergration test case * refactor: use EnableWebSecurity annotation * refactor: patch prefix * Update src/test/java/run/halo/app/integration/security/AuthenticationTest.java Co-authored-by: John Niang * refactor: mvc path prefix config Co-authored-by: John Niang --- build.gradle | 3 + .../run/halo/app/config/WebMvcConfig.java | 27 +++++ .../halo/app/config/WebSecurityConfig.java | 107 ++++++++++++++++++ .../authentication/OauthController.java | 19 ++++ .../entrypoint/JwtAccessDeniedHandler.java | 57 ++++++++++ .../JwtAuthenticationEntryPoint.java | 31 +++++ .../app/infra/properties/HaloProperties.java | 17 +++ .../app/infra/properties/JwtProperties.java | 86 ++++++++++++++ .../run/halo/app/infra/utils/HaloUtils.java | 14 +++ src/main/resources/app.key | 28 +++++ src/main/resources/app.pub | 9 ++ src/main/resources/application.yaml | 6 + .../run/halo/app/PathPrefixPredicateTest.java | 30 +++++ .../security/AuthenticationTest.java | 83 ++++++++++++++ src/test/resources/app.key | 28 +++++ src/test/resources/app.pub | 9 ++ 16 files changed, 554 insertions(+) create mode 100644 src/main/java/run/halo/app/config/WebMvcConfig.java create mode 100644 src/main/java/run/halo/app/config/WebSecurityConfig.java create mode 100644 src/main/java/run/halo/app/identity/authentication/OauthController.java create mode 100644 src/main/java/run/halo/app/identity/entrypoint/JwtAccessDeniedHandler.java create mode 100644 src/main/java/run/halo/app/identity/entrypoint/JwtAuthenticationEntryPoint.java create mode 100644 src/main/java/run/halo/app/infra/properties/HaloProperties.java create mode 100644 src/main/java/run/halo/app/infra/properties/JwtProperties.java create mode 100644 src/main/java/run/halo/app/infra/utils/HaloUtils.java create mode 100644 src/main/resources/app.key create mode 100644 src/main/resources/app.pub create mode 100644 src/test/java/run/halo/app/PathPrefixPredicateTest.java create mode 100644 src/test/java/run/halo/app/integration/security/AuthenticationTest.java create mode 100644 src/test/resources/app.key create mode 100644 src/test/resources/app.pub diff --git a/build.gradle b/build.gradle index 00e3af5cb..d79c45a76 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-oauth2-jose' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation "org.springframework.boot:spring-boot-starter-jetty" @@ -55,6 +56,8 @@ dependencies { implementation "org.flywaydb:flyway-mysql" compileOnly 'org.projectlombok:lombok' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly "com.h2database:h2" diff --git a/src/main/java/run/halo/app/config/WebMvcConfig.java b/src/main/java/run/halo/app/config/WebMvcConfig.java new file mode 100644 index 000000000..a0e10f204 --- /dev/null +++ b/src/main/java/run/halo/app/config/WebMvcConfig.java @@ -0,0 +1,27 @@ +package run.halo.app.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerTypePredicate; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import run.halo.app.Application; + +/** + * Spring web mvc config. + * + * @author guqing + * @date 2022-04-12 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.addPathPrefix("/api/v1", + c -> HandlerTypePredicate.forAnnotation(RestController.class) + .and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName())) + .test(c) + ); + } +} diff --git a/src/main/java/run/halo/app/config/WebSecurityConfig.java b/src/main/java/run/halo/app/config/WebSecurityConfig.java new file mode 100644 index 000000000..8b04c7e4f --- /dev/null +++ b/src/main/java/run/halo/app/config/WebSecurityConfig.java @@ -0,0 +1,107 @@ +package run.halo.app.config; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import java.io.IOException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.Customizer; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler; +import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint; +import run.halo.app.infra.properties.JwtProperties; + +/** + * @author guqing + * @date 2022-04-12 + */ +@EnableWebSecurity +@EnableConfigurationProperties(JwtProperties.class) +public class WebSecurityConfig { + + private final RSAPublicKey key; + + private final RSAPrivateKey priv; + + public WebSecurityConfig(JwtProperties jwtProperties) throws IOException { + this.key = jwtProperties.readPublicKey(); + this.priv = jwtProperties.readPrivateKey(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .antMatchers("/api/**", "/apis/**").authenticated() + ) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(Customizer.withDefaults()) + .sessionManagement( + (session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) + .accessDeniedHandler(new JwtAccessDeniedHandler()) + ); + // @formatter:on + return http.build(); + } + + @Bean + UserDetailsService users() { + // @formatter:off + return new InMemoryUserDetailsManager( + User.withUsername("user") + .password("{noop}password") + .authorities("app") + .build() + ); + // @formatter:on + } + + @Bean + JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey(this.key).build(); + } + + @Bean + JwtEncoder jwtEncoder() { + JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build(); + JWKSource jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); + return new NimbusJwtEncoder(jwks); + } + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User.withUsername("user") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } +} diff --git a/src/main/java/run/halo/app/identity/authentication/OauthController.java b/src/main/java/run/halo/app/identity/authentication/OauthController.java new file mode 100644 index 000000000..7e6daa305 --- /dev/null +++ b/src/main/java/run/halo/app/identity/authentication/OauthController.java @@ -0,0 +1,19 @@ +package run.halo.app.identity.authentication; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author guqing + * @date 2022-04-12 + */ +@RestController +@RequestMapping("/oauth") +public class OauthController { + + @GetMapping("login") + public String login() { + return "hello"; + } +} diff --git a/src/main/java/run/halo/app/identity/entrypoint/JwtAccessDeniedHandler.java b/src/main/java/run/halo/app/identity/entrypoint/JwtAccessDeniedHandler.java new file mode 100644 index 000000000..dab11c37d --- /dev/null +++ b/src/main/java/run/halo/app/identity/entrypoint/JwtAccessDeniedHandler.java @@ -0,0 +1,57 @@ +package run.halo.app.identity.entrypoint; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.util.Assert; +import run.halo.app.infra.utils.HaloUtils; + +/** + * @author guqing + * @date 2022-04-12 + */ +@Slf4j +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + private String errorPage; + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + if (response.isCommitted()) { + log.trace("Did not write to response since already committed"); + return; + } + if (this.errorPage == null) { + log.debug("Responding with 403 status code"); + response.sendError(HttpStatus.FORBIDDEN.value(), + HttpStatus.FORBIDDEN.getReasonPhrase()); + return; + } + // Put exception into request scope (perhaps of use to a view) + request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); + // Set the 403 status code. + response.setStatus(HttpStatus.FORBIDDEN.value()); + // forward to error page. + if (log.isDebugEnabled()) { + log.debug("Forwarding to [{}] with status code 403", this.errorPage); + } + if (HaloUtils.isAjaxRequest(request)) { + response.getWriter().write("access denied!"); + return; + } + request.getRequestDispatcher(this.errorPage).forward(request, response); + } + + public void setErrorPage(String errorPage) { + Assert.isTrue(errorPage == null || errorPage.startsWith("/"), + "errorPage must begin with '/'"); + this.errorPage = errorPage; + } +} diff --git a/src/main/java/run/halo/app/identity/entrypoint/JwtAuthenticationEntryPoint.java b/src/main/java/run/halo/app/identity/entrypoint/JwtAuthenticationEntryPoint.java new file mode 100644 index 000000000..9ac32ea84 --- /dev/null +++ b/src/main/java/run/halo/app/identity/entrypoint/JwtAuthenticationEntryPoint.java @@ -0,0 +1,31 @@ +package run.halo.app.identity.entrypoint; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import run.halo.app.infra.utils.HaloUtils; + +/** + * @author guqing + * @date 2022-04-12 + */ +@Slf4j +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + log.warn("Unauthorized error: {}", authException.getMessage()); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + if (HaloUtils.isAjaxRequest(request)) { + response.getWriter().write(HttpStatus.UNAUTHORIZED.getReasonPhrase()); + return; + } + response.sendError(HttpStatus.UNAUTHORIZED.value(), + HttpStatus.UNAUTHORIZED.getReasonPhrase()); + } +} diff --git a/src/main/java/run/halo/app/infra/properties/HaloProperties.java b/src/main/java/run/halo/app/infra/properties/HaloProperties.java new file mode 100644 index 000000000..8eec0d7f4 --- /dev/null +++ b/src/main/java/run/halo/app/infra/properties/HaloProperties.java @@ -0,0 +1,17 @@ +package run.halo.app.infra.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author guqing + * @date 2022-04-12 + */ +@ConfigurationProperties(prefix = "halo") +public class HaloProperties { + + private final JwtProperties jwt = new JwtProperties(); + + public JwtProperties getJwt() { + return jwt; + } +} diff --git a/src/main/java/run/halo/app/infra/properties/JwtProperties.java b/src/main/java/run/halo/app/infra/properties/JwtProperties.java new file mode 100644 index 000000000..991a6997c --- /dev/null +++ b/src/main/java/run/halo/app/infra/properties/JwtProperties.java @@ -0,0 +1,86 @@ +package run.halo.app.infra.properties; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.core.io.Resource; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +/** + * @author guqing + * @date 2022-04-12 + */ +@ConfigurationProperties(prefix = "halo.security.oauth2.jwt") +public class JwtProperties { + + /** + * JSON Web Algorithm used for verifying the digital signatures. + */ + private String jwsAlgorithm = "RS256"; + + /** + * Location of the file containing the public key used to verify a JWT. + */ + private Resource publicKeyLocation; + + private Resource privateKeyLocation; + + public String getJwsAlgorithm() { + return this.jwsAlgorithm; + } + + public void setJwsAlgorithm(String jwsAlgorithm) { + this.jwsAlgorithm = jwsAlgorithm; + } + + public Resource getPublicKeyLocation() { + return this.publicKeyLocation; + } + + public void setPublicKeyLocation(Resource publicKeyLocation) { + this.publicKeyLocation = publicKeyLocation; + } + + public Resource getPrivateKeyLocation() { + return privateKeyLocation; + } + + public void setPrivateKeyLocation(Resource privateKeyLocation) { + this.privateKeyLocation = privateKeyLocation; + } + + public RSAPublicKey readPublicKey() throws IOException { + String key = "halo.security.oauth2.jwt.public-key-location"; + Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null"); + if (!this.publicKeyLocation.exists()) { + throw new InvalidConfigurationPropertyValueException(key, this.publicKeyLocation, + "Public key location does not exist"); + } + try (InputStream inputStream = this.publicKeyLocation.getInputStream()) { + String source = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + return RsaKeyConverters.x509() + .convert(new ByteArrayInputStream(source.getBytes())); + } + } + + public RSAPrivateKey readPrivateKey() throws IOException { + String key = "halo.security.oauth2.jwt.private-key-location"; + Assert.notNull(this.privateKeyLocation, "PrivateKeyLocation must not be null"); + if (!this.privateKeyLocation.exists()) { + throw new InvalidConfigurationPropertyValueException(key, this.privateKeyLocation, + "Private key location does not exist"); + } + try (InputStream inputStream = this.privateKeyLocation.getInputStream()) { + String source = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + return RsaKeyConverters.pkcs8() + .convert(new ByteArrayInputStream(source.getBytes())); + } + } +} diff --git a/src/main/java/run/halo/app/infra/utils/HaloUtils.java b/src/main/java/run/halo/app/infra/utils/HaloUtils.java new file mode 100644 index 000000000..c6cab57f8 --- /dev/null +++ b/src/main/java/run/halo/app/infra/utils/HaloUtils.java @@ -0,0 +1,14 @@ +package run.halo.app.infra.utils; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * @author guqing + * @date 2022-04-12 + */ +public class HaloUtils { + public static boolean isAjaxRequest(HttpServletRequest request) { + String requestedWith = request.getHeader("x-requested-with"); + return requestedWith == null || requestedWith.equalsIgnoreCase("XMLHttpRequest"); + } +} diff --git a/src/main/resources/app.key b/src/main/resources/app.key new file mode 100644 index 000000000..5bbada69e --- /dev/null +++ b/src/main/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOjnDY1K1lrOrK +ETfKfDlVGVbPCiy+TDmTaXg4SWjdHUpXfqbXMkSX/j2dJ/ECqb/FtsvVxiSwRieG +3MWDKWlNRz0C0QKrsoDYbcvLf68uc7L5eKFZhu0AkXP4T5BIbdMXH8V0+5e+6R+n +eHahFhMyaiYoHVrPMrW2Jn9iWIXuNTDpg9VFhejN4jG1wQqIu1puKeGYPQvtfNO5 +Ef5cQdEFCvFfuDQvNhLgI1f798qY6EVFfRo2S3LLCut3wfDzRZiUN4Kz8qYz42Zv +97GS1gW/lfcEsmBApov9xiIaUzUECN35XbZYMK5Y4gfhAseZ+tlj+YarEiPjAtL1 +JPUehCmRAgMBAAECggEAKmaI+ammsoFtbO9d4X3gkvxxmmx/RM0G4KC84ekH0qPp +l85S10flVsIEydbiHWbVC/P7IbXb4Cd2g7OcA9GjYQ6nkoVvI+mvkz3uoKZkQofT +jGxbyrHswroY8Tb76jJJK60E7n+a5cCbE9ihmW2boTSzAncMJg5FyM9cRMbhL0Vz +h90/gE2U8awQ8Ug47BN1Dk/awxB9f5zVqI+LCGC0Py0/oQudjSaqPihydTsuqkhV +xNu3NMcL/POt9WxmYyJFDJRW3+EYraPumdUsIWw8p4JJDt1jkyNpSbjGhu8vzRYX +0QSo1pa3VrDY4guEMk4RdJsKJDqQPTvCTTgDYBzFlQKBgQDq98CRLTwqHSEWyVKN +0KRujhVAVEmLDvPxZ2tVaMM37RanCHYSfHLiYCD54rUv7BFWjQ+hfq3iHUpgrefN +KRS9e01mT0f24sAsWfhrFzrhlHaQStFgOw4uvwIDCfzrBeQQsqcAvWSjNr8CqSMX +UIGz9oB6EP39PT3QxT3oYf3ItwKBgQDhC6WN78+0sf2zlptQ7V02eWaRfePtQfmb +ow3c9aF8V7sSwDzjInqV5Bva4RyftYRTYZttBiANjGZ1pSNPi/2p7b+0hxJ1pPf8 +6VcFDJBGLbFYNDWOux13KRJToMY0ckzSeBXgkWLVFSfESuoXzy+8bj5eMavJLg6L +2Ek6q6mH9wKBgBZmmE0+6sV5EXaCqwQqKAMCOLRxVLGVM1yIZ4s0+aeTSt2RyO/q +PWmnkH1CR9PRxbVirWLQGPO9pyGgcsD0ca2+25otZMb8xyVzTmOnS03GQadv+pYa +CzgZra9sfFhLr3qIDbPcWoPU7FDsnxPR8QufLJB2nkBOXl5Q753/+ZnxAoGBAI47 +GisWwaNmSv3R1d/T5PGk0Jprgj5VUDh5WS2pYKKBoA49yT2UcP2C6cfwNnMJ+dPp +AJ5rHJ7zeV4pPKPtyig3xs2GALixxrnlj8X1Jsnz3v3sIV1QDVNedeK83ggPpVXv +54PC3z/k2vlIj6L0oyroUiqeIgBIR5FC5SVbkQ4JAoGBAOEGQkqw1xR3fd27J6/R +s9hOhItPnjExf5yqeg0nbZYIGd+6PiaVBBWUefZDDS79KUwTiqiHGP7iEVghJr9C +xJI9odzY8WQJ+Q9ZQy1VQfP5mkRUTTkABhykXfWsHckO7yP6c3kwNIOOki8QPrmY +3GKNb5HtQVpazCvrB5PFh65g +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/main/resources/app.pub b/src/main/resources/app.pub new file mode 100644 index 000000000..d1af67b4b --- /dev/null +++ b/src/main/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzo5w2NStZazqyhE3ynw5 +VRlWzwosvkw5k2l4OElo3R1KV36m1zJEl/49nSfxAqm/xbbL1cYksEYnhtzFgylp +TUc9AtECq7KA2G3Ly3+vLnOy+XihWYbtAJFz+E+QSG3TFx/FdPuXvukfp3h2oRYT +MmomKB1azzK1tiZ/YliF7jUw6YPVRYXozeIxtcEKiLtabinhmD0L7XzTuRH+XEHR +BQrxX7g0LzYS4CNX+/fKmOhFRX0aNktyywrrd8Hw80WYlDeCs/KmM+Nmb/exktYF +v5X3BLJgQKaL/cYiGlM1BAjd+V22WDCuWOIH4QLHmfrZY/mGqxIj4wLS9ST1HoQp +kQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4b318c42a..22791068e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -39,6 +39,12 @@ spring: locations: - classpath:db/migration/common - classpath:db/migration/{vendor} +halo: + security: + oauth2: + jwt: + public-key-location: classpath:app.pub + private-key-location: classpath:app.key logging: level: run.halo.app: DEBUG diff --git a/src/test/java/run/halo/app/PathPrefixPredicateTest.java b/src/test/java/run/halo/app/PathPrefixPredicateTest.java new file mode 100644 index 000000000..72c977d2b --- /dev/null +++ b/src/test/java/run/halo/app/PathPrefixPredicateTest.java @@ -0,0 +1,30 @@ +package run.halo.app; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerTypePredicate; +import run.halo.app.identity.authentication.OauthController; + +/** + * Test case for api path prefix predicate. + * + * @author guqing + * @date 2022-04-13 + */ +public class PathPrefixPredicateTest { + + @Test + public void prefixPredicate() { + boolean falseResult = HandlerTypePredicate.forAnnotation(RestController.class) + .and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName())) + .test(getClass()); + assertThat(falseResult).isFalse(); + + boolean result = HandlerTypePredicate.forAnnotation(RestController.class) + .and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName())) + .test(OauthController.class); + assertThat(result).isTrue(); + } +} diff --git a/src/test/java/run/halo/app/integration/security/AuthenticationTest.java b/src/test/java/run/halo/app/integration/security/AuthenticationTest.java new file mode 100644 index 000000000..1a9692797 --- /dev/null +++ b/src/test/java/run/halo/app/integration/security/AuthenticationTest.java @@ -0,0 +1,83 @@ +package run.halo.app.integration.security; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import jakarta.servlet.Filter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import run.halo.app.config.WebSecurityConfig; + +/** + * @author guqing + * @date 2022-04-12 + */ +@TestPropertySource(properties = {"halo.security.oauth2.jwt.public-key-location=classpath:app.pub", + "halo.security.oauth2.jwt.private-key-location=classpath:app.key"}) +@WebMvcTest +@Import(WebSecurityConfig.class) +public class AuthenticationTest { + + private MockMvc mockMvc; + + @Autowired + SecurityFilterChain securityFilterChain; + + @BeforeEach + public void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(AnonymousAllowedController.class, + SecuredController.class) + .addFilters(securityFilterChain.getFilters().toArray(new Filter[] {})) + .build(); + } + + @Test + public void allowAccessTest() throws Exception { + mockMvc.perform(get("/anonymous")) + .andDo(print()) + .andExpect(status().is2xxSuccessful()) + .andExpect(content().string("Hello, now you see me.")); + } + + @Test + public void securedApiTest() throws Exception { + mockMvc.perform(get("/api/secured")) + .andDo(print()) + .andExpect(status().is(HttpStatus.UNAUTHORIZED.value())) + .andExpect(content().string("Unauthorized")); + } + + @Controller + @RequestMapping("/anonymous") + static class AnonymousAllowedController { + @GetMapping + @ResponseBody + public String hello() { + return "Hello, now you see me."; + } + } + + @RestController + @RequestMapping("/api/secured") + static class SecuredController { + @GetMapping + public String hello() { + return "You can't see me."; + } + } +} diff --git a/src/test/resources/app.key b/src/test/resources/app.key new file mode 100644 index 000000000..5bbada69e --- /dev/null +++ b/src/test/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOjnDY1K1lrOrK +ETfKfDlVGVbPCiy+TDmTaXg4SWjdHUpXfqbXMkSX/j2dJ/ECqb/FtsvVxiSwRieG +3MWDKWlNRz0C0QKrsoDYbcvLf68uc7L5eKFZhu0AkXP4T5BIbdMXH8V0+5e+6R+n +eHahFhMyaiYoHVrPMrW2Jn9iWIXuNTDpg9VFhejN4jG1wQqIu1puKeGYPQvtfNO5 +Ef5cQdEFCvFfuDQvNhLgI1f798qY6EVFfRo2S3LLCut3wfDzRZiUN4Kz8qYz42Zv +97GS1gW/lfcEsmBApov9xiIaUzUECN35XbZYMK5Y4gfhAseZ+tlj+YarEiPjAtL1 +JPUehCmRAgMBAAECggEAKmaI+ammsoFtbO9d4X3gkvxxmmx/RM0G4KC84ekH0qPp +l85S10flVsIEydbiHWbVC/P7IbXb4Cd2g7OcA9GjYQ6nkoVvI+mvkz3uoKZkQofT +jGxbyrHswroY8Tb76jJJK60E7n+a5cCbE9ihmW2boTSzAncMJg5FyM9cRMbhL0Vz +h90/gE2U8awQ8Ug47BN1Dk/awxB9f5zVqI+LCGC0Py0/oQudjSaqPihydTsuqkhV +xNu3NMcL/POt9WxmYyJFDJRW3+EYraPumdUsIWw8p4JJDt1jkyNpSbjGhu8vzRYX +0QSo1pa3VrDY4guEMk4RdJsKJDqQPTvCTTgDYBzFlQKBgQDq98CRLTwqHSEWyVKN +0KRujhVAVEmLDvPxZ2tVaMM37RanCHYSfHLiYCD54rUv7BFWjQ+hfq3iHUpgrefN +KRS9e01mT0f24sAsWfhrFzrhlHaQStFgOw4uvwIDCfzrBeQQsqcAvWSjNr8CqSMX +UIGz9oB6EP39PT3QxT3oYf3ItwKBgQDhC6WN78+0sf2zlptQ7V02eWaRfePtQfmb +ow3c9aF8V7sSwDzjInqV5Bva4RyftYRTYZttBiANjGZ1pSNPi/2p7b+0hxJ1pPf8 +6VcFDJBGLbFYNDWOux13KRJToMY0ckzSeBXgkWLVFSfESuoXzy+8bj5eMavJLg6L +2Ek6q6mH9wKBgBZmmE0+6sV5EXaCqwQqKAMCOLRxVLGVM1yIZ4s0+aeTSt2RyO/q +PWmnkH1CR9PRxbVirWLQGPO9pyGgcsD0ca2+25otZMb8xyVzTmOnS03GQadv+pYa +CzgZra9sfFhLr3qIDbPcWoPU7FDsnxPR8QufLJB2nkBOXl5Q753/+ZnxAoGBAI47 +GisWwaNmSv3R1d/T5PGk0Jprgj5VUDh5WS2pYKKBoA49yT2UcP2C6cfwNnMJ+dPp +AJ5rHJ7zeV4pPKPtyig3xs2GALixxrnlj8X1Jsnz3v3sIV1QDVNedeK83ggPpVXv +54PC3z/k2vlIj6L0oyroUiqeIgBIR5FC5SVbkQ4JAoGBAOEGQkqw1xR3fd27J6/R +s9hOhItPnjExf5yqeg0nbZYIGd+6PiaVBBWUefZDDS79KUwTiqiHGP7iEVghJr9C +xJI9odzY8WQJ+Q9ZQy1VQfP5mkRUTTkABhykXfWsHckO7yP6c3kwNIOOki8QPrmY +3GKNb5HtQVpazCvrB5PFh65g +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/test/resources/app.pub b/src/test/resources/app.pub new file mode 100644 index 000000000..d1af67b4b --- /dev/null +++ b/src/test/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzo5w2NStZazqyhE3ynw5 +VRlWzwosvkw5k2l4OElo3R1KV36m1zJEl/49nSfxAqm/xbbL1cYksEYnhtzFgylp +TUc9AtECq7KA2G3Ly3+vLnOy+XihWYbtAJFz+E+QSG3TFx/FdPuXvukfp3h2oRYT +MmomKB1azzK1tiZ/YliF7jUw6YPVRYXozeIxtcEKiLtabinhmD0L7XzTuRH+XEHR +BQrxX7g0LzYS4CNX+/fKmOhFRX0aNktyywrrd8Hw80WYlDeCs/KmM+Nmb/exktYF +v5X3BLJgQKaL/cYiGlM1BAjd+V22WDCuWOIH4QLHmfrZY/mGqxIj4wLS9ST1HoQp +kQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file