在上边类文件中,构建了JWT所需的所有Spring @ Beans。最重要的@Bean是:JwtAccessTokenConverter,JwtTokenStore和DefaultTokenServices。
- JwtAccessTokenConverter使用自签名证书对生成的令牌进行签名。
- JwtTokenStore实现仅从令牌本身读取数据。并不是真正的商店,因为它从不持久化任何东西,并且使用JwtAccessTokenConverter生成和读取令牌。
- DefaultTokenServices使用TokenStore来保存令牌。
配置文件中配置信息
security: jwt: key-store: classpath:keystore.jks key-store-password: letmein key-pair-alias: mytestkey key-pair-password: changeme
资源服务器配置
资源服务器托管HTTP资源,该资源可以是文档,照片或其他内容。
依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> </dependencies>
定义资源保护
下面的代码定义了endpoint /me,/me返回了Principal对象,并且要求经过身份验证的用户具有ROLE USER授予权限才能访问。
import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController @RequestMapping("/me") public class UserController { @GetMapping @PreAuthorize("hasRole('ROLE_USER')") public ResponseEntity<Principal> get(final Principal principal) { return ResponseEntity.ok(principal); } }
@PreAuthorize注解在执行代码之前验证用户是否具有给定角色,以使其起作用,能够启动该注解,需要进行配置
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfiguration { }
资源服务器配置
要解码JWT令牌,必须使用授权服务器上使用的自签名证书中的公钥对令牌进行签名,为此,我们首先创建一个@ConfigurationProperties类以绑定配置属性。
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; @ConfigurationProperties("security") public class SecurityProperties { private JwtProperties jwt; public JwtProperties getJwt() { return jwt; } public void setJwt(JwtProperties jwt) { this.jwt = jwt; } public static class JwtProperties { private Resource publicKey; public Resource getPublicKey() { return publicKey; } public void setPublicKey(Resource publicKey) { this.publicKey = publicKey; } } }
使用以下命令从生成的JKS导出公钥:
$ keytool -list -rfc --keystore keystore.jks | openssl x509 -inform pem -pubkey -noout
生成如下示例的代码块
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmWI2jtKwvf0W1hdMdajc h+mFx9FZe3CZnKNvT/d0+2O6V1Pgkz7L2FcQx2uoV7gHgk5mmb2MZUsy/rDKj0dM fLzyXqBcCRxD6avALwu8AAiGRxe2dl8HqIHyo7P4R1nUaea1WCZB/i7AxZNAQtcC cSvMvF2t33p3vYXY6SqMucMD4yHOTXexoWhzwRqjyyC8I8uCYJ+xIfQvaK9Q1RzK Rj99IRa1qyNgdeHjkwW9v2Fd4O/Ln1Tzfnk/dMLqxaNsXPw37nw+OUhycFDPPQF/ H4Q4+UDJ3ATf5Z2yQKkUQlD45OO2mIXjkWprAmOCi76dLB2yzhCX/plGJwcgb8XH EQIDAQAB -----END PUBLIC KEY-----
将其复制到public.txt文件并将其放置在/ src / main / resources中,然后配置指向该文件的application.yml:
security: jwt: public-key: classpath:public.txt
增加配置内容
import org.apache.commons.io.IOUtils; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import java.io.IOException; import static java.nio.charset.StandardCharsets.UTF_8; @Configuration @EnableResourceServer @EnableConfigurationProperties(SecurityProperties.class) public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { private static final String ROOT_PATTERN = "/**"; private final SecurityProperties securityProperties; private TokenStore tokenStore; public ResourceServerConfiguration(final SecurityProperties securityProperties) { this.securityProperties = securityProperties; } @Override public void configure(final ResourceServerSecurityConfigurer resources) { resources.tokenStore(tokenStore()); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(HttpMethod.GET, ROOT_PATTERN).access("#oauth2.hasScope('read')") .antMatchers(HttpMethod.POST, ROOT_PATTERN).access("#oauth2.hasScope('write')") .antMatchers(HttpMethod.PATCH, ROOT_PATTERN).access("#oauth2.hasScope('write')") .antMatchers(HttpMethod.PUT, ROOT_PATTERN).access("#oauth2.hasScope('write')") .antMatchers(HttpMethod.DELETE, ROOT_PATTERN).access("#oauth2.hasScope('write')"); } @Bean public DefaultTokenServices tokenServices(final TokenStore tokenStore) { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore); return tokenServices; } @Bean public TokenStore tokenStore() { if (tokenStore == null) { tokenStore = new JwtTokenStore(jwtAccessTokenConverter()); } return tokenStore; } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPublicKeyAsString()); return converter; } private String getPublicKeyAsString() { try { return IOUtils.toString(securityProperties.getJwt().getPublicKey().getInputStream(), UTF_8); } catch (IOException e) { throw new RuntimeException(e); } } }
此配置的重要部分是三个@Bean:JwtAccessTokenConverter,TokenStore和DefaultTokenServices:
- JwtAccessTokenConverter使用JKS公钥。
- JwtTokenStore使用JwtAccessTokenConverter读取令牌。
- DefaultTokenServices使用JwtTokenStore来保留令牌。
集成测试
为了一起测试,我们需要同时启动Authorization Server和Resource Server,在我的设置中它将分别在9000端口和9100端口上运行。
$ curl -u clientId:secret -X POST localhost:9000/oauth/token\?grant_type=password\&username=user\&password=pass { "access_token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDgxODk0NDUsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiYjFjYWQ3MTktZTkwMS00Njk5LTlhOWEtYTIwYzk2NDM5NjAzIiwiY2xpZW50X2lkIjoiY2xpZW50SWQiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.LkQ3KAj2kPY7yKmwXlhIFaHtt-31mJGWPb-_VpC8PWo9IBUpZQxg76WpahBJjet6O1ICx8b5Ab2CxH7ErTl0tL1jk5VZ_kp66E9E7bUQn-C09CY0fqxAan3pzpGrJsUvcR4pzyzLoRCuAqVRF5K2mdDQUZ8NaP0oXeVRuxyRdgjwMAkQGHpFC_Fk-7Hbsq2Y0GikD0UdkaH2Ey_vVyKy5aj3NrAZs62KFvQfSbifxd4uBHzUJSkiFE2Cx3u1xKs3W2q8MladwMwlQmWJROH6lDjQiybUZOEhJaktxQYGAinScnm11-9WOdaqohcr65PAQt48__rMRi0TUgvsxpz6ow", "token_type" : "bearer", "refresh_token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl0sImF0aSI6ImIxY2FkNzE5LWU5MDEtNDY5OS05YTlhLWEyMGM5NjQzOTYwMyIsImV4cCI6MTU1MDc4MTE0NSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6Ijg2OWFjZjM2LTJiODAtNGY5Ni04MzUwLTA5NTgyMzE3NTAzMCIsImNsaWVudF9pZCI6ImNsaWVudElkIn0.TDQwUNb627-f0-Cjn1vWZXFpzZSGpeKZq85ivA9zY_atOXM2WfjOxTLE6phnNLevjLSNAGrx1skm_sx6leQlrrmDi36nwiR7lvhv8xMbn1DkF5KaoWPhldW7GHsSIiauMu_cJ5Kmq89ZOEOlxYoXlLwfWYo75ISkKNYqko98yDogGrRAJxtc1aKIBLypLchhoCf8w43efd11itwvBdaLIb5ACfN30kztUqQtbeL8voQP6tOsRZbCgbOOKMTulOCRyBvaora4GJDV2qdvXdCUT-kORKDj9liqt2ae7OJzb2FuuXCGqBUrxYYK-H-wdwh7XFkXVe74Lev9YDUbyEmDHg", "expires_in" : 299, "scope" : "read write", "jti" : "b1cad719-e901-4699-9a9a-a20c96439603" }
访问资源
现在您已经生成了令牌,复制访问令牌并将其添加到授权HTTP标头上的请求中,例如:
curl localhost:9100/me -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDgxODk0NDUsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiYjFjYWQ3MTktZTkwMS00Njk5LTlhOWEtYTIwYzk2NDM5NjAzIiwiY2xpZW50X2lkIjoiY2xpZW50SWQiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.LkQ3KAj2kPY7yKmwXlhIFaHtt-31mJGWPb-_VpC8PWo9IBUpZQxg76WpahBJjet6O1ICx8b5Ab2CxH7ErTl0tL1jk5VZ_kp66E9E7bUQn-C09CY0fqxAan3pzpGrJsUvcR4pzyzLoRCuAqVRF5K2mdDQUZ8NaP0oXeVRuxyRdgjwMAkQGHpFC_Fk-7Hbsq2Y0GikD0UdkaH2Ey_vVyKy5aj3NrAZs62KFvQfSbifxd4uBHzUJSkiFE2Cx3u1xKs3W2q8MladwMwlQmWJROH6lDjQiybUZOEhJaktxQYGAinScnm11-9WOdaqohcr65PAQt48__rMRi0TUgvsxpz6ow" { "authorities" : [ { "authority" : "ROLE_GUEST" } ], "details" : { "remoteAddress" : "127.0.0.1", "sessionId" : null, "tokenValue" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDgyMzcxNDEsInVzZXJfbmFtZSI6Imd1ZXN0IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9HVUVTVCJdLCJqdGkiOiIzNDk1ODE1MC0wOGJkLTQwMDYtYmNhMC1lM2RkYjAxMGU2NjUiLCJjbGllbnRfaWQiOiJjbGllbnRJZCIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdfQ.WUwAh-aKgh_Bqk-a9ijw67EI6H8gFrb3D_WdwlEcITskIybhacHjT6E7cUXjdBT7GCRvvJ-yxzFJIQyI6y0t61SInpqVG2GlAwtTxR5reG0e4ZtcKoq2rbQghK8hWenGplGT31kjDY78zZv-WqCAc0-MM4cC06fTXFzdhsdueY789lCasSD4WMMC6bWbN098lHF96rMpCdlW13EalrPgcKeuvZtUBrC8ntL8Bg3LRMcU1bFKTRAwlVxw1aYyqeEN4NSxkiSgQod2dltA-b3c15L-fXoOWNGnPB68hqgK48ymuemRQTSg3eKmHFAQdDL6pxQ8_D_ZWAL3QhsKQVGDKg", "tokenType" : "Bearer", "decodedDetails" : null }, "authenticated" : true, "userAuthentication" : { "authorities" : [ { "authority" : "ROLE_GUEST" } ], "details" : null, "authenticated" : true, "principal" : "guest", "credentials" : "N/A", "name" : "guest" }, "credentials" : "", "principal" : "guest", "clientOnly" : false, "oauth2Request" : { "clientId" : "clientId", "scope" : [ "read", "write" ], "requestParameters" : { "client_id" : "clientId" }, "resourceIds" : [ ], "authorities" : [ ], "approved" : true, "refresh" : false, "redirectUri" : null, "responseTypes" : [ ], "extensions" : { }, "grantType" : null, "refreshTokenRequest" : null }, "name" : "guest" }
通过示例,演示了配合使用。