mirror of https://github.com/halo-dev/halo
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 <johnniang@fastmail.com> * refactor: mvc path prefix config Co-authored-by: John Niang <johnniang@fastmail.com>pull/1841/head
parent
e297b0efcf
commit
b3c82396ac
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-mail'
|
implementation 'org.springframework.boot:spring-boot-starter-mail'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
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-thymeleaf'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
||||||
|
@ -55,6 +56,8 @@ dependencies {
|
||||||
implementation "org.flywaydb:flyway-mysql"
|
implementation "org.flywaydb:flyway-mysql"
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
|
||||||
|
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
|
||||||
|
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
|
|
||||||
runtimeOnly "com.h2database:h2"
|
runtimeOnly "com.h2database:h2"
|
||||||
|
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<SecurityContext> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -39,6 +39,12 @@ spring:
|
||||||
locations:
|
locations:
|
||||||
- classpath:db/migration/common
|
- classpath:db/migration/common
|
||||||
- classpath:db/migration/{vendor}
|
- classpath:db/migration/{vendor}
|
||||||
|
halo:
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
jwt:
|
||||||
|
public-key-location: classpath:app.pub
|
||||||
|
private-key-location: classpath:app.key
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
run.halo.app: DEBUG
|
run.halo.app: DEBUG
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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-----
|
|
@ -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-----
|
Loading…
Reference in New Issue