Spring Security OAuth2研究(二) — OAuth2密碼授權模式
一 、項目搭建
引入依賴
SpringCloud
版本 — Hoxton.SR3
SpringBoot 2.2.6.RELEASE
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--web 模塊-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--undertow容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!--緩存依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
SpringBoot
版本 SpringBoot 2.2.6.RELEASE
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--web 模塊-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--undertow容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!--緩存依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
YAML配置文件
server:
port: 48888
tomcat:
uri-encoding: utf-8
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/markerccc?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
application:
name: auth
redis:
database: 0
host: localhost
password:
port: 6379
timeout: 10000
lettuce:
pool:
max-active: 8
max-idle: 8
max-wait: 1ms
min-idle: 0
shutdown-timeout: 100ms
二、書寫代碼
授權服務器配置
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* <p>
* Description:
*
* Parameter 0 of constructor in com.example.demo.config.AuthorizationServerConfig required a single bean, but 2 were found:
* - markClientDetailsServiceImpl: defined in file [E:\IdeaProject\demoAUth\target\classes\com\example\demo\service\MarkClientDetailsServiceImpl.class]
* - clientDetailsService: defined in BeanDefinition defined in class path resource [org/springframework/security/oauth2/config/annotation/configuration/ClientDetailsServiceConfiguration.class]
* </p>
*/
/**
* 總結: 這裏的兩個bean ClientDetailsService UserDetailsService 請務必與你手擼的名字保持一致, 這裏注入的名稱請保持與你的類名保持一致, 不然會出現上面的錯誤
*/
private final ClientDetailsService markClientDetailsServiceImpl;
private final AuthenticationManager authenticationManagerBean;
private final RedisConnectionFactory redisConnectionFactory;
private final UserDetailsService userDetailsServiceImpl;
// private final TokenEnhancer pigxTokenEnhancer;
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
clients.withClientDetails(markClientDetailsServiceImpl);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.tokenStore(tokenStore())
// token增強, 如果需要自己擴展 只需要注入
// org.springframework.security.oauth2.provider.token.TokenEnhancer;
// .tokenEnhancer(tokenEnhancer)
.userDetailsService(userDetailsServiceImpl)
.authenticationManager(authenticationManagerBean)
.reuseRefreshTokens(false);
}
@Bean
public TokenStore tokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix("markerccc_abc:");
tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() {
@Override
public String extractKey(OAuth2Authentication authentication) {
return super.extractKey(authentication) + StrUtil.COLON + "1";
}
});
return tokenStore;
}
}
Web安全配置適配器
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@Primary
@Order(90)
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private MobileSecurityConfigurer mobileSecurityConfigurer;
@Override
@SneakyThrows
protected void configure(HttpSecurity http) {
http
.formLogin()
// .loginPage("/token/login")
// .loginProcessingUrl("/token/form")
// .failureHandler(authenticationFailureHandler())
.and()
.logout()
.logoutSuccessHandler((request, response, authentication) -> {
String referer = request.getHeader(HttpHeaders.REFERER);
response.sendRedirect(referer);
})
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
.authorizeRequests()
.antMatchers(
"/token/**",
"/actuator/**",
"/mobile/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
// 這裏是我做手機號登錄的配置處理器, 這裏你們可以先去掉
// .apply(mobileSecurityConfigurer);
}
/**
* 不攔截靜態資源
*
* @param web
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/css/**");
}
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() {
return super.authenticationManagerBean();
}
/**
* https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-updated Encoded password does not look like
* BCrypt
* 這裏的密碼加密模式請參考上面的鏈接Spring說的很清楚
*
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
創建Redis配置類
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnMissingBean(CacheManagerCustomizers.class)
public class RedisCacheManagerConfig {
@Bean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
return new CacheManagerCustomizers(customizers.getIfAvailable());
}
}
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
@AllArgsConstructor
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
創建User類
import java.util.Collection;
import lombok.Getter;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.User;
public class MarkCCCUser extends User {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 用戶ID
*/
@Getter
private Integer id;
/**
* 部門ID
*/
@Getter
private Integer deptId;
/**
* 手機號
*/
@Getter
private String phone;
/**
* 頭像
*/
@Getter
private String avatar;
/**
* Construct the <code>User</code> with the details required by
* {@link DaoAuthenticationProvider}.
*
* @param id 用戶ID
* @param deptId 部門ID
* @param tenantId 租戶ID
* @param username the username presented to the
* <code>DaoAuthenticationProvider</code>
* @param password the password that should be presented to the
* <code>DaoAuthenticationProvider</code>
* @param enabled set to <code>true</code> if the user is enabled
* @param accountNonExpired set to <code>true</code> if the account has not expired
* @param credentialsNonExpired set to <code>true</code> if the credentials have not
* expired
* @param accountNonLocked set to <code>true</code> if the account is not locked
* @param authorities the authorities that should be granted to the caller if they
* presented the correct username and password and the user is enabled. Not null.
* @throws IllegalArgumentException if a <code>null</code> value was passed either as
* a parameter or as an element in the <code>GrantedAuthority</code> collection
*/
public MarkCCCUser(Integer id, Integer deptId, String phone, String avatar, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
this.deptId = deptId;
this.phone = phone;
this.avatar = avatar;
}
}
創建UserDetailsServiceImpl類
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class MarkUserDetailsServiceImpl implements MarkUserDetailsService {
// private final RemoteUserService remoteUserService;
private final CacheManager cacheManager;
/**
* 用戶密碼登錄
*
* @param username 用戶名
* @return
* @throws UsernameNotFoundException
*/
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String username) {
// 查詢用戶具體實現, 自己去實現
Set<String> dbAuthsSet = new HashSet<>();
Collection<? extends GrantedAuthority> authorities
= AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(new String[0]));
return new MarkCCCUser(1, 1, "15072146145", "1", 1, "markerccc", "{noop}123456", true, true, true, true, authorities);
}
}
創建ClientDetailsService的實現類
@Slf4j
@Service
public class MarkClientDetailsServiceImpl extends JdbcClientDetailsService {
public MarkClientDetailsServiceImpl(DataSource dataSource) {
super(dataSource);
}
/**
* 重寫原生方法支持redis緩存
*
* @param clientId
* @return ClientDetails
* @throws InvalidClientException
*/
@Override
@Cacheable(value = "mark_oauth:client:details", key = "#clientId", unless = "#result == null")
public ClientDetails loadClientByClientId(String clientId) {
super.setSelectClientDetailsSql(String.format("請自己引用下面的SQL", "1"));
return super.loadClientByClientId(clientId);
}
}
SELECT client_id,
CONCAT('{noop}', client_secret) AS client_secret,
resource_ids,
scope,
authorized_grant_types,
web_server_redirect_uri,
authorities,
access_token_validity,
refresh_token_validity,
additional_information,
autoapprove
FROM sys_oauth_client_details
WHERE client_id = ?
AND del_flag = 0
AND tenant_id = %s
創建Application類
@SpringCloudApplication
// @SpringBootApplication 使用SpringBoot時請用這個註解
public class OAuth2Application {
public static void main(String[] args) {
SpringApplication.run(OAuth2Application.class, args);
}
}
三、開始測試
請求路徑
使用postman
請求路徑 localhost:48888/oauth/token
參數
header | 請求頭 |
---|---|
Authorization |
Basic client_id:client_secret |
client_id:client_secret 這裏需要變成Base64 加密 |
|
這個數據存於sys_oauth_client_details 由ClientDetailsService 類查詢而出, 具體實現爲MarkClientDetailsServiceImpl |
|
form-data | 表單 |
grant_type |
授權模式, 存在於sys_oauth_client_details 的 authorized_grant_types 字段中 |
username |
用戶名, 存在於用戶表中 |
password |
密碼, 存在於用戶表中 |
x-www-form-urlencoded | 表單 |
grant_type |
授權模式, 存在於sys_oauth_client_details 的 authorized_grant_types 字段中 |
username |
用戶名, 存在於用戶表中 |
password |
密碼, 存在於用戶表中 |
請求流程 | |
---|---|
請求攔截 | |
BasicAuthenticationFilter |
Basic認證攔截器 |
ClientDetailsService |
client 查詢接口 |
InMemoryClientDetailsService |
從內存中查詢client , 實現ClientDetailsService |
JdbcClientDetailsService |
從數據庫中查詢client , 實現ClientDetailsService |
MarkClientDetailsServiceImpl |
實現JdbcClientDetailsService |
請求開始 | |
AbstractEndpoint |
實現InitializingBean |
AuthorizationEndpoint |
繼承AbstractEndpoint 這裏不是重點 |
TokenEndpoint |
繼承AbstractEndpoint 這個爲密碼授權模式的入口 |
TokenEndpoint.postAccessToken() |
該方法上有@RequestMapping(value = "/oauth/token", method=RequestMethod.POST) 這個註解 |
ClientDetailsService.loadClientByClientId() |
查詢sys_oauth_client_details 信息, 位於postAccessToken() 的96行 |
getTokenGranter().grant() |
進行授權, 位於postAccessToken() 的132行, ``TokenGranter拿到的是 TokenGranter` |
TokenGranter |
授權接口 |
AbstractTokenGranter |
實現TokenGranter |
getAccessToken(client, tokenRequest) |
|
ResourceOwnerPasswordTokenGranter |
繼承AbstractTokenGranter |
getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) |
由getAccessToken(client, tokenRequest) 調用 |
authenticationManager.authenticate(userAuth) |
|
AuthenticationManager |
認證管理器 |
ProviderManager |
|
provider.authenticate(authentication) |
|
AbstractUserDetailsAuthenticationProvider |
由ProviderManager 175行調用 |
retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication) |
由AbstractUserDetailsAuthenticationProvider 144行調用 |
DaoAuthenticationProvider |
|
this.getUserDetailsService().loadUserByUsername(username) |
查詢用戶信息 |
MarkUserDetailsServiceImpl |
調用由我們重寫的方法查詢用戶 |
四、表結構
/*
Navicat Premium Data Transfer
Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 50729
Source Host : localhost:3306
Source Schema : markerccc
Target Server Type : MySQL
Target Server Version : 50729
File Encoding : 65001
Date: 28/04/2020 14:57:54
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `sys_oauth_client_details`;
CREATE TABLE `sys_oauth_client_details` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`client_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0',
`tenant_id` int(11) NOT NULL DEFAULT 0 COMMENT '所屬租戶',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '終端信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_oauth_client_details
-- ----------------------------
INSERT INTO `sys_oauth_client_details` VALUES (1, 'app', NULL, 'app', 'server', 'password,refresh_token,authorization_code,client_credentials,implicit', NULL, NULL, 43200, 2592001, NULL, 'true', '0', 1);
INSERT INTO `sys_oauth_client_details` VALUES (2, 'daemon', NULL, 'daemon', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', '0', 1);
INSERT INTO `sys_oauth_client_details` VALUES (3, 'gen', NULL, 'gen', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', '0', 1);
INSERT INTO `sys_oauth_client_details` VALUES (4, 'mp', NULL, 'mp', 'server', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, 'true', '0', 1);
INSERT INTO `sys_oauth_client_details` VALUES (5, 'test', NULL, 'test', 'server', 'password,refresh_token,authorization_code,client_credentials', NULL, NULL, NULL, NULL, NULL, 'false', '0', 1);
SET FOREIGN_KEY_CHECKS = 1;