feat: add token provider (#1841)

pull/1844/head
guqing 2022-04-13 19:38:34 +08:00 committed by GitHub
parent b3c82396ac
commit 90d61a27e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 238 additions and 0 deletions

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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 JWTs 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;
}
}
}

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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("-", "");
}
}

View File

@ -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();
}
}