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, "1xxxxxxxxxx", "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;