前言
今天看一个群里的小伙伴在聊权限认证的有关事情。那么,也来聊一下,采用oAuth2+JWT+Security来实现权限认证的方式方法。
对于这个项目,我们将通过Spring Boot使用Spring Security 5来构建。
开发环境
- JDK 1.8
- Maven 3.0+
OAuth2 术语
- 资源所有者授权应用程序访问其帐户的用户。该访问仅限于范围。
- 资源服务器:在客户端获得访问令牌后处理已认证请求的服务器,即完成具体业务实现的服务器
- 客户端代表资源所有者访问受保护资源的应用程序。
- 授权服务器在成功验证客户端和资源所有者并授权请求后发出访问令牌的服务器。
- 访问令牌用于访问受保护资源的唯一令牌
- 范围许可
- JWTJSON Web Token是一种用于在RFC 7519中定义的在双方之间安全地表示声明的方法
- 授权类型是一种获取访问令牌的方法
依赖
<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-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>
数据库使用
为了方便演示实例,我们采用h2 database数据库
完成基础数据库创建,当然现在流行的是缓存+持久化形式实现。
CREATE TABLE IF NOT EXISTS oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256) NOT NULL, scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4000), autoapprove VARCHAR(256) ); CREATE TABLE IF NOT EXISTS oauth_client_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(256) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256) ); CREATE TABLE IF NOT EXISTS oauth_access_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(256), user_name VARCHAR(256), client_id VARCHAR(256), authentication BLOB, refresh_token VARCHAR(256) ); CREATE TABLE IF NOT EXISTS oauth_refresh_token ( token_id VARCHAR(256), token BLOB, authentication BLOB ); CREATE TABLE IF NOT EXISTS oauth_code ( code VARCHAR(256), authentication BLOB );
插入示例语句
-- 客户端信息 INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity) VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);
含义是,客户端clientID具有读写许可,授权类型为密码、刷新令牌、客户端认证 有效期300ms
完善,建表语句
CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(256) NOT NULL, password VARCHAR(256) NOT NULL, enabled TINYINT(1), UNIQUE KEY unique_username(username) ); CREATE TABLE IF NOT EXISTS authorities ( username VARCHAR(256) NOT NULL, authority VARCHAR(256) NOT NULL, PRIMARY KEY(username, authority) );
插入基础数据
-- 加密的密码是 `pass` INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1); INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');
Spring Security配置
import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import javax.sql.DataSource; @EnableWebSecurity public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { private final DataSource dataSource; private PasswordEncoder passwordEncoder; private UserDetailsService userDetailsService; public WebSecurityConfiguration(final DataSource dataSource) { this.dataSource = dataSource; } @Override protected void configure(final AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { if (passwordEncoder == null) { passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); } return passwordEncoder; } @Bean public UserDetailsService userDetailsService() { if (userDetailsService == null) { userDetailsService = new JdbcDaoImpl(); ((JdbcDaoImpl) userDetailsService).setDataSource(dataSource); } return userDetailsService; } }
使用的是Spring Boot,则将自动配置DataSource对象
由于某些自动配置的Spring @Bean需要Spring Security的AuthenticationManager,因此有必要重写authenticationManagerBean方法并将其注释为@Bean。
PasswordEncoder将由PasswordEncoderFactories.createDelegatingPasswordEncoder()处理,该处理基于前缀的一些密码编码器和委托,在我们的示例中,我们使用{bcrypt}作为密码的前缀。
授权服务器配置
授权服务器会验证客户端和用户凭据并提供令牌,在本示例中,我们将生成JSON Web Token又名JWT。
要对生成的JWT令牌进行签名,我们将使用自签名证书,并在使用Spring Configuration开始之前这样做,创建一个@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 keyStore; private String keyStorePassword; private String keyPairAlias; private String keyPairPassword; public Resource getKeyStore() { return keyStore; } public void setKeyStore(Resource keyStore) { this.keyStore = keyStore; } public String getKeyStorePassword() { return keyStorePassword; } public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; } public String getKeyPairAlias() { return keyPairAlias; } public void setKeyPairAlias(String keyPairAlias) { this.keyPairAlias = keyPairAlias; } public String getKeyPairPassword() { return keyPairPassword; } public void setKeyPairPassword(String keyPairPassword) { this.keyPairPassword = keyPairPassword; } } }
添加配置类
import com.marcosbarbero.lab.sec.oauth.jwt.config.props.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.ClientDetailsService; 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 org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import javax.sql.DataSource; import java.security.KeyPair; @Configuration @EnableAuthorizationServer @EnableConfigurationProperties(SecurityProperties.class) public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { private final DataSource dataSource; private final PasswordEncoder passwordEncoder; private final AuthenticationManager authenticationManager; private final SecurityProperties securityProperties; private JwtAccessTokenConverter jwtAccessTokenConverter; private TokenStore tokenStore; public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder, final AuthenticationManager authenticationManager, final SecurityProperties securityProperties) { this.dataSource = dataSource; this.passwordEncoder = passwordEncoder; this.authenticationManager = authenticationManager; this.securityProperties = securityProperties; } @Bean public TokenStore tokenStore() { if (tokenStore == null) { tokenStore = new JwtTokenStore(jwtAccessTokenConverter()); } return tokenStore; } @Bean public DefaultTokenServices tokenServices(final TokenStore tokenStore, final ClientDetailsService clientDetailsService) { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setSupportRefreshToken(true); tokenServices.setTokenStore(tokenStore); tokenServices.setClientDetailsService(clientDetailsService); tokenServices.setAuthenticationManager(this.authenticationManager); return tokenServices; } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { if (jwtAccessTokenConverter != null) { return jwtAccessTokenConverter; } SecurityProperties.JwtProperties jwtProperties = securityProperties.getJwt(); KeyPair keyPair = keyPair(jwtProperties, keyStoreKeyFactory(jwtProperties)); jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair); return jwtAccessTokenConverter; } @Override public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(this.dataSource); } @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) { endpoints.authenticationManager(this.authenticationManager) .accessTokenConverter(jwtAccessTokenConverter()) .tokenStore(tokenStore()); } @Override public void configure(final AuthorizationServerSecurityConfigurer oauthServer) { oauthServer.passwordEncoder(this.passwordEncoder).tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } private KeyPair keyPair(SecurityProperties.JwtProperties jwtProperties, KeyStoreKeyFactory keyStoreKeyFactory) { return keyStoreKeyFactory.getKeyPair(jwtProperties.getKeyPairAlias(), jwtProperties.getKeyPairPassword().toCharArray()); } private KeyStoreKeyFactory keyStoreKeyFactory(SecurityProperties.JwtProperties jwtProperties) { return new KeyStoreKeyFactory(jwtProperties.getKeyStore(), jwtProperties.getKeyStorePassword().toCharArray()); } }