mirror of https://github.com/halo-dev/halo
feat: add token provider (#1841)
parent
b3c82396ac
commit
90d61a27e9
|
@ -42,6 +42,9 @@ bootJar {
|
|||
}
|
||||
|
||||
ext['h2.version'] = '2.1.210'
|
||||
ext {
|
||||
commonsLang3 = "3.12.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
@ -54,6 +57,9 @@ dependencies {
|
|||
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
||||
implementation "org.flywaydb:flyway-core"
|
||||
implementation "org.flywaydb:flyway-mysql"
|
||||
|
||||
implementation "org.apache.commons:commons-lang3:$commonsLang3"
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
|
||||
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package run.halo.app.identity.authentication;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @date 2022-04-12
|
||||
*/
|
||||
@Data
|
||||
public class AccessToken implements Serializable {
|
||||
private String tokenType;
|
||||
|
||||
private Jwt accessToken;
|
||||
|
||||
private Jwt refreshToken;
|
||||
private Map<String, Object> additionalInformation;
|
||||
|
||||
@JsonIgnore
|
||||
private long expiration;
|
||||
|
||||
public AccessToken(Jwt accessToken) {
|
||||
this.tokenType = "Bearer".toLowerCase();
|
||||
this.additionalInformation = Collections.emptyMap();
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return this.expiration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package run.halo.app.identity.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.infra.properties.JwtProperties;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
|
||||
/**
|
||||
* Jwt token utils.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2022-04-12
|
||||
*/
|
||||
@Component
|
||||
@EnableConfigurationProperties(JwtProperties.class)
|
||||
public class JwtTokenProvider implements Serializable {
|
||||
|
||||
private final JwtEncoder jwtEncoder;
|
||||
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
public JwtTokenProvider(JwtEncoder jwtEncoder, JwtDecoder jwtDecoder,
|
||||
JwtProperties jwtProperties) {
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
this.jwtDecoder = jwtDecoder;
|
||||
this.jwtProperties = jwtProperties;
|
||||
}
|
||||
|
||||
private JwtClaimsSet createJwt(Authentication authentication, Instant expireAt) {
|
||||
String scope = authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.joining(" "));
|
||||
return JwtClaimsSet.builder()
|
||||
// JWT ID (jti)
|
||||
.id(HaloUtils.simpleUUID())
|
||||
// 签发者
|
||||
.issuer(StringUtils.defaultIfBlank(jwtProperties.getIssuerUri(),
|
||||
"https://halo.run"))
|
||||
.issuedAt(Instant.now())
|
||||
// Authentication#getName maps to the JWT’s sub property, if one is present.
|
||||
.subject(authentication.getName())
|
||||
// expiration time (exp) claim
|
||||
.expiresAt(expireAt)
|
||||
.claim("scope", scope)
|
||||
.build();
|
||||
}
|
||||
|
||||
public AccessToken getToken(Authentication authentication) {
|
||||
Instant expireAt = Instant.now().plusMillis(SecurityConstant.EXPIRATION_TIME);
|
||||
JwtClaimsSet tokenClaimsSet = createJwt(authentication, expireAt);
|
||||
Jwt token = jwtEncoder.encode(JwtEncoderParameters.from(tokenClaimsSet));
|
||||
|
||||
JwtClaimsSet refreshTokenClaimsSet =
|
||||
createJwt(authentication, expireAt.plus(30, ChronoUnit.MINUTES));
|
||||
Jwt refreshToken = jwtEncoder.encode(JwtEncoderParameters.from(refreshTokenClaimsSet));
|
||||
|
||||
AccessToken accessToken = new AccessToken(token);
|
||||
accessToken.setRefreshToken(refreshToken);
|
||||
accessToken.setExpiration(expireAt.toEpochMilli());
|
||||
accessToken.setTokenType(SecurityConstant.TOKEN_PREFIX);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public Jwt verify(String token) {
|
||||
try {
|
||||
return jwtDecoder.decode(token);
|
||||
} catch (JwtException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package run.halo.app.identity.authentication;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @date 2022-04-13
|
||||
*/
|
||||
public interface SecurityConstant {
|
||||
|
||||
/**
|
||||
* 30 mins
|
||||
*/
|
||||
long EXPIRATION_TIME = 1_800_000;
|
||||
|
||||
/**
|
||||
* Token prefix
|
||||
*/
|
||||
String TOKEN_PREFIX = "Bearer";
|
||||
|
||||
/**
|
||||
* Authentication header
|
||||
*/
|
||||
String HEADER_STRING = "Authorization";
|
||||
|
||||
/**
|
||||
* login uri
|
||||
*/
|
||||
String LOGIN_URL = "/api/v1/oauth/login";
|
||||
}
|
|
@ -20,6 +20,12 @@ import org.springframework.util.StreamUtils;
|
|||
@ConfigurationProperties(prefix = "halo.security.oauth2.jwt")
|
||||
public class JwtProperties {
|
||||
|
||||
/**
|
||||
* URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0
|
||||
* Authorization Server Metadata endpoint defined by RFC 8414.
|
||||
*/
|
||||
private String issuerUri;
|
||||
|
||||
/**
|
||||
* JSON Web Algorithm used for verifying the digital signatures.
|
||||
*/
|
||||
|
@ -32,6 +38,14 @@ public class JwtProperties {
|
|||
|
||||
private Resource privateKeyLocation;
|
||||
|
||||
public String getIssuerUri() {
|
||||
return issuerUri;
|
||||
}
|
||||
|
||||
public void setIssuerUri(String issuerUri) {
|
||||
this.issuerUri = issuerUri;
|
||||
}
|
||||
|
||||
public String getJwsAlgorithm() {
|
||||
return this.jwsAlgorithm;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -11,4 +12,8 @@ public class HaloUtils {
|
|||
String requestedWith = request.getHeader("x-requested-with");
|
||||
return requestedWith == null || requestedWith.equalsIgnoreCase("XMLHttpRequest");
|
||||
}
|
||||
|
||||
public static String simpleUUID() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package run.halo.app;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import run.halo.app.identity.authentication.AccessToken;
|
||||
import run.halo.app.identity.authentication.JwtTokenProvider;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @date 2022-04-13
|
||||
*/
|
||||
@WithMockUser(username = "test", password = "test")
|
||||
@TestPropertySource(properties = {"halo.security.oauth2.jwt.public-key-location=classpath:app.pub",
|
||||
"halo.security.oauth2.jwt.private-key-location=classpath:app.key"})
|
||||
@SpringBootTest
|
||||
public class JwtTokenProviderTest {
|
||||
|
||||
@Autowired
|
||||
private JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
private AccessToken accessToken;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
accessToken = jwtTokenProvider.getToken(authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createJwt() throws MalformedURLException {
|
||||
Jwt token = accessToken.getAccessToken();
|
||||
Jwt refreshToken = accessToken.getRefreshToken();
|
||||
System.out.println(token.getTokenValue());
|
||||
assertThat(token.getClaims()).isNotNull()
|
||||
.containsEntry("sub", "test")
|
||||
.containsEntry("scope", "ROLE_USER")
|
||||
.containsEntry("iss", new URL("https://halo.run"))
|
||||
.containsKey("iss")
|
||||
.containsKey("jti");
|
||||
|
||||
assertThat(refreshToken.getClaims())
|
||||
.isNotNull()
|
||||
.containsEntry("sub", "test")
|
||||
.containsKey("iss")
|
||||
.containsKey("jti");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyBadToken() {
|
||||
Jwt badToken = jwtTokenProvider.verify("badToken");
|
||||
assertThat(badToken).isNull();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue